diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt index 5af4fdae5..b48c5eb1c 100644 --- a/src/iceberg/CMakeLists.txt +++ b/src/iceberg/CMakeLists.txt @@ -167,7 +167,9 @@ set(ICEBERG_SOURCES util/truncate_util.cc util/type_util.cc util/url_encoder.cc - util/uuid.cc) + util/uuid.cc + view_metadata.cc + view_version.cc) set(ICEBERG_STATIC_BUILD_INTERFACE_LIBS) set(ICEBERG_SHARED_BUILD_INTERFACE_LIBS) diff --git a/src/iceberg/cpp20_compatibility_internal.h b/src/iceberg/cpp20_compatibility_internal.h index 9f5d24515..0c35ca111 100644 --- a/src/iceberg/cpp20_compatibility_internal.h +++ b/src/iceberg/cpp20_compatibility_internal.h @@ -201,6 +201,8 @@ #include "iceberg/util/uuid.h" #include "iceberg/util/visit_type.h" #include "iceberg/util/visitor_generate.h" +#include "iceberg/view_metadata.h" +#include "iceberg/view_version.h" #ifdef ICEBERG_PUBLIC_HEADERS_BUNDLE # include "iceberg/arrow/arrow_io_util.h" diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt index 37e20e339..95b2c4d9e 100644 --- a/src/iceberg/test/CMakeLists.txt +++ b/src/iceberg/test/CMakeLists.txt @@ -127,6 +127,8 @@ add_iceberg_test(json_serde_test add_iceberg_test(scheduled_executor_test SOURCES scheduled_executor_test.cc) set_tests_properties(scheduled_executor_test PROPERTIES TIMEOUT 60) +add_iceberg_test(view_metadata_test SOURCES view_metadata_test.cc) + add_iceberg_test(util_test USE_DATA SOURCES diff --git a/src/iceberg/test/view_metadata_test.cc b/src/iceberg/test/view_metadata_test.cc new file mode 100644 index 000000000..51228755f --- /dev/null +++ b/src/iceberg/test/view_metadata_test.cc @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/view_metadata.h" + +#include +#include +#include + +#include + +#include "iceberg/schema.h" +#include "iceberg/schema_field.h" +#include "iceberg/test/matchers.h" +#include "iceberg/type.h" + +namespace iceberg { +namespace { + +ViewRepresentation SqlRepr(std::string sql, std::string dialect) { + return SqlViewRepresentation{.sql = std::move(sql), .dialect = std::move(dialect)}; +} + +std::shared_ptr MakeSchema(int32_t schema_id) { + return std::make_shared( + std::vector{SchemaField::MakeRequired(1, "customer_id", int64()), + SchemaField::MakeOptional(2, "country", string())}, + schema_id); +} + +ViewVersion MakeVersion(int32_t version_id, int32_t schema_id, + std::vector representations) { + return ViewVersion{ + .version_id = version_id, + .schema_id = schema_id, + .timestamp_ms = TimePointMs{std::chrono::milliseconds(1600000000000)}, + .summary = {{"engine-name", "test"}}, + .representations = std::move(representations), + .default_catalog = "prod", + .default_namespace = Namespace{{"analytics"}}, + }; +} + +ViewMetadata MakeValidMetadata() { + ViewMetadata metadata; + metadata.format_version = ViewMetadata::kDefaultViewFormatVersion; + metadata.view_uuid = "fa6506c3-7681-40c8-86dc-e36561f83385"; + metadata.location = "s3://bucket/warehouse/analytics.db/active_customers"; + metadata.schemas = {MakeSchema(0)}; + metadata.current_version_id = 0; + metadata.versions = {std::make_shared( + MakeVersion(0, 0, {SqlRepr("SELECT customer_id FROM customers", "spark")}))}; + metadata.version_log = {ViewHistoryEntry{ + .timestamp_ms = TimePointMs{std::chrono::milliseconds(1600000000000)}, + .version_id = 0, + }}; + metadata.properties = {{"comment", "active customers"}}; + return metadata; +} + +TEST(ViewMetadataTest, ValidateAcceptsValidMetadata) { + ViewMetadata metadata = MakeValidMetadata(); + EXPECT_THAT(metadata.Validate(), IsOk()); + + auto current = metadata.CurrentVersion(); + ASSERT_THAT(current, IsOk()); + EXPECT_EQ(current->get()->version_id, 0); + + auto current_schema = metadata.CurrentSchema(); + ASSERT_THAT(current_schema, IsOk()); + EXPECT_EQ(current_schema->get()->schema_id(), 0); +} + +TEST(ViewMetadataTest, ValidateAcceptsUnknownRepresentation) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations.push_back( + UnknownViewRepresentation{.type = "cypher"}); + EXPECT_THAT(metadata.Validate(), IsOk()); +} + +TEST(ViewMetadataTest, ValidateAcceptsMultipleVersionsAndDialects) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.schemas.push_back(MakeSchema(1)); + metadata.current_version_id = 1; + metadata.versions[0]->representations.push_back( + SqlRepr("SELECT customer_id, country FROM customers WHERE active = true", "trino")); + metadata.versions.push_back(std::make_shared(MakeVersion( + 1, 1, + {SqlRepr("SELECT customer_id, country FROM customers WHERE active = true", + "Trino")}))); + metadata.version_log.push_back(ViewHistoryEntry{ + .timestamp_ms = TimePointMs{std::chrono::milliseconds(1600000001000)}, + .version_id = 1, + }); + EXPECT_THAT(metadata.Validate(), IsOk()); + + auto current_schema = metadata.CurrentSchema(); + ASSERT_THAT(current_schema, IsOk()); + EXPECT_EQ(current_schema->get()->schema_id(), 1); +} + +TEST(ViewMetadataTest, ValidateRejectsUnsupportedFormatVersion) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.format_version = 2; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsEmptyUuid) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.view_uuid.clear(); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsEmptyLocation) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.location.clear(); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsMissingSchemas) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.schemas.clear(); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsNullSchema) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.schemas.push_back(nullptr); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsDuplicateSchemaIds) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.schemas.push_back(MakeSchema(0)); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsMissingVersions) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions.clear(); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsNullVersion) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions.push_back(nullptr); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsDuplicateVersionIds) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions.push_back(std::make_shared( + MakeVersion(0, 0, {SqlRepr("SELECT customer_id FROM customers", "trino")}))); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsUnlistedCurrentVersion) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.current_version_id = 7; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); + + auto current = metadata.CurrentVersion(); + EXPECT_THAT(current, IsError(ErrorKind::kNotFound)); +} + +TEST(ViewMetadataTest, ValidateRejectsUnlistedSchemaReference) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->schema_id = 5; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsEmptyDefaultNamespace) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->default_namespace = Namespace{}; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsDuplicateDialects) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations.push_back( + SqlRepr("SELECT customer_id FROM customers", "spark")); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsDuplicateDialectsWithCaseFolding) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations.push_back( + SqlRepr("SELECT customer_id FROM customers", "SPARK")); + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsSqlRepresentationWithoutSql) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations = {SqlRepr("", "spark")}; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsSqlRepresentationWithoutDialect) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations = {SqlRepr("SELECT 1", "")}; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, ValidateRejectsUnknownRepresentationWithoutType) { + ViewMetadata metadata = MakeValidMetadata(); + metadata.versions[0]->representations = {UnknownViewRepresentation{.type = ""}}; + EXPECT_THAT(metadata.Validate(), IsError(ErrorKind::kInvalid)); +} + +TEST(ViewMetadataTest, AccessorsReturnNotFoundForMissingEntries) { + ViewMetadata metadata = MakeValidMetadata(); + EXPECT_THAT(metadata.VersionById(999), IsError(ErrorKind::kNotFound)); + EXPECT_THAT(metadata.SchemaById(999), IsError(ErrorKind::kNotFound)); + + metadata.versions.push_back(nullptr); + EXPECT_THAT(metadata.VersionById(999), IsError(ErrorKind::kNotFound)); +} + +TEST(ViewMetadataTest, EqualityComparesAllFields) { + ViewMetadata lhs = MakeValidMetadata(); + ViewMetadata rhs = MakeValidMetadata(); + EXPECT_EQ(lhs, rhs); + + rhs.properties.at("comment") = "changed"; + EXPECT_NE(lhs, rhs); + + ViewMetadata changed = MakeValidMetadata(); + changed.versions[0]->representations.push_back( + UnknownViewRepresentation{.type = "cypher"}); + EXPECT_NE(lhs, changed); + + ViewMetadata unknown = lhs; + unknown.versions[0]->representations.push_back( + UnknownViewRepresentation{.type = "cypher"}); + EXPECT_EQ(unknown, changed); +} + +TEST(ViewVersionTest, EqualityComparesAllFields) { + ViewVersion lhs = MakeVersion(0, 0, {SqlRepr("SELECT 1", "spark")}); + ViewVersion rhs = MakeVersion(0, 0, {SqlRepr("SELECT 1", "spark")}); + EXPECT_EQ(lhs, rhs); + + rhs.representations[0] = SqlRepr("SELECT 1", "trino"); + EXPECT_NE(lhs, rhs); + + rhs.representations[0] = SqlRepr("SELECT 2", "spark"); + EXPECT_NE(lhs, rhs); +} + +TEST(ViewRepresentationTest, Helpers) { + const ViewRepresentation sql = SqlRepr("SELECT 1", "spark"); + const auto* sql_repr = GetSql(sql); + ASSERT_NE(sql_repr, nullptr); + EXPECT_EQ(sql_repr->dialect, "spark"); + EXPECT_EQ(RepresentationType(sql), kSqlRepresentationType); + + const ViewRepresentation unknown = UnknownViewRepresentation{.type = "cypher"}; + EXPECT_EQ(GetSql(unknown), nullptr); + EXPECT_EQ(RepresentationType(unknown), "cypher"); +} + +} // namespace +} // namespace iceberg diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h index 477774a17..de78c273d 100644 --- a/src/iceberg/type_fwd.h +++ b/src/iceberg/type_fwd.h @@ -146,6 +146,13 @@ struct TableMetadata; class InheritableMetadata; class SnapshotSummaryBuilder; +/// \brief View metadata. +struct SqlViewRepresentation; +struct UnknownViewRepresentation; +struct ViewHistoryEntry; +struct ViewMetadata; +struct ViewVersion; + /// \brief Expression. class BoundPredicate; class BoundReference; diff --git a/src/iceberg/util/string_util.h b/src/iceberg/util/string_util.h index f1431e49d..09e7d9648 100644 --- a/src/iceberg/util/string_util.h +++ b/src/iceberg/util/string_util.h @@ -98,6 +98,24 @@ class ICEBERG_EXPORT StringUtils { return ToLower(str).starts_with(ToLower(prefix)); } + /// \brief Case-insensitive equality folding only ASCII letters (A-Z, a-z). + /// + /// Non-ASCII bytes compare unchanged, so folding never changes the length + /// of either operand. Use this for identifiers, such as SQL dialects, whose + /// comparison rules specify ASCII folding; use EqualsIgnoreCase for + /// Unicode-aware comparison. + static bool EqualsIgnoreCaseAscii(std::string_view a, std::string_view b) { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (ToLowerAscii(a[i]) != ToLowerAscii(b[i])) { + return false; + } + } + return true; + } + /// \brief Count the number of code points in a UTF-8 string. static size_t CodePointCount(std::string_view str) { size_t count = 0; diff --git a/src/iceberg/view_metadata.cc b/src/iceberg/view_metadata.cc new file mode 100644 index 000000000..ae757bc50 --- /dev/null +++ b/src/iceberg/view_metadata.cc @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/view_metadata.h" + +#include +#include + +#include "iceberg/schema.h" +#include "iceberg/util/macros.h" +#include "iceberg/util/string_util.h" + +namespace iceberg { + +Status ViewMetadata::Validate() const { + if (format_version != kSupportedViewFormatVersion) { + return Invalid("Unsupported view format version: {}", + static_cast(format_version)); + } + if (view_uuid.empty()) { + return Invalid("View UUID must not be empty"); + } + if (location.empty()) { + return Invalid("View location must not be empty"); + } + if (schemas.empty()) { + return Invalid("View metadata must list at least one schema"); + } + + std::unordered_set schema_ids; + for (const auto& schema : schemas) { + if (schema == nullptr) { + return Invalid("View schemas must not be null"); + } + if (!schema_ids.insert(schema->schema_id()).second) { + return Invalid("Duplicate view schema ID: {}", schema->schema_id()); + } + } + + if (versions.empty()) { + return Invalid("View metadata must list at least one version"); + } + std::unordered_set version_ids; + for (const auto& version : versions) { + if (version == nullptr) { + return Invalid("View versions must not be null"); + } + if (!version_ids.insert(version->version_id).second) { + return Invalid("Duplicate view version ID: {}", version->version_id); + } + } + if (!version_ids.contains(current_version_id)) { + return Invalid("Current version ID {} is not a listed view version", + current_version_id); + } + + for (const auto& version : versions) { + if (!schema_ids.contains(version->schema_id)) { + return Invalid("View version {} references unlisted schema ID {}", + version->version_id, version->schema_id); + } + if (version->default_namespace.levels.empty()) { + return Invalid("View version {} must have a non-empty default namespace", + version->version_id); + } + + std::vector dialects; + for (const auto& repr : version->representations) { + if (const auto* sql = GetSql(repr)) { + if (sql->sql.empty()) { + return Invalid("View version {} has an SQL representation without SQL", + version->version_id); + } + if (sql->dialect.empty()) { + return Invalid("View version {} has an SQL representation without a dialect", + version->version_id); + } + for (const auto& dialect : dialects) { + if (StringUtils::EqualsIgnoreCaseAscii(dialect, sql->dialect)) { + return Invalid( + "View version {} has multiple SQL representations for dialect '{}'", + version->version_id, sql->dialect); + } + } + dialects.push_back(sql->dialect); + } else { + const auto& unknown = std::get(repr); + if (unknown.type.empty()) { + return Invalid("View version {} has an unknown representation without a type", + version->version_id); + } + } + } + } + return {}; +} + +Result> ViewMetadata::CurrentVersion() const { + return VersionById(current_version_id); +} + +Result> ViewMetadata::VersionById(int32_t version_id) const { + auto iter = std::ranges::find_if(versions, [version_id](const auto& version) { + return version != nullptr && version->version_id == version_id; + }); + if (iter == versions.end()) { + return NotFound("View version with ID {} is not found", version_id); + } + return *iter; +} + +Result> ViewMetadata::CurrentSchema() const { + ICEBERG_ASSIGN_OR_RAISE(auto version, CurrentVersion()); + return SchemaById(version->schema_id); +} + +Result> ViewMetadata::SchemaById(int32_t schema_id) const { + auto iter = std::ranges::find_if(schemas, [schema_id](const auto& schema) { + return schema != nullptr && schema->schema_id() == schema_id; + }); + if (iter == schemas.end()) { + return NotFound("View schema with ID {} is not found", schema_id); + } + return *iter; +} + +bool ViewMetadata::Equals(const ViewMetadata& other) const { + auto shared_ptr_equals = [](const auto& lhs, const auto& rhs) { + if (lhs == nullptr || rhs == nullptr) { + return lhs == rhs; + } + return *lhs == *rhs; + }; + return format_version == other.format_version && view_uuid == other.view_uuid && + location == other.location && schemas.size() == other.schemas.size() && + std::equal(schemas.begin(), schemas.end(), other.schemas.begin(), + shared_ptr_equals) && + current_version_id == other.current_version_id && + versions.size() == other.versions.size() && + std::equal(versions.begin(), versions.end(), other.versions.begin(), + shared_ptr_equals) && + version_log == other.version_log && properties == other.properties; +} + +} // namespace iceberg diff --git a/src/iceberg/view_metadata.h b/src/iceberg/view_metadata.h new file mode 100644 index 000000000..988157ed0 --- /dev/null +++ b/src/iceberg/view_metadata.h @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +/// \file iceberg/view_metadata.h +/// View metadata for Iceberg views. + +#include +#include +#include +#include +#include + +#include "iceberg/iceberg_export.h" +#include "iceberg/result.h" +#include "iceberg/type_fwd.h" +#include "iceberg/util/timepoint.h" +#include "iceberg/view_version.h" + +namespace iceberg { + +/// \brief Represents a view history entry, which records a change to the +/// current version of a view. +struct ICEBERG_EXPORT ViewHistoryEntry { + /// The timestamp in milliseconds of the change to the current version. + TimePointMs timestamp_ms; + /// ID of the view version that was made current. + int32_t version_id; + + /// \brief Compare two view history entries for equality. + friend bool operator==(const ViewHistoryEntry& lhs, const ViewHistoryEntry& rhs) { + return lhs.timestamp_ms == rhs.timestamp_ms && lhs.version_id == rhs.version_id; + } +}; + +/// \brief Represents the metadata for an Iceberg view. +/// +/// View metadata follows view format version 1. Table-format schema +/// restrictions are deliberately not applied to the view's schemas: view +/// schemas are projections of the referenced tables' columns and may use +/// types or nullability that table data would reject. +struct ICEBERG_EXPORT ViewMetadata { + static constexpr int8_t kDefaultViewFormatVersion = 1; + static constexpr int8_t kSupportedViewFormatVersion = 1; + + /// An integer version number for the view format; must be 1. + int8_t format_version; + /// A UUID that identifies the view, generated when the view is created. + std::string view_uuid; + /// The view's base location; used to create metadata file locations. + std::string location; + /// A list of known schemas. + std::vector> schemas; + /// ID of the current version of the view. + int32_t current_version_id; + /// A list of known versions of the view. + std::vector> versions; + /// A list of timestamp and version ID pairs that encodes changes to the + /// current version. + std::vector version_log; + /// A string to string map of view properties. + std::unordered_map properties; + + /// \brief Validate view format v1 invariants. + /// + /// Checks referential integrity, required fields, and per-dialect + /// uniqueness of SQL representations, without applying table-format schema + /// restrictions. Never modifies the metadata. + /// + /// \return Status indicating success, or ErrorKind::kInvalid with the first + /// violated invariant. + Status Validate() const; + + /// \brief Get the current view version. + /// \return the current version or ErrorKind::kNotFound if it is not listed. + /// \note The returned version is guaranteed to be not null. + Result> CurrentVersion() const; + + /// \brief Get a view version by ID. + /// \param version_id ID of the view version to look up + /// \return the version or ErrorKind::kNotFound if it is not listed. + /// \note The returned version is guaranteed to be not null. + Result> VersionById(int32_t version_id) const; + + /// \brief Get the schema of the current view version. + /// \return the current schema or ErrorKind::kNotFound if the current + /// version or its schema is not listed. + /// \note The returned schema is guaranteed to be not null. + Result> CurrentSchema() const; + + /// \brief Get a view schema by ID. + /// \param schema_id ID of the schema to look up + /// \return the schema or ErrorKind::kNotFound if it is not listed. + /// \note The returned schema is guaranteed to be not null. + Result> SchemaById(int32_t schema_id) const; + + /// \brief Compare two view metadata for equality. + friend bool operator==(const ViewMetadata& lhs, const ViewMetadata& rhs) { + return lhs.Equals(rhs); + } + + private: + /// \brief Compare two view metadata for equality. + bool Equals(const ViewMetadata& other) const; +}; + +} // namespace iceberg diff --git a/src/iceberg/view_version.cc b/src/iceberg/view_version.cc new file mode 100644 index 000000000..691b304ee --- /dev/null +++ b/src/iceberg/view_version.cc @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/view_version.h" + +namespace iceberg { + +const SqlViewRepresentation* GetSql(const ViewRepresentation& repr) { + return std::get_if(&repr); +} + +std::string_view RepresentationType(const ViewRepresentation& repr) { + if (const auto* sql = GetSql(repr)) { + return kSqlRepresentationType; + } + return std::get(repr).type; +} + +bool ViewVersion::Equals(const ViewVersion& other) const { + return version_id == other.version_id && schema_id == other.schema_id && + timestamp_ms == other.timestamp_ms && summary == other.summary && + representations == other.representations && + default_catalog == other.default_catalog && + default_namespace == other.default_namespace; +} + +} // namespace iceberg diff --git a/src/iceberg/view_version.h b/src/iceberg/view_version.h new file mode 100644 index 000000000..3626c817d --- /dev/null +++ b/src/iceberg/view_version.h @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +/// \file iceberg/view_version.h +/// A version of an Iceberg view. + +#include +#include +#include +#include +#include +#include +#include + +#include "iceberg/iceberg_export.h" +#include "iceberg/table_identifier.h" +#include "iceberg/util/timepoint.h" + +namespace iceberg { + +/// \brief The SQL representation of a view version's definition. +/// +/// Each SQL representation pairs a query with its engine dialect, such as +/// "spark". Dialect strings are preserved verbatim and compared with ASCII +/// case folding. +struct ICEBERG_EXPORT SqlViewRepresentation { + /// The SQL SELECT statement that defines the view. + std::string sql; + /// The dialect of the SELECT statement, such as "spark" or "trino". + std::string dialect; + + /// \brief Compare two SQL representations for equality. + friend bool operator==(const SqlViewRepresentation& lhs, + const SqlViewRepresentation& rhs) = default; +}; + +/// \brief An opaque representation type this library does not interpret. +/// +/// Unknown representations are preserved so that metadata round trips and +/// property edits do not lose them. Definition changes that involve them are +/// rejected instead of being partially rewritten. +struct ICEBERG_EXPORT UnknownViewRepresentation { + /// The representation type identifier from the metadata, such as "cypher". + std::string type; + + /// \brief Compare two unknown representations for equality. + friend bool operator==(const UnknownViewRepresentation& lhs, + const UnknownViewRepresentation& rhs) = default; +}; + +/// \brief A representation of a view version's definition: SQL or opaque. +using ViewRepresentation = std::variant; + +/// \brief The type identifier of the SQL representation. +inline constexpr std::string_view kSqlRepresentationType = "sql"; + +/// \brief Get the SQL representation. +/// +/// \return the SQL representation, or nullptr when the representation is not +/// a SQL representation. The pointer remains valid until the representation +/// is modified or destroyed. +ICEBERG_EXPORT const SqlViewRepresentation* GetSql(const ViewRepresentation& repr); + +/// \brief Get the representation's type identifier. +/// +/// Returns kSqlRepresentationType for SQL representations and the opaque +/// type identifier otherwise. +ICEBERG_EXPORT std::string_view RepresentationType(const ViewRepresentation& repr); + +/// \brief A view version, the state of a view's definition at a point in time. +/// +/// View versions are immutable: a definition change creates a new version. +/// A version can carry more than one representation, but at most one SQL +/// representation per dialect. +struct ICEBERG_EXPORT ViewVersion { + /// ID of the view version. + int32_t version_id; + /// ID of the schema for this view version. + int32_t schema_id; + /// Timestamp in milliseconds from the unix epoch when the version was created. + TimePointMs timestamp_ms; + /// Summary metadata about the version, such as "engine-name" and + /// "engine-version". + std::unordered_map summary; + /// A list of representations for the view definition. + std::vector representations; + /// Catalog name to use when a reference in the SELECT does not contain a + /// catalog. When unset, the catalog in which the view is stored is used. + std::optional default_catalog; + /// Namespace to use when a reference in the SELECT is a single identifier. + Namespace default_namespace; + + /// \brief Compare two view versions for equality. + friend bool operator==(const ViewVersion& lhs, const ViewVersion& rhs) { + return lhs.Equals(rhs); + } + + private: + /// \brief Compare two view versions for equality. + bool Equals(const ViewVersion& other) const; +}; + +} // namespace iceberg