From 0f16a38b159a8cffa2336ec2513160e49429059c Mon Sep 17 00:00:00 2001 From: Angela-CMU Date: Wed, 31 Jan 2024 10:18:14 -0500 Subject: [PATCH 1/6] Remove multi-versioning in design --- doc/design_doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/design_doc.md b/doc/design_doc.md index 7ecb141..3399da3 100644 --- a/doc/design_doc.md +++ b/doc/design_doc.md @@ -11,7 +11,7 @@ The goal of this project is to design and implement a **Catalog Service** for an We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. ![system architecture](./assets/system-architecture.png) ### Data Model -We adhere to the Iceberg data model, arranging tables based on namespaces, with each table uniquely identified by its name. Our goal is to enable multi-versioning, facilitating point-in-time queries and allowing for queries at a specific historical version of the table. +We adhere to the Iceberg data model, arranging tables based on namespaces, with each table uniquely identified by its name. For every table in the catalog, there is an associated metadata file. This file contains a collection of manifests, each of which references the table's information at different points in time. The manifest file is an in-memory, non-persistent component that gets recreated based on on-disk files during service restarts. (If it is not frequently updated, we could dump it to disk every time we update it) From e27668690abb89f28c9113d1846869bd4345fec3 Mon Sep 17 00:00:00 2001 From: Yen-Ju Wu Date: Sun, 25 Feb 2024 16:41:46 -0500 Subject: [PATCH 2/6] update design_doc.md --- doc/assets/system-architecture.png | Bin 265036 -> 391404 bytes doc/design_doc.md | 19 ++++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/doc/assets/system-architecture.png b/doc/assets/system-architecture.png index d35109e8c4a62f16cb3d2d32fc25598e22ac5b5b..11edc28662d1f333d6a0b7b845f390a8656c8ece 100644 GIT binary patch literal 391404 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

C{w&(y(0-y1Q9r3P8u>ZSBN4*<; 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 CC!-Yr diff --git a/doc/design_doc.md b/doc/design_doc.md index 3399da3..e70921c 100644 --- a/doc/design_doc.md +++ b/doc/design_doc.md @@ -8,22 +8,20 @@ ### Goal The goal of this project is to design and implement a **Catalog Service** for an OLAP database system. The Catalog aims for managing metadata and providing a centralized repository for storing information about the structure and organization of data within the OLAP database. This project aims to produce a functional catalog that adheres to [the Iceberg catalog specification](https://iceberg.apache.org/spec/) exposed through [REST API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). ## Architectural Design -We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. +We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. We will use pickleDB as the key-value store to store (namespace, tables) and (table_name, metadata) as two (key, value) pairs as local db files. [TODO: server part] ![system architecture](./assets/system-architecture.png) ### Data Model We adhere to the Iceberg data model, arranging tables based on namespaces, with each table uniquely identified by its name. - -For every table in the catalog, there is an associated metadata file. This file contains a collection of manifests, each of which references the table's information at different points in time. The manifest file is an in-memory, non-persistent component that gets recreated based on on-disk files during service restarts. (If it is not frequently updated, we could dump it to disk every time we update it) - -To enhance startup and recovery times, we periodically save the in-memory index to disk. This ensures a quicker restoration process by utilizing the dumped index data. -![Catalog Data Model](./assets/iceberg-metadata.png) +For every namespace in the database, there are associated list of tables. +For every table in the catalog, there are associated metadata, including statistics, version, table-uuid, location, last-column-id, schema, and partition-spec. +[TODO: data model (struct of metadata)] ### Use Cases #### Namespace -create/update/delete namespace. +create/delete/rename namespace #### Table -create/update/delete table -#### Query Table’s Metadata +create/delete/rename table +#### Query Table’s Metadata (including statistics, version, table-uuid, location, last-column-id, schema, and partition-spec) get metadeta by {namespace}/{table} ## Design Rationale @@ -32,11 +30,10 @@ get metadeta by {namespace}/{table} * Data durability mechanisms will be implemented to prevent data loss during restarts. * Performance: * Optimization on data retrieval and storage strategies to minimize latency in metadata access. - * Efficient indexing mechanisms, such as Bloom filters, enhance query performance. - * Partitioning strategies facilitate data pruning and improve query execution performance. * Engineering Complexity / Maintainability: * Centralized metadata management achieved by separating data and metadata, reducing complexity and facilitating consistent metadata handling. * Code modularity and clear interfaces facilitate easier updates and improvements. + * We adopt the existing kvstore (pickleDB) and server (Rocket) to mitigate the engineering complexity. * Testing: * Comprehensive testing plans cover correctness through unit tests and performance through long-running regression tests. Unit tests focus on individual components of the catalog service, while regression tests evaluate system-wide performance and stability. * Other Implementations: From 349be48f8efd13bea237f4cdf14ab965f5eb4bf8 Mon Sep 17 00:00:00 2001 From: Angela-CMU Date: Mon, 26 Feb 2024 13:17:03 -0500 Subject: [PATCH 3/6] add pickle and rocket link --- doc/design_doc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/design_doc.md b/doc/design_doc.md index e70921c..6f22df0 100644 --- a/doc/design_doc.md +++ b/doc/design_doc.md @@ -8,7 +8,7 @@ ### Goal The goal of this project is to design and implement a **Catalog Service** for an OLAP database system. The Catalog aims for managing metadata and providing a centralized repository for storing information about the structure and organization of data within the OLAP database. This project aims to produce a functional catalog that adheres to [the Iceberg catalog specification](https://iceberg.apache.org/spec/) exposed through [REST API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). ## Architectural Design -We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. We will use pickleDB as the key-value store to store (namespace, tables) and (table_name, metadata) as two (key, value) pairs as local db files. [TODO: server part] +We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. We will use [pickleDB](https://docs.rs/pickledb/latest/pickledb/) as the key-value store to store (namespace, tables) and (table_name, metadata) as two (key, value) pairs as local db files. [TODO: server part] ![system architecture](./assets/system-architecture.png) ### Data Model We adhere to the Iceberg data model, arranging tables based on namespaces, with each table uniquely identified by its name. @@ -33,7 +33,7 @@ get metadeta by {namespace}/{table} * Engineering Complexity / Maintainability: * Centralized metadata management achieved by separating data and metadata, reducing complexity and facilitating consistent metadata handling. * Code modularity and clear interfaces facilitate easier updates and improvements. - * We adopt the existing kvstore (pickleDB) and server (Rocket) to mitigate the engineering complexity. + * We adopt the existing kvstore ([pickleDB](https://docs.rs/pickledb/latest/pickledb/)) and server ([Rocket](https://github.com/rwf2/Rocket)) to mitigate the engineering complexity. * Testing: * Comprehensive testing plans cover correctness through unit tests and performance through long-running regression tests. Unit tests focus on individual components of the catalog service, while regression tests evaluate system-wide performance and stability. * Other Implementations: From 5eeeea3990310d0240c112aa679e8da2e83d7653 Mon Sep 17 00:00:00 2001 From: zhouzilong <529620861@qq.com> Date: Tue, 27 Feb 2024 08:18:52 -0500 Subject: [PATCH 4/6] update architectural design & data model --- doc/design_doc.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/design_doc.md b/doc/design_doc.md index 6f22df0..407b7a0 100644 --- a/doc/design_doc.md +++ b/doc/design_doc.md @@ -8,13 +8,14 @@ ### Goal The goal of this project is to design and implement a **Catalog Service** for an OLAP database system. The Catalog aims for managing metadata and providing a centralized repository for storing information about the structure and organization of data within the OLAP database. This project aims to produce a functional catalog that adheres to [the Iceberg catalog specification](https://iceberg.apache.org/spec/) exposed through [REST API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). ## Architectural Design -We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. We will use [pickleDB](https://docs.rs/pickledb/latest/pickledb/) as the key-value store to store (namespace, tables) and (table_name, metadata) as two (key, value) pairs as local db files. [TODO: server part] +We follow the logic model described below. The input of our service comes from execution engine and I/O service. And we will provide metadata to planner and scheduler. We will use [pickleDB](https://docs.rs/pickledb/latest/pickledb/) as the key-value store to store (namespace, tables) and (table_name, metadata) as two (key, value) pairs as local db files. +We will use [Rocket](https://rocket.rs) as the web framework handling incoming API traffic. ![system architecture](./assets/system-architecture.png) ### Data Model We adhere to the Iceberg data model, arranging tables based on namespaces, with each table uniquely identified by its name. For every namespace in the database, there are associated list of tables. For every table in the catalog, there are associated metadata, including statistics, version, table-uuid, location, last-column-id, schema, and partition-spec. -[TODO: data model (struct of metadata)] +The parameters for request and response can be referenced from [REST API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). We directly import Iceberg-Rust as a starting point. ### Use Cases #### Namespace From 4fd1c280239916add23ce69e385fef2ffad1c401 Mon Sep 17 00:00:00 2001 From: Zilong Zhou <60960532+zhouzilong2020@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:04:03 -0500 Subject: [PATCH 5/6] init server framework (#11) * import iceberg * server struct * rename crates to libs * init db --- .gitignore | 4 +- Cargo.toml | 27 +- Makefile | 54 + libs/iceberg/Cargo.toml | 67 + libs/iceberg/DEPENDENCIES.rust.tsv | 276 ++ libs/iceberg/src/avro/mod.rs | 21 + libs/iceberg/src/avro/schema.rs | 955 +++++++ libs/iceberg/src/catalog/mod.rs | 1068 ++++++++ libs/iceberg/src/error.rs | 429 ++++ libs/iceberg/src/expr/mod.rs | 42 + libs/iceberg/src/expr/predicate.rs | 93 + libs/iceberg/src/expr/term.rs | 37 + libs/iceberg/src/io.rs | 508 ++++ libs/iceberg/src/lib.rs | 55 + libs/iceberg/src/scan.rs | 448 ++++ libs/iceberg/src/spec/datatypes.rs | 1056 ++++++++ libs/iceberg/src/spec/manifest.rs | 2007 +++++++++++++++ libs/iceberg/src/spec/manifest_list.rs | 1468 +++++++++++ libs/iceberg/src/spec/mod.rs | 40 + libs/iceberg/src/spec/partition.rs | 492 ++++ libs/iceberg/src/spec/schema.rs | 1289 ++++++++++ libs/iceberg/src/spec/snapshot.rs | 404 +++ libs/iceberg/src/spec/sort.rs | 480 ++++ libs/iceberg/src/spec/table_metadata.rs | 1572 ++++++++++++ libs/iceberg/src/spec/transform.rs | 861 +++++++ libs/iceberg/src/spec/values.rs | 2237 +++++++++++++++++ libs/iceberg/src/table.rs | 65 + libs/iceberg/src/transaction.rs | 370 +++ libs/iceberg/src/transform/bucket.rs | 245 ++ libs/iceberg/src/transform/identity.rs | 31 + libs/iceberg/src/transform/mod.rs | 55 + libs/iceberg/src/transform/temporal.rs | 412 +++ libs/iceberg/src/transform/truncate.rs | 218 ++ libs/iceberg/src/transform/void.rs | 30 + libs/iceberg/src/writer/file_writer/mod.rs | 39 + libs/iceberg/src/writer/mod.rs | 35 + .../testdata/avro_schema_manifest_entry.json | 286 +++ .../avro_schema_manifest_file_v1.json | 139 + .../avro_schema_manifest_file_v2.json | 141 ++ .../testdata/example_table_metadata_v2.json | 61 + .../TableMetadataUnsupportedVersion.json | 36 + .../table_metadata/TableMetadataV1Valid.json | 42 + .../TableMetadataV2CurrentSchemaNotFound.json | 88 + ...TableMetadataV2MissingLastPartitionId.json | 73 + .../TableMetadataV2MissingPartitionSpecs.json | 67 + .../TableMetadataV2MissingSchemas.json | 71 + .../TableMetadataV2MissingSortOrder.json | 54 + .../table_metadata/TableMetadataV2Valid.json | 122 + .../TableMetadataV2ValidMinimal.json | 71 + rustfmt.toml | 4 + scripts/parse_dependencies.py | 42 + src/db/mod.rs | 5 + src/main.rs | 31 +- src/server/mod.rs | 1 + src/server/routes/config.rs | 4 + src/server/routes/metric.rs | 5 + src/server/routes/mod.rs | 5 + src/server/routes/namespace.rs | 35 + src/server/routes/table.rs | 47 + 59 files changed, 18916 insertions(+), 4 deletions(-) create mode 100644 Makefile create mode 100644 libs/iceberg/Cargo.toml create mode 100644 libs/iceberg/DEPENDENCIES.rust.tsv create mode 100644 libs/iceberg/src/avro/mod.rs create mode 100644 libs/iceberg/src/avro/schema.rs create mode 100644 libs/iceberg/src/catalog/mod.rs create mode 100644 libs/iceberg/src/error.rs create mode 100644 libs/iceberg/src/expr/mod.rs create mode 100644 libs/iceberg/src/expr/predicate.rs create mode 100644 libs/iceberg/src/expr/term.rs create mode 100644 libs/iceberg/src/io.rs create mode 100644 libs/iceberg/src/lib.rs create mode 100644 libs/iceberg/src/scan.rs create mode 100644 libs/iceberg/src/spec/datatypes.rs create mode 100644 libs/iceberg/src/spec/manifest.rs create mode 100644 libs/iceberg/src/spec/manifest_list.rs create mode 100644 libs/iceberg/src/spec/mod.rs create mode 100644 libs/iceberg/src/spec/partition.rs create mode 100644 libs/iceberg/src/spec/schema.rs create mode 100644 libs/iceberg/src/spec/snapshot.rs create mode 100644 libs/iceberg/src/spec/sort.rs create mode 100644 libs/iceberg/src/spec/table_metadata.rs create mode 100644 libs/iceberg/src/spec/transform.rs create mode 100644 libs/iceberg/src/spec/values.rs create mode 100644 libs/iceberg/src/table.rs create mode 100644 libs/iceberg/src/transaction.rs create mode 100644 libs/iceberg/src/transform/bucket.rs create mode 100644 libs/iceberg/src/transform/identity.rs create mode 100644 libs/iceberg/src/transform/mod.rs create mode 100644 libs/iceberg/src/transform/temporal.rs create mode 100644 libs/iceberg/src/transform/truncate.rs create mode 100644 libs/iceberg/src/transform/void.rs create mode 100644 libs/iceberg/src/writer/file_writer/mod.rs create mode 100644 libs/iceberg/src/writer/mod.rs create mode 100644 libs/iceberg/testdata/avro_schema_manifest_entry.json create mode 100644 libs/iceberg/testdata/avro_schema_manifest_file_v1.json create mode 100644 libs/iceberg/testdata/avro_schema_manifest_file_v2.json create mode 100644 libs/iceberg/testdata/example_table_metadata_v2.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataUnsupportedVersion.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV1Valid.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSchemas.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSortOrder.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2Valid.json create mode 100644 libs/iceberg/testdata/table_metadata/TableMetadataV2ValidMinimal.json create mode 100644 rustfmt.toml create mode 100644 scripts/parse_dependencies.py create mode 100644 src/db/mod.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/routes/config.rs create mode 100644 src/server/routes/metric.rs create mode 100644 src/server/routes/mod.rs create mode 100644 src/server/routes/namespace.rs create mode 100644 src/server/routes/table.rs diff --git a/.gitignore b/.gitignore index 0000196..6d586ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ Cargo.lock *.pdb # macOS resource forks and .DS_Store files -.DS_Store \ No newline at end of file +.DS_Store + +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 380993f..6c16e15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,33 @@ +# 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. + [package] name = "catalog2" version = "0.1.0" edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/cmu-db/15721-s24-catalog2" +rust-version = "1.75.0" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +rocket = { version = "0.5.0", features = ["json", "http2"] } +iceberg = { src = "./libs/iceberg" } +dotenv = "0.15.0" +pickledb = "^0.5.0" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..efc7fd7 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# 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. + +.EXPORT_ALL_VARIABLES: + +RUST_LOG = debug + +build: + cargo build + +run: + cargo build + target/debug/catalog2 + +check-fmt: + cargo fmt --all -- --check + +check-clippy: + cargo clippy --all-targets --all-features --workspace -- -D warnings + +cargo-sort: + cargo install cargo-sort + cargo sort -c -w + +fix-toml: + cargo install taplo-cli --locked + taplo fmt + +check-toml: + cargo install taplo-cli --locked + taplo check + +check: check-fmt check-clippy cargo-sort check-toml + +unit-test: + cargo test --no-fail-fast --lib --all-features --workspace + +test: + cargo test --no-fail-fast --all-targets --all-features --workspace + cargo test --no-fail-fast --doc --all-features --workspace \ No newline at end of file diff --git a/libs/iceberg/Cargo.toml b/libs/iceberg/Cargo.toml new file mode 100644 index 0000000..694e12e --- /dev/null +++ b/libs/iceberg/Cargo.toml @@ -0,0 +1,67 @@ +# 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. + +[package] +name = "iceberg" +version = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } + +categories = ["database"] +description = "Apache Iceberg Rust implementation" +repository = { workspace = true } +license = { workspace = true } +keywords = ["iceberg"] + +[dependencies] +anyhow = { workspace = true } +apache-avro = { workspace = true } +arrow-arith = { workspace = true } +arrow-array = { workspace = true } +arrow-schema = { workspace = true } +async-trait = { workspace = true } +bimap = { workspace = true } +bitvec = { workspace = true } +chrono = { workspace = true } +derive_builder = { workspace = true } +either = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +murmur3 = { workspace = true } +once_cell = { workspace = true } +opendal = { workspace = true } +ordered-float = { workspace = true } +reqwest = { workspace = true } +rust_decimal = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +serde_repr = { workspace = true } +serde_with = { workspace = true } +typed-builder = { workspace = true } +url = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +tempfile = { workspace = true } +tera = { workspace = true } +tokio = { workspace = true } diff --git a/libs/iceberg/DEPENDENCIES.rust.tsv b/libs/iceberg/DEPENDENCIES.rust.tsv new file mode 100644 index 0000000..f47f941 --- /dev/null +++ b/libs/iceberg/DEPENDENCIES.rust.tsv @@ -0,0 +1,276 @@ +crate 0BSD Apache-2.0 Apache-2.0 WITH LLVM-exception BSD-2-Clause BSD-3-Clause BSL-1.0 CC0-1.0 ISC MIT OpenSSL Unicode-DFS-2016 Unlicense Zlib +addr2line@0.21.0 X X +adler@1.0.2 X X X +adler32@1.2.0 X +ahash@0.8.6 X X +android-tzdata@0.1.1 X X +android_system_properties@0.1.5 X X +anyhow@1.0.77 X X +apache-avro@0.16.0 X +arrayvec@0.7.4 X X +arrow-arith@49.0.0 X +arrow-array@49.0.0 X +arrow-buffer@49.0.0 X +arrow-data@49.0.0 X +arrow-schema@49.0.0 X +async-compat@0.2.3 X X +async-trait@0.1.75 X X +autocfg@1.1.0 X X +backon@0.4.1 X +backtrace@0.3.69 X X +base64@0.21.5 X X +base64ct@1.6.0 X X +bimap@0.6.3 X X +bitflags@1.3.2 X X +bitflags@2.4.1 X X +bitvec@1.0.1 X +block-buffer@0.10.4 X X +bumpalo@3.14.0 X X +byteorder@1.5.0 X X +bytes@1.5.0 X +cc@1.0.83 X X +cfg-if@1.0.0 X X +chrono@0.4.31 X X +const-oid@0.9.6 X X +const-random@0.1.17 X X +const-random-macro@0.1.16 X X +core-foundation@0.9.4 X X +core-foundation-sys@0.8.6 X X +core2@0.4.0 X X +cpufeatures@0.2.11 X X +crc32fast@1.3.2 X X +crunchy@0.2.2 X +crypto-common@0.1.6 X X +darling@0.14.4 X +darling@0.20.3 X +darling_core@0.14.4 X +darling_core@0.20.3 X +darling_macro@0.14.4 X +darling_macro@0.20.3 X +dary_heap@0.3.6 X X +der@0.7.8 X X +deranged@0.3.10 X X +derive_builder@0.13.0 X X +derive_builder_core@0.13.0 X X +derive_builder_macro@0.13.0 X X +digest@0.10.7 X X +dlv-list@0.5.2 X X +either@1.9.0 X X +encoding_rs@0.8.33 X X X +equivalent@1.0.1 X X +fastrand@1.9.0 X X +fastrand@2.0.1 X X +flagset@0.4.4 X +fnv@1.0.7 X X +foreign-types@0.3.2 X X +foreign-types-shared@0.1.1 X X +form_urlencoded@1.2.1 X X +funty@2.0.0 X +futures@0.3.30 X X +futures-channel@0.3.30 X X +futures-core@0.3.30 X X +futures-executor@0.3.30 X X +futures-io@0.3.30 X X +futures-macro@0.3.30 X X +futures-sink@0.3.30 X X +futures-task@0.3.30 X X +futures-util@0.3.30 X X +generic-array@0.14.7 X +getrandom@0.2.11 X X +gimli@0.28.1 X X +h2@0.3.22 X +half@2.3.1 X X +hashbrown@0.13.2 X X +hashbrown@0.14.3 X X +heck@0.4.1 X X +hex@0.4.3 X X +hmac@0.12.1 X X +home@0.5.9 X X +http@0.2.11 X X +http-body@0.4.6 X +httparse@1.8.0 X X +httpdate@1.0.3 X X +hyper@0.14.28 X +hyper-rustls@0.24.2 X X X +hyper-tls@0.5.0 X X +iana-time-zone@0.1.58 X X +iana-time-zone-haiku@0.1.2 X X +iceberg@0.2.0 X +ident_case@1.0.1 X X +idna@0.5.0 X X +indexmap@2.1.0 X X +instant@0.1.12 X +ipnet@2.9.0 X X +itertools@0.12.0 X X +itoa@1.0.10 X X +js-sys@0.3.66 X X +jsonwebtoken@9.2.0 X +lazy_static@1.4.0 X X +libc@0.2.151 X X +libflate@2.0.0 X +libflate_lz77@2.0.0 X +libm@0.2.8 X X +linux-raw-sys@0.4.12 X X X +lock_api@0.4.11 X X +log@0.4.20 X X +md-5@0.10.6 X X +memchr@2.6.4 X X +mime@0.3.17 X X +miniz_oxide@0.7.1 X X X +mio@0.8.10 X +murmur3@0.5.2 X X +native-tls@0.2.11 X X +num@0.4.1 X X +num-bigint@0.4.4 X X +num-bigint-dig@0.8.4 X X +num-complex@0.4.4 X X +num-integer@0.1.45 X X +num-iter@0.1.43 X X +num-rational@0.4.1 X X +num-traits@0.2.17 X X +object@0.32.2 X X +once_cell@1.19.0 X X +opendal@0.44.0 X +openssl@0.10.62 X +openssl-macros@0.1.1 X X +openssl-probe@0.1.5 X X +openssl-sys@0.9.98 X +ordered-float@4.2.0 X +ordered-multimap@0.7.1 X +parking_lot@0.12.1 X X +parking_lot_core@0.9.9 X X +pem@3.0.3 X +pem-rfc7468@0.7.0 X X +percent-encoding@2.3.1 X X +pin-project@1.1.3 X X +pin-project-internal@1.1.3 X X +pin-project-lite@0.2.13 X X +pin-utils@0.1.0 X X +pkcs1@0.7.5 X X +pkcs8@0.10.2 X X +pkg-config@0.3.28 X X +powerfmt@0.2.0 X X +ppv-lite86@0.2.17 X X +proc-macro2@1.0.71 X X +quad-rand@0.2.1 X +quick-xml@0.30.0 X +quick-xml@0.31.0 X +quote@1.0.33 X X +radium@0.7.0 X +rand@0.8.5 X X +rand_chacha@0.3.1 X X +rand_core@0.6.4 X X +redox_syscall@0.4.1 X +regex-lite@0.1.5 X X +reqsign@0.14.6 X +reqwest@0.11.23 X X +ring@0.17.7 X +rle-decode-fast@1.0.3 X X +rsa@0.9.6 X X +rust-ini@0.20.0 X +rust_decimal@1.33.1 X +rustc-demangle@0.1.23 X X +rustix@0.38.28 X X X +rustls@0.21.10 X X X +rustls-native-certs@0.6.3 X X X +rustls-pemfile@1.0.4 X X X +rustls-webpki@0.101.7 X +rustversion@1.0.14 X X +ryu@1.0.16 X X +schannel@0.1.22 X +scopeguard@1.2.0 X X +sct@0.7.1 X X X +security-framework@2.9.2 X X +security-framework-sys@2.9.1 X X +serde@1.0.193 X X +serde_bytes@0.11.13 X X +serde_derive@1.0.193 X X +serde_json@1.0.108 X X +serde_repr@0.1.17 X X +serde_urlencoded@0.7.1 X X +serde_with@3.4.0 X X +serde_with_macros@3.4.0 X X +sha1@0.10.6 X X +sha2@0.10.8 X X +signature@2.2.0 X X +simple_asn1@0.6.2 X +slab@0.4.9 X +smallvec@1.11.2 X X +socket2@0.5.5 X X +spin@0.5.2 X +spin@0.9.8 X +spki@0.7.3 X X +strsim@0.10.0 X +strum@0.25.0 X +strum_macros@0.25.3 X +subtle@2.5.0 X +syn@1.0.109 X X +syn@2.0.43 X X +system-configuration@0.5.1 X X +system-configuration-sys@0.5.0 X X +tap@1.0.1 X +tempfile@3.8.1 X X +thiserror@1.0.52 X X +thiserror-impl@1.0.52 X X +time@0.3.31 X X +time-core@0.1.2 X X +time-macros@0.2.16 X X +tiny-keccak@2.0.2 X +tinyvec@1.6.0 X X X +tinyvec_macros@0.1.1 X X X +tokio@1.35.1 X +tokio-macros@2.2.0 X +tokio-native-tls@0.3.1 X +tokio-rustls@0.24.1 X X +tokio-util@0.7.10 X +tower-service@0.3.2 X +tracing@0.1.40 X +tracing-core@0.1.32 X +try-lock@0.2.5 X +typed-builder@0.16.2 X X +typed-builder@0.18.0 X X +typed-builder-macro@0.16.2 X X +typed-builder-macro@0.18.0 X X +typenum@1.17.0 X X +unicode-bidi@0.3.14 X X +unicode-ident@1.0.12 X X X +unicode-normalization@0.1.22 X X +untrusted@0.9.0 X +url@2.5.0 X X +urlencoding@2.1.3 X +uuid@1.6.1 X X +vcpkg@0.2.15 X X +version_check@0.9.4 X X +want@0.3.1 X +wasi@0.11.0+wasi-snapshot-preview1 X X X +wasm-bindgen@0.2.89 X X +wasm-bindgen-backend@0.2.89 X X +wasm-bindgen-futures@0.4.39 X X +wasm-bindgen-macro@0.2.89 X X +wasm-bindgen-macro-support@0.2.89 X X +wasm-bindgen-shared@0.2.89 X X +wasm-streams@0.3.0 X X +web-sys@0.3.66 X X +windows-core@0.51.1 X X +windows-sys@0.48.0 X X +windows-sys@0.52.0 X X +windows-targets@0.48.5 X X +windows-targets@0.52.0 X X +windows_aarch64_gnullvm@0.48.5 X X +windows_aarch64_gnullvm@0.52.0 X X +windows_aarch64_msvc@0.48.5 X X +windows_aarch64_msvc@0.52.0 X X +windows_i686_gnu@0.48.5 X X +windows_i686_gnu@0.52.0 X X +windows_i686_msvc@0.48.5 X X +windows_i686_msvc@0.52.0 X X +windows_x86_64_gnu@0.48.5 X X +windows_x86_64_gnu@0.52.0 X X +windows_x86_64_gnullvm@0.48.5 X X +windows_x86_64_gnullvm@0.52.0 X X +windows_x86_64_msvc@0.48.5 X X +windows_x86_64_msvc@0.52.0 X X +winreg@0.50.0 X +wyz@0.5.1 X +zerocopy@0.7.32 X X X +zeroize@1.7.0 X X diff --git a/libs/iceberg/src/avro/mod.rs b/libs/iceberg/src/avro/mod.rs new file mode 100644 index 0000000..bdccb2f --- /dev/null +++ b/libs/iceberg/src/avro/mod.rs @@ -0,0 +1,21 @@ +// 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. + +//! Avro related codes. +#[allow(dead_code)] +mod schema; +pub(crate) use schema::*; diff --git a/libs/iceberg/src/avro/schema.rs b/libs/iceberg/src/avro/schema.rs new file mode 100644 index 0000000..636f128 --- /dev/null +++ b/libs/iceberg/src/avro/schema.rs @@ -0,0 +1,955 @@ +// 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. + +//! Conversion between iceberg and avro schema. +use std::collections::BTreeMap; + +use crate::spec::{ + visit_schema, ListType, MapType, NestedField, NestedFieldRef, PrimitiveType, Schema, + SchemaVisitor, StructType, Type, +}; +use crate::{ensure_data_valid, Error, ErrorKind, Result}; +use apache_avro::schema::{ + DecimalSchema, FixedSchema, Name, RecordField as AvroRecordField, RecordFieldOrder, + RecordSchema, UnionSchema, +}; +use apache_avro::Schema as AvroSchema; +use itertools::{Either, Itertools}; +use serde_json::{Number, Value}; + +const FILED_ID_PROP: &str = "field-id"; +const UUID_BYTES: usize = 16; +const UUID_LOGICAL_TYPE: &str = "uuid"; +// # TODO: https://github.com/apache/iceberg-rust/issues/86 +// This const may better to maintain in avro-rs. +const LOGICAL_TYPE: &str = "logicalType"; + +struct SchemaToAvroSchema { + schema: String, +} + +type AvroSchemaOrField = Either; + +impl SchemaVisitor for SchemaToAvroSchema { + type T = AvroSchemaOrField; + + fn schema(&mut self, _schema: &Schema, value: AvroSchemaOrField) -> Result { + let mut avro_schema = value.unwrap_left(); + + if let AvroSchema::Record(record) = &mut avro_schema { + record.name = Name::from(self.schema.as_str()); + } else { + return Err(Error::new( + ErrorKind::Unexpected, + "Schema result must be avro record!", + )); + } + + Ok(Either::Left(avro_schema)) + } + + fn field( + &mut self, + field: &NestedFieldRef, + avro_schema: AvroSchemaOrField, + ) -> Result { + let mut field_schema = avro_schema.unwrap_left(); + if let AvroSchema::Record(record) = &mut field_schema { + record.name = Name::from(format!("r{}", field.id).as_str()); + } + + if !field.required { + field_schema = avro_optional(field_schema)?; + } + + let mut avro_record_field = AvroRecordField { + name: field.name.clone(), + schema: field_schema, + order: RecordFieldOrder::Ignore, + position: 0, + doc: field.doc.clone(), + aliases: None, + default: None, + custom_attributes: Default::default(), + }; + + if !field.required { + avro_record_field.default = Some(Value::Null); + } + avro_record_field.custom_attributes.insert( + FILED_ID_PROP.to_string(), + Value::Number(Number::from(field.id)), + ); + + Ok(Either::Right(avro_record_field)) + } + + fn r#struct( + &mut self, + _struct: &StructType, + results: Vec, + ) -> Result { + let avro_fields = results.into_iter().map(|r| r.unwrap_right()).collect_vec(); + + Ok(Either::Left( + // The name of this record schema should be determined later, by schema name or field + // name, here we use a temporary placeholder to do it. + avro_record_schema("null", avro_fields)?, + )) + } + + fn list(&mut self, list: &ListType, value: AvroSchemaOrField) -> Result { + let mut field_schema = value.unwrap_left(); + + if let AvroSchema::Record(record) = &mut field_schema { + record.name = Name::from(format!("r{}", list.element_field.id).as_str()); + } + + if !list.element_field.required { + field_schema = avro_optional(field_schema)?; + } + + // TODO: We need to add element id prop here, but rust's avro schema doesn't support property except record schema. + Ok(Either::Left(AvroSchema::Array(Box::new(field_schema)))) + } + + fn map( + &mut self, + map: &MapType, + key_value: AvroSchemaOrField, + value: AvroSchemaOrField, + ) -> Result { + let key_field_schema = key_value.unwrap_left(); + let mut value_field_schema = value.unwrap_left(); + if !map.value_field.required { + value_field_schema = avro_optional(value_field_schema)?; + } + + if matches!(key_field_schema, AvroSchema::String) { + Ok(Either::Left(AvroSchema::Map(Box::new(value_field_schema)))) + } else { + // Avro map requires that key must be string type. Here we convert it to array if key is + // not string type. + let key_field = { + let mut field = AvroRecordField { + name: map.key_field.name.clone(), + doc: None, + aliases: None, + default: None, + schema: key_field_schema, + order: RecordFieldOrder::Ascending, + position: 0, + custom_attributes: Default::default(), + }; + field.custom_attributes.insert( + FILED_ID_PROP.to_string(), + Value::Number(Number::from(map.key_field.id)), + ); + field + }; + + let value_field = { + let mut field = AvroRecordField { + name: map.value_field.name.clone(), + doc: None, + aliases: None, + default: None, + schema: value_field_schema, + order: RecordFieldOrder::Ignore, + position: 0, + custom_attributes: Default::default(), + }; + field.custom_attributes.insert( + FILED_ID_PROP.to_string(), + Value::Number(Number::from(map.value_field.id)), + ); + field + }; + + let fields = vec![key_field, value_field]; + let item_avro_schema = avro_record_schema( + format!("k{}_v{}", map.key_field.id, map.value_field.id).as_str(), + fields, + )?; + + Ok(Either::Left(AvroSchema::Array(item_avro_schema.into()))) + } + } + + fn primitive(&mut self, p: &PrimitiveType) -> Result { + let avro_schema = match p { + PrimitiveType::Boolean => AvroSchema::Boolean, + PrimitiveType::Int => AvroSchema::Int, + PrimitiveType::Long => AvroSchema::Long, + PrimitiveType::Float => AvroSchema::Float, + PrimitiveType::Double => AvroSchema::Double, + PrimitiveType::Date => AvroSchema::Date, + PrimitiveType::Time => AvroSchema::TimeMicros, + PrimitiveType::Timestamp => AvroSchema::TimestampMicros, + PrimitiveType::Timestamptz => AvroSchema::TimestampMicros, + PrimitiveType::String => AvroSchema::String, + PrimitiveType::Uuid => avro_fixed_schema(UUID_BYTES, Some(UUID_LOGICAL_TYPE))?, + PrimitiveType::Fixed(len) => avro_fixed_schema((*len) as usize, None)?, + PrimitiveType::Binary => AvroSchema::Bytes, + PrimitiveType::Decimal { precision, scale } => { + avro_decimal_schema(*precision as usize, *scale as usize)? + } + }; + Ok(Either::Left(avro_schema)) + } +} + +/// Converting iceberg schema to avro schema. +pub(crate) fn schema_to_avro_schema(name: impl ToString, schema: &Schema) -> Result { + let mut converter = SchemaToAvroSchema { + schema: name.to_string(), + }; + + visit_schema(schema, &mut converter).map(Either::unwrap_left) +} + +fn avro_record_schema(name: &str, fields: Vec) -> Result { + let lookup = fields + .iter() + .enumerate() + .map(|f| (f.1.name.clone(), f.0)) + .collect(); + + Ok(AvroSchema::Record(RecordSchema { + name: Name::new(name)?, + aliases: None, + doc: None, + fields, + lookup, + attributes: Default::default(), + })) +} + +pub(crate) fn avro_fixed_schema(len: usize, logical_type: Option<&str>) -> Result { + let attributes = if let Some(logical_type) = logical_type { + BTreeMap::from([( + LOGICAL_TYPE.to_string(), + Value::String(logical_type.to_string()), + )]) + } else { + Default::default() + }; + Ok(AvroSchema::Fixed(FixedSchema { + name: Name::new(format!("fixed_{len}").as_str())?, + aliases: None, + doc: None, + size: len, + attributes, + })) +} + +pub(crate) fn avro_decimal_schema(precision: usize, scale: usize) -> Result { + Ok(AvroSchema::Decimal(DecimalSchema { + precision, + scale, + inner: Box::new(AvroSchema::Bytes), + })) +} + +fn avro_optional(avro_schema: AvroSchema) -> Result { + Ok(AvroSchema::Union(UnionSchema::new(vec![ + AvroSchema::Null, + avro_schema, + ])?)) +} + +fn is_avro_optional(avro_schema: &AvroSchema) -> bool { + match avro_schema { + AvroSchema::Union(union) => union.is_nullable(), + _ => false, + } +} + +/// Post order avro schema visitor. +pub(crate) trait AvroSchemaVisitor { + type T; + + fn record(&mut self, record: &RecordSchema, fields: Vec) -> Result; + + fn union(&mut self, union: &UnionSchema, options: Vec) -> Result; + + fn array(&mut self, array: &AvroSchema, item: Self::T) -> Result; + fn map(&mut self, map: &AvroSchema, value: Self::T) -> Result; + + fn primitive(&mut self, schema: &AvroSchema) -> Result; +} + +/// Visit avro schema in post order visitor. +pub(crate) fn visit(schema: &AvroSchema, visitor: &mut V) -> Result { + match schema { + AvroSchema::Record(record) => { + let field_results = record + .fields + .iter() + .map(|f| visit(&f.schema, visitor)) + .collect::>>()?; + + visitor.record(record, field_results) + } + AvroSchema::Union(union) => { + let option_results = union + .variants() + .iter() + .map(|f| visit(f, visitor)) + .collect::>>()?; + + visitor.union(union, option_results) + } + AvroSchema::Array(item) => { + let item_result = visit(item, visitor)?; + visitor.array(schema, item_result) + } + AvroSchema::Map(inner) => { + let item_result = visit(inner, visitor)?; + visitor.map(schema, item_result) + } + schema => visitor.primitive(schema), + } +} + +struct AvroSchemaToSchema { + next_id: i32, +} + +impl AvroSchemaToSchema { + fn next_field_id(&mut self) -> i32 { + self.next_id += 1; + self.next_id + } +} + +impl AvroSchemaVisitor for AvroSchemaToSchema { + // Only `AvroSchema::Null` will return `None` + type T = Option; + + fn record( + &mut self, + record: &RecordSchema, + field_types: Vec>, + ) -> Result> { + let mut fields = Vec::with_capacity(field_types.len()); + for (avro_field, typ) in record.fields.iter().zip_eq(field_types) { + let field_id = avro_field + .custom_attributes + .get(FILED_ID_PROP) + .and_then(Value::as_i64) + .ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Can't convert field, missing field id: {avro_field:?}"), + ) + })?; + + let optional = is_avro_optional(&avro_field.schema); + + let mut field = if optional { + NestedField::optional(field_id as i32, &avro_field.name, typ.unwrap()) + } else { + NestedField::required(field_id as i32, &avro_field.name, typ.unwrap()) + }; + + if let Some(doc) = &avro_field.doc { + field = field.with_doc(doc); + } + + fields.push(field.into()); + } + + Ok(Some(Type::Struct(StructType::new(fields)))) + } + + fn union( + &mut self, + union: &UnionSchema, + mut options: Vec>, + ) -> Result> { + ensure_data_valid!( + options.len() <= 2 && !options.is_empty(), + "Can't convert avro union type {:?} to iceberg.", + union + ); + + if options.len() > 1 { + ensure_data_valid!( + options[0].is_none(), + "Can't convert avro union type {:?} to iceberg.", + union + ); + } + + if options.len() == 1 { + Ok(Some(options.remove(0).unwrap())) + } else { + Ok(Some(options.remove(1).unwrap())) + } + } + + fn array(&mut self, array: &AvroSchema, item: Option) -> Result { + if let AvroSchema::Array(item_schema) = array { + let element_field = NestedField::list_element( + self.next_field_id(), + item.unwrap(), + !is_avro_optional(item_schema), + ) + .into(); + Ok(Some(Type::List(ListType { element_field }))) + } else { + Err(Error::new( + ErrorKind::Unexpected, + "Expected avro array schema, but {array}", + )) + } + } + + fn map(&mut self, map: &AvroSchema, value: Option) -> Result> { + if let AvroSchema::Map(value_schema) = map { + // Due to avro rust implementation's limitation, we can't store attributes in map schema, + // we will fix it later when it has been resolved. + let key_field = NestedField::map_key_element( + self.next_field_id(), + Type::Primitive(PrimitiveType::String), + ); + let value_field = NestedField::map_value_element( + self.next_field_id(), + value.unwrap(), + !is_avro_optional(value_schema), + ); + Ok(Some(Type::Map(MapType { + key_field: key_field.into(), + value_field: value_field.into(), + }))) + } else { + Err(Error::new( + ErrorKind::Unexpected, + "Expected avro map schema, but {map}", + )) + } + } + + fn primitive(&mut self, schema: &AvroSchema) -> Result> { + let typ = match schema { + AvroSchema::Decimal(decimal) => { + Type::decimal(decimal.precision as u32, decimal.scale as u32)? + } + AvroSchema::Date => Type::Primitive(PrimitiveType::Date), + AvroSchema::TimeMicros => Type::Primitive(PrimitiveType::Time), + AvroSchema::TimestampMicros => Type::Primitive(PrimitiveType::Timestamp), + AvroSchema::Boolean => Type::Primitive(PrimitiveType::Boolean), + AvroSchema::Int => Type::Primitive(PrimitiveType::Int), + AvroSchema::Long => Type::Primitive(PrimitiveType::Long), + AvroSchema::Float => Type::Primitive(PrimitiveType::Float), + AvroSchema::Double => Type::Primitive(PrimitiveType::Double), + AvroSchema::String | AvroSchema::Enum(_) => Type::Primitive(PrimitiveType::String), + AvroSchema::Fixed(fixed) => { + if let Some(logical_type) = fixed.attributes.get(LOGICAL_TYPE) { + let logical_type = logical_type.as_str().ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + "logicalType in attributes of avro schema is not a string type", + ) + })?; + match logical_type { + UUID_LOGICAL_TYPE => Type::Primitive(PrimitiveType::Uuid), + ty => { + return Err(Error::new( + ErrorKind::FeatureUnsupported, + format!( + "Logical type {ty} is not support in iceberg primitive type.", + ), + )) + } + } + } else { + Type::Primitive(PrimitiveType::Fixed(fixed.size as u64)) + } + } + AvroSchema::Bytes => Type::Primitive(PrimitiveType::Binary), + AvroSchema::Null => return Ok(None), + _ => { + return Err(Error::new( + ErrorKind::Unexpected, + "Unable to convert avro {schema} to iceberg primitive type.", + )) + } + }; + + Ok(Some(typ)) + } +} + +/// Converts avro schema to iceberg schema. +pub(crate) fn avro_schema_to_schema(avro_schema: &AvroSchema) -> Result { + if let AvroSchema::Record(_) = avro_schema { + let mut converter = AvroSchemaToSchema { next_id: 0 }; + let typ = visit(avro_schema, &mut converter)?.expect("Iceberg schema should not be none."); + if let Type::Struct(s) = typ { + Schema::builder() + .with_fields(s.fields().iter().cloned()) + .build() + } else { + Err(Error::new( + ErrorKind::Unexpected, + format!("Expected to convert avro record schema to struct type, but {typ}"), + )) + } + } else { + Err(Error::new( + ErrorKind::DataInvalid, + "Can't convert non record avro schema to iceberg schema: {avro_schema}", + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::avro::schema::AvroSchemaToSchema; + use crate::spec::{ListType, MapType, NestedField, PrimitiveType, Schema, StructType, Type}; + use apache_avro::schema::{Namespace, UnionSchema}; + use apache_avro::Schema as AvroSchema; + use std::fs::read_to_string; + + fn read_test_data_file_to_avro_schema(filename: &str) -> AvroSchema { + let input = read_to_string(format!( + "{}/testdata/{}", + env!("CARGO_MANIFEST_DIR"), + filename + )) + .unwrap(); + + AvroSchema::parse_str(input.as_str()).unwrap() + } + + fn check_schema_conversion( + avro_schema: AvroSchema, + expected_iceberg_schema: Schema, + check_avro_to_iceberg: bool, + ) { + if check_avro_to_iceberg { + let converted_iceberg_schema = avro_schema_to_schema(&avro_schema).unwrap(); + assert_eq!(expected_iceberg_schema, converted_iceberg_schema); + } + + let converted_avro_schema = schema_to_avro_schema( + avro_schema.name().unwrap().fullname(Namespace::None), + &expected_iceberg_schema, + ) + .unwrap(); + assert_eq!(avro_schema, converted_avro_schema); + } + + #[test] + fn test_manifest_file_v1_schema() { + let fields = vec![ + NestedField::required(500, "manifest_path", PrimitiveType::String.into()) + .with_doc("Location URI with FS scheme") + .into(), + NestedField::required(501, "manifest_length", PrimitiveType::Long.into()) + .with_doc("Total file size in bytes") + .into(), + NestedField::required(502, "partition_spec_id", PrimitiveType::Int.into()) + .with_doc("Spec ID used to write") + .into(), + NestedField::optional(503, "added_snapshot_id", PrimitiveType::Long.into()) + .with_doc("Snapshot ID that added the manifest") + .into(), + NestedField::optional(504, "added_data_files_count", PrimitiveType::Int.into()) + .with_doc("Added entry count") + .into(), + NestedField::optional(505, "existing_data_files_count", PrimitiveType::Int.into()) + .with_doc("Existing entry count") + .into(), + NestedField::optional(506, "deleted_data_files_count", PrimitiveType::Int.into()) + .with_doc("Deleted entry count") + .into(), + NestedField::optional( + 507, + "partitions", + ListType { + element_field: NestedField::list_element( + 508, + StructType::new(vec![ + NestedField::required( + 509, + "contains_null", + PrimitiveType::Boolean.into(), + ) + .with_doc("True if any file has a null partition value") + .into(), + NestedField::optional( + 518, + "contains_nan", + PrimitiveType::Boolean.into(), + ) + .with_doc("True if any file has a nan partition value") + .into(), + NestedField::optional(510, "lower_bound", PrimitiveType::Binary.into()) + .with_doc("Partition lower bound for all files") + .into(), + NestedField::optional(511, "upper_bound", PrimitiveType::Binary.into()) + .with_doc("Partition upper bound for all files") + .into(), + ]) + .into(), + true, + ) + .into(), + } + .into(), + ) + .with_doc("Summary for each partition") + .into(), + NestedField::optional(512, "added_rows_count", PrimitiveType::Long.into()) + .with_doc("Added rows count") + .into(), + NestedField::optional(513, "existing_rows_count", PrimitiveType::Long.into()) + .with_doc("Existing rows count") + .into(), + NestedField::optional(514, "deleted_rows_count", PrimitiveType::Long.into()) + .with_doc("Deleted rows count") + .into(), + ]; + + let iceberg_schema = Schema::builder().with_fields(fields).build().unwrap(); + check_schema_conversion( + read_test_data_file_to_avro_schema("avro_schema_manifest_file_v1.json"), + iceberg_schema, + false, + ); + } + + #[test] + fn test_avro_list_required_primitive() { + let avro_schema = { + AvroSchema::parse_str( + r#" +{ + "type": "record", + "name": "avro_schema", + "fields": [ + { + "name": "array_with_string", + "type": { + "type": "array", + "items": "string", + "default": [], + "element-id": 101 + }, + "field-id": 100 + } + ] +}"#, + ) + .unwrap() + }; + + let iceberg_schema = { + Schema::builder() + .with_fields(vec![NestedField::required( + 100, + "array_with_string", + ListType { + element_field: NestedField::list_element( + 101, + PrimitiveType::String.into(), + true, + ) + .into(), + } + .into(), + ) + .into()]) + .build() + .unwrap() + }; + + check_schema_conversion(avro_schema, iceberg_schema, false); + } + + #[test] + fn test_avro_list_wrapped_primitive() { + let avro_schema = { + AvroSchema::parse_str( + r#" +{ + "type": "record", + "name": "avro_schema", + "fields": [ + { + "name": "array_with_string", + "type": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "element-id": 101 + }, + "field-id": 100 + } + ] +} +"#, + ) + .unwrap() + }; + + let iceberg_schema = { + Schema::builder() + .with_fields(vec![NestedField::required( + 100, + "array_with_string", + ListType { + element_field: NestedField::list_element( + 101, + PrimitiveType::String.into(), + true, + ) + .into(), + } + .into(), + ) + .into()]) + .build() + .unwrap() + }; + + check_schema_conversion(avro_schema, iceberg_schema, false); + } + + #[test] + fn test_avro_list_required_record() { + let avro_schema = { + AvroSchema::parse_str( + r#" +{ + "type": "record", + "name": "avro_schema", + "fields": [ + { + "name": "array_with_record", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "r101", + "fields": [ + { + "name": "contains_null", + "type": "boolean", + "field-id": 102 + }, + { + "name": "contains_nan", + "type": ["null", "boolean"], + "field-id": 103 + } + ] + }, + "element-id": 101 + }, + "field-id": 100 + } + ] +} +"#, + ) + .unwrap() + }; + + let iceberg_schema = { + Schema::builder() + .with_fields(vec![NestedField::required( + 100, + "array_with_record", + ListType { + element_field: NestedField::list_element( + 101, + StructType::new(vec![ + NestedField::required( + 102, + "contains_null", + PrimitiveType::Boolean.into(), + ) + .into(), + NestedField::optional( + 103, + "contains_nan", + PrimitiveType::Boolean.into(), + ) + .into(), + ]) + .into(), + true, + ) + .into(), + } + .into(), + ) + .into()]) + .build() + .unwrap() + }; + + check_schema_conversion(avro_schema, iceberg_schema, false); + } + + #[test] + fn test_resolve_union() { + let avro_schema = UnionSchema::new(vec![ + AvroSchema::Null, + AvroSchema::String, + AvroSchema::Boolean, + ]) + .unwrap(); + + let mut converter = AvroSchemaToSchema { next_id: 0 }; + + let options = avro_schema + .variants() + .iter() + .map(|v| converter.primitive(v).unwrap()) + .collect(); + assert!(converter.union(&avro_schema, options).is_err()); + } + + #[test] + fn test_string_type() { + let mut converter = AvroSchemaToSchema { next_id: 0 }; + let avro_schema = AvroSchema::String; + + assert_eq!( + Some(PrimitiveType::String.into()), + converter.primitive(&avro_schema).unwrap() + ); + } + + #[test] + fn test_map_type() { + let avro_schema = { + AvroSchema::parse_str( + r#" +{ + "type": "map", + "values": ["null", "long"], + "key-id": 101, + "value-id": 102 +} +"#, + ) + .unwrap() + }; + + let mut converter = AvroSchemaToSchema { next_id: 0 }; + let iceberg_type = Type::Map(MapType { + key_field: NestedField::map_key_element(1, PrimitiveType::String.into()).into(), + value_field: NestedField::map_value_element(2, PrimitiveType::Long.into(), false) + .into(), + }); + + assert_eq!( + iceberg_type, + converter + .map(&avro_schema, Some(PrimitiveType::Long.into())) + .unwrap() + .unwrap() + ); + } + + #[test] + fn test_fixed_type() { + let avro_schema = { + AvroSchema::parse_str( + r#" + {"name": "test", "type": "fixed", "size": 22} + "#, + ) + .unwrap() + }; + + let mut converter = AvroSchemaToSchema { next_id: 0 }; + + let iceberg_type = Type::from(PrimitiveType::Fixed(22)); + + assert_eq!( + iceberg_type, + converter.primitive(&avro_schema).unwrap().unwrap() + ); + } + + #[test] + fn test_unknown_primitive() { + let mut converter = AvroSchemaToSchema { next_id: 0 }; + + assert!(converter.primitive(&AvroSchema::Duration).is_err()); + } + + #[test] + fn test_no_field_id() { + let avro_schema = { + AvroSchema::parse_str( + r#" +{ + "type": "record", + "name": "avro_schema", + "fields": [ + { + "name": "array_with_string", + "type": "string" + } + ] +} +"#, + ) + .unwrap() + }; + + assert!(avro_schema_to_schema(&avro_schema).is_err()); + } + + #[test] + fn test_decimal_type() { + let avro_schema = { + AvroSchema::parse_str( + r#" + {"type": "bytes", "logicalType": "decimal", "precision": 25, "scale": 19} + "#, + ) + .unwrap() + }; + + let mut converter = AvroSchemaToSchema { next_id: 0 }; + + assert_eq!( + Type::decimal(25, 19).unwrap(), + converter.primitive(&avro_schema).unwrap().unwrap() + ); + } + + #[test] + fn test_date_type() { + let mut converter = AvroSchemaToSchema { next_id: 0 }; + + assert_eq!( + Type::from(PrimitiveType::Date), + converter.primitive(&AvroSchema::Date).unwrap().unwrap() + ); + } +} diff --git a/libs/iceberg/src/catalog/mod.rs b/libs/iceberg/src/catalog/mod.rs new file mode 100644 index 0000000..708e6bf --- /dev/null +++ b/libs/iceberg/src/catalog/mod.rs @@ -0,0 +1,1068 @@ +// 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. + +//! Catalog API for Apache Iceberg + +use crate::spec::{ + FormatVersion, Schema, Snapshot, SnapshotReference, SortOrder, UnboundPartitionSpec, +}; +use crate::table::Table; +use crate::{Error, ErrorKind, Result}; +use async_trait::async_trait; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::mem::take; +use std::ops::Deref; +use typed_builder::TypedBuilder; +use urlencoding::encode; +use uuid::Uuid; + +/// The catalog API for Iceberg Rust. +#[async_trait] +pub trait Catalog: Debug + Sync + Send { + /// List namespaces from table. + async fn list_namespaces(&self, parent: Option<&NamespaceIdent>) + -> Result>; + + /// Create a new namespace inside the catalog. + async fn create_namespace( + &self, + namespace: &NamespaceIdent, + properties: HashMap, + ) -> Result; + + /// Get a namespace information from the catalog. + async fn get_namespace(&self, namespace: &NamespaceIdent) -> Result; + + /// Check if namespace exists in catalog. + async fn namespace_exists(&self, namespace: &NamespaceIdent) -> Result; + + /// Update a namespace inside the catalog. + /// + /// # Behavior + /// + /// The properties must be the full set of namespace. + async fn update_namespace( + &self, + namespace: &NamespaceIdent, + properties: HashMap, + ) -> Result<()>; + + /// Drop a namespace from the catalog. + async fn drop_namespace(&self, namespace: &NamespaceIdent) -> Result<()>; + + /// List tables from namespace. + async fn list_tables(&self, namespace: &NamespaceIdent) -> Result>; + + /// Create a new table inside the namespace. + async fn create_table( + &self, + namespace: &NamespaceIdent, + creation: TableCreation, + ) -> Result; + + /// Load table from the catalog. + async fn load_table(&self, table: &TableIdent) -> Result
; + + /// Drop a table from the catalog. + async fn drop_table(&self, table: &TableIdent) -> Result<()>; + + /// Check if a table exists in the catalog. + async fn stat_table(&self, table: &TableIdent) -> Result; + + /// Rename a table in the catalog. + async fn rename_table(&self, src: &TableIdent, dest: &TableIdent) -> Result<()>; + + /// Update a table to the catalog. + async fn update_table(&self, commit: TableCommit) -> Result
; +} + +/// NamespaceIdent represents the identifier of a namespace in the catalog. +/// +/// The namespace identifier is a list of strings, where each string is a +/// component of the namespace. It's catalog implementer's responsibility to +/// handle the namespace identifier correctly. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct NamespaceIdent(Vec); + +impl NamespaceIdent { + /// Create a new namespace identifier with only one level. + pub fn new(name: String) -> Self { + Self(vec![name]) + } + + /// Create a multi-level namespace identifier from vector. + pub fn from_vec(names: Vec) -> Result { + if names.is_empty() { + return Err(Error::new( + ErrorKind::DataInvalid, + "Namespace identifier can't be empty!", + )); + } + Ok(Self(names)) + } + + /// Try to create namespace identifier from an iterator of string. + pub fn from_strs(iter: impl IntoIterator) -> Result { + Self::from_vec(iter.into_iter().map(|s| s.to_string()).collect()) + } + + /// Returns url encoded format. + pub fn encode_in_url(&self) -> String { + encode(&self.as_ref().join("\u{1F}")).to_string() + } + + /// Returns inner strings. + pub fn inner(self) -> Vec { + self.0 + } +} + +impl AsRef> for NamespaceIdent { + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +impl Deref for NamespaceIdent { + type Target = [String]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Namespace represents a namespace in the catalog. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Namespace { + name: NamespaceIdent, + properties: HashMap, +} + +impl Namespace { + /// Create a new namespace. + pub fn new(name: NamespaceIdent) -> Self { + Self::with_properties(name, HashMap::default()) + } + + /// Create a new namespace with properties. + pub fn with_properties(name: NamespaceIdent, properties: HashMap) -> Self { + Self { name, properties } + } + + /// Get the name of the namespace. + pub fn name(&self) -> &NamespaceIdent { + &self.name + } + + /// Get the properties of the namespace. + pub fn properties(&self) -> &HashMap { + &self.properties + } +} + +/// TableIdent represents the identifier of a table in the catalog. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct TableIdent { + /// Namespace of the table. + pub namespace: NamespaceIdent, + /// Table name. + pub name: String, +} + +impl TableIdent { + /// Create a new table identifier. + pub fn new(namespace: NamespaceIdent, name: String) -> Self { + Self { namespace, name } + } + + /// Get the namespace of the table. + pub fn namespace(&self) -> &NamespaceIdent { + &self.namespace + } + + /// Get the name of the table. + pub fn name(&self) -> &str { + &self.name + } + + /// Try to create table identifier from an iterator of string. + pub fn from_strs(iter: impl IntoIterator) -> Result { + let mut vec: Vec = iter.into_iter().map(|s| s.to_string()).collect(); + let table_name = vec.pop().ok_or_else(|| { + Error::new(ErrorKind::DataInvalid, "Table identifier can't be empty!") + })?; + let namespace_ident = NamespaceIdent::from_vec(vec)?; + + Ok(Self { + namespace: namespace_ident, + name: table_name, + }) + } +} + +/// TableCreation represents the creation of a table in the catalog. +#[derive(Debug, TypedBuilder)] +pub struct TableCreation { + /// The name of the table. + pub name: String, + /// The location of the table. + #[builder(default, setter(strip_option))] + pub location: Option, + /// The schema of the table. + pub schema: Schema, + /// The partition spec of the table, could be None. + #[builder(default, setter(strip_option))] + pub partition_spec: Option, + /// The sort order of the table. + #[builder(default, setter(strip_option))] + pub sort_order: Option, + /// The properties of the table. + #[builder(default)] + pub properties: HashMap, +} + +/// TableCommit represents the commit of a table in the catalog. +#[derive(Debug, TypedBuilder)] +#[builder(build_method(vis = "pub(crate)"))] +pub struct TableCommit { + /// The table ident. + ident: TableIdent, + /// The requirements of the table. + /// + /// Commit will fail if the requirements are not met. + requirements: Vec, + /// The updates of the table. + updates: Vec, +} + +impl TableCommit { + /// Return the table identifier. + pub fn identifier(&self) -> &TableIdent { + &self.ident + } + + /// Take all requirements. + pub fn take_requirements(&mut self) -> Vec { + take(&mut self.requirements) + } + + /// Take all updates. + pub fn take_updates(&mut self) -> Vec { + take(&mut self.updates) + } +} + +/// TableRequirement represents a requirement for a table in the catalog. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum TableRequirement { + /// The table must not already exist; used for create transactions + #[serde(rename = "assert-create")] + NotExist, + /// The table UUID must match the requirement. + #[serde(rename = "assert-table-uuid")] + UuidMatch { + /// Uuid of original table. + uuid: Uuid, + }, + /// The table branch or tag identified by the requirement's `reference` must + /// reference the requirement's `snapshot-id`. + #[serde(rename = "assert-ref-snapshot-id")] + RefSnapshotIdMatch { + /// The reference of the table to assert. + r#ref: String, + /// The snapshot id of the table to assert. + /// If the id is `None`, the ref must not already exist. + #[serde(rename = "snapshot-id")] + snapshot_id: Option, + }, + /// The table's last assigned column id must match the requirement. + #[serde(rename = "assert-last-assigned-field-id")] + LastAssignedFieldIdMatch { + /// The last assigned field id of the table to assert. + #[serde(rename = "last-assigned-field-id")] + last_assigned_field_id: i64, + }, + /// The table's current schema id must match the requirement. + #[serde(rename = "assert-current-schema-id")] + CurrentSchemaIdMatch { + /// Current schema id of the table to assert. + #[serde(rename = "current-schema-id")] + current_schema_id: i64, + }, + /// The table's last assigned partition id must match the + /// requirement. + #[serde(rename = "assert-last-assigned-partition-id")] + LastAssignedPartitionIdMatch { + /// Last assigned partition id of the table to assert. + #[serde(rename = "last-assigned-partition-id")] + last_assigned_partition_id: i64, + }, + /// The table's default spec id must match the requirement. + #[serde(rename = "assert-default-spec-id")] + DefaultSpecIdMatch { + /// Default spec id of the table to assert. + #[serde(rename = "default-spec-id")] + default_spec_id: i64, + }, + /// The table's default sort order id must match the requirement. + #[serde(rename = "assert-default-sort-order-id")] + DefaultSortOrderIdMatch { + /// Default sort order id of the table to assert. + #[serde(rename = "default-sort-order-id")] + default_sort_order_id: i64, + }, +} + +/// TableUpdate represents an update to a table in the catalog. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(tag = "action", rename_all = "kebab-case")] +pub enum TableUpdate { + /// Upgrade table's format version + #[serde(rename_all = "kebab-case")] + UpgradeFormatVersion { + /// Target format upgrade to. + format_version: FormatVersion, + }, + /// Assign a new UUID to the table + #[serde(rename_all = "kebab-case")] + AssignUuid { + /// The new UUID to assign. + uuid: Uuid, + }, + /// Add a new schema to the table + #[serde(rename_all = "kebab-case")] + AddSchema { + /// The schema to add. + schema: Schema, + /// The last column id of the table. + last_column_id: Option, + }, + /// Set table's current schema + #[serde(rename_all = "kebab-case")] + SetCurrentSchema { + /// Schema ID to set as current, or -1 to set last added schema + schema_id: i32, + }, + /// Add a new partition spec to the table + AddSpec { + /// The partition spec to add. + spec: UnboundPartitionSpec, + }, + /// Set table's default spec + #[serde(rename_all = "kebab-case")] + SetDefaultSpec { + /// Partition spec ID to set as the default, or -1 to set last added spec + spec_id: i32, + }, + /// Add sort order to table. + #[serde(rename_all = "kebab-case")] + AddSortOrder { + /// Sort order to add. + sort_order: SortOrder, + }, + /// Set table's default sort order + #[serde(rename_all = "kebab-case")] + SetDefaultSortOrder { + /// Sort order ID to set as the default, or -1 to set last added sort order + sort_order_id: i32, + }, + /// Add snapshot to table. + #[serde(rename_all = "kebab-case")] + AddSnapshot { + /// Snapshot to add. + snapshot: Snapshot, + }, + /// Set table's snapshot ref. + #[serde(rename_all = "kebab-case")] + SetSnapshotRef { + /// Name of snapshot reference to set. + ref_name: String, + /// Snapshot reference to set. + #[serde(flatten)] + reference: SnapshotReference, + }, + /// Remove table's snapshots + #[serde(rename_all = "kebab-case")] + RemoveSnapshots { + /// Snapshot ids to remove. + snapshot_ids: Vec, + }, + /// Remove snapshot reference + #[serde(rename_all = "kebab-case")] + RemoveSnapshotRef { + /// Name of snapshot reference to remove. + ref_name: String, + }, + /// Update table's location + SetLocation { + /// New location for table. + location: String, + }, + /// Update table's properties + SetProperties { + /// Properties to update for table. + updates: HashMap, + }, + /// Remove table's properties + RemoveProperties { + /// Properties to remove + removals: Vec, + }, +} + +#[cfg(test)] +mod tests { + use crate::spec::{ + FormatVersion, NestedField, NullOrder, Operation, PrimitiveType, Schema, Snapshot, + SnapshotReference, SnapshotRetention, SortDirection, SortField, SortOrder, Summary, + Transform, Type, UnboundPartitionField, UnboundPartitionSpec, + }; + use crate::{NamespaceIdent, TableIdent, TableRequirement, TableUpdate}; + use serde::de::DeserializeOwned; + use serde::Serialize; + use std::collections::HashMap; + use std::fmt::Debug; + use uuid::uuid; + + #[test] + fn test_create_table_id() { + let table_id = TableIdent { + namespace: NamespaceIdent::from_strs(vec!["ns1"]).unwrap(), + name: "t1".to_string(), + }; + + assert_eq!(table_id, TableIdent::from_strs(vec!["ns1", "t1"]).unwrap()); + } + + fn test_serde_json( + json: impl ToString, + expected: T, + ) { + let json_str = json.to_string(); + let actual: T = serde_json::from_str(&json_str).expect("Failed to parse from json"); + assert_eq!(actual, expected, "Parsed value is not equal to expected"); + + let restored: T = serde_json::from_str( + &serde_json::to_string(&actual).expect("Failed to serialize to json"), + ) + .expect("Failed to parse from serialized json"); + + assert_eq!( + restored, expected, + "Parsed restored value is not equal to expected" + ); + } + + #[test] + fn test_table_uuid() { + test_serde_json( + r#" +{ + "type": "assert-table-uuid", + "uuid": "2cc52516-5e73-41f2-b139-545d41a4e151" +} + "#, + TableRequirement::UuidMatch { + uuid: uuid!("2cc52516-5e73-41f2-b139-545d41a4e151"), + }, + ); + } + + #[test] + fn test_assert_table_not_exists() { + test_serde_json( + r#" +{ + "type": "assert-create" +} + "#, + TableRequirement::NotExist, + ); + } + + #[test] + fn test_assert_ref_snapshot_id() { + test_serde_json( + r#" +{ + "type": "assert-ref-snapshot-id", + "ref": "snapshot-name", + "snapshot-id": null +} + "#, + TableRequirement::RefSnapshotIdMatch { + r#ref: "snapshot-name".to_string(), + snapshot_id: None, + }, + ); + + test_serde_json( + r#" +{ + "type": "assert-ref-snapshot-id", + "ref": "snapshot-name", + "snapshot-id": 1 +} + "#, + TableRequirement::RefSnapshotIdMatch { + r#ref: "snapshot-name".to_string(), + snapshot_id: Some(1), + }, + ); + } + + #[test] + fn test_assert_last_assigned_field_id() { + test_serde_json( + r#" +{ + "type": "assert-last-assigned-field-id", + "last-assigned-field-id": 12 +} + "#, + TableRequirement::LastAssignedFieldIdMatch { + last_assigned_field_id: 12, + }, + ); + } + + #[test] + fn test_assert_current_schema_id() { + test_serde_json( + r#" +{ + "type": "assert-current-schema-id", + "current-schema-id": 4 +} + "#, + TableRequirement::CurrentSchemaIdMatch { + current_schema_id: 4, + }, + ); + } + + #[test] + fn test_assert_last_assigned_partition_id() { + test_serde_json( + r#" +{ + "type": "assert-last-assigned-partition-id", + "last-assigned-partition-id": 1004 +} + "#, + TableRequirement::LastAssignedPartitionIdMatch { + last_assigned_partition_id: 1004, + }, + ); + } + + #[test] + fn test_assert_default_spec_id() { + test_serde_json( + r#" +{ + "type": "assert-default-spec-id", + "default-spec-id": 5 +} + "#, + TableRequirement::DefaultSpecIdMatch { default_spec_id: 5 }, + ); + } + + #[test] + fn test_assert_default_sort_order() { + let json = r#" +{ + "type": "assert-default-sort-order-id", + "default-sort-order-id": 10 +} + "#; + + let update = TableRequirement::DefaultSortOrderIdMatch { + default_sort_order_id: 10, + }; + + test_serde_json(json, update); + } + + #[test] + fn test_parse_assert_invalid() { + assert!( + serde_json::from_str::( + r#" +{ + "default-sort-order-id": 10 +} +"# + ) + .is_err(), + "Table requirements should not be parsed without type." + ); + } + + #[test] + fn test_assign_uuid() { + test_serde_json( + r#" +{ + "action": "assign-uuid", + "uuid": "2cc52516-5e73-41f2-b139-545d41a4e151" +} + "#, + TableUpdate::AssignUuid { + uuid: uuid!("2cc52516-5e73-41f2-b139-545d41a4e151"), + }, + ); + } + + #[test] + fn test_upgrade_format_version() { + test_serde_json( + r#" +{ + "action": "upgrade-format-version", + "format-version": 2 +} + "#, + TableUpdate::UpgradeFormatVersion { + format_version: FormatVersion::V2, + }, + ); + } + + #[test] + fn test_add_schema() { + let test_schema = Schema::builder() + .with_schema_id(1) + .with_identifier_field_ids(vec![2]) + .with_fields(vec![ + NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String)).into(), + NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean)).into(), + ]) + .build() + .unwrap(); + test_serde_json( + r#" +{ + "action": "add-schema", + "schema": { + "type": "struct", + "schema-id": 1, + "fields": [ + { + "id": 1, + "name": "foo", + "required": false, + "type": "string" + }, + { + "id": 2, + "name": "bar", + "required": true, + "type": "int" + }, + { + "id": 3, + "name": "baz", + "required": false, + "type": "boolean" + } + ], + "identifier-field-ids": [ + 2 + ] + }, + "last-column-id": 3 +} + "#, + TableUpdate::AddSchema { + schema: test_schema.clone(), + last_column_id: Some(3), + }, + ); + + test_serde_json( + r#" +{ + "action": "add-schema", + "schema": { + "type": "struct", + "schema-id": 1, + "fields": [ + { + "id": 1, + "name": "foo", + "required": false, + "type": "string" + }, + { + "id": 2, + "name": "bar", + "required": true, + "type": "int" + }, + { + "id": 3, + "name": "baz", + "required": false, + "type": "boolean" + } + ], + "identifier-field-ids": [ + 2 + ] + } +} + "#, + TableUpdate::AddSchema { + schema: test_schema.clone(), + last_column_id: None, + }, + ); + } + + #[test] + fn test_set_current_schema() { + test_serde_json( + r#" +{ + "action": "set-current-schema", + "schema-id": 23 +} + "#, + TableUpdate::SetCurrentSchema { schema_id: 23 }, + ); + } + + #[test] + fn test_add_spec() { + test_serde_json( + r#" +{ + "action": "add-spec", + "spec": { + "fields": [ + { + "source-id": 4, + "name": "ts_day", + "transform": "day" + }, + { + "source-id": 1, + "name": "id_bucket", + "transform": "bucket[16]" + }, + { + "source-id": 2, + "name": "id_truncate", + "transform": "truncate[4]" + } + ] + } +} + "#, + TableUpdate::AddSpec { + spec: UnboundPartitionSpec::builder() + .with_unbound_partition_field( + UnboundPartitionField::builder() + .source_id(4) + .name("ts_day".to_string()) + .transform(Transform::Day) + .build(), + ) + .with_unbound_partition_field( + UnboundPartitionField::builder() + .source_id(1) + .name("id_bucket".to_string()) + .transform(Transform::Bucket(16)) + .build(), + ) + .with_unbound_partition_field( + UnboundPartitionField::builder() + .source_id(2) + .name("id_truncate".to_string()) + .transform(Transform::Truncate(4)) + .build(), + ) + .build() + .unwrap(), + }, + ); + } + + #[test] + fn test_set_default_spec() { + test_serde_json( + r#" +{ + "action": "set-default-spec", + "spec-id": 1 +} + "#, + TableUpdate::SetDefaultSpec { spec_id: 1 }, + ) + } + + #[test] + fn test_add_sort_order() { + let json = r#" +{ + "action": "add-sort-order", + "sort-order": { + "order-id": 1, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } +} + "#; + + let update = TableUpdate::AddSortOrder { + sort_order: SortOrder::builder() + .with_order_id(1) + .with_sort_field( + SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(), + ) + .with_sort_field( + SortField::builder() + .source_id(3) + .direction(SortDirection::Descending) + .null_order(NullOrder::Last) + .transform(Transform::Bucket(4)) + .build(), + ) + .build_unbound() + .unwrap(), + }; + + test_serde_json(json, update); + } + + #[test] + fn test_set_default_order() { + let json = r#" +{ + "action": "set-default-sort-order", + "sort-order-id": 2 +} + "#; + let update = TableUpdate::SetDefaultSortOrder { sort_order_id: 2 }; + + test_serde_json(json, update); + } + + #[test] + fn test_add_snapshot() { + let json = r#" +{ + "action": "add-snapshot", + "snapshot": { + "snapshot-id": 3055729675574597000, + "parent-snapshot-id": 3051729675574597000, + "timestamp-ms": 1555100955770, + "sequence-number": 1, + "summary": { + "operation": "append" + }, + "manifest-list": "s3://a/b/2.avro", + "schema-id": 1 + } +} + "#; + + let update = TableUpdate::AddSnapshot { + snapshot: Snapshot::builder() + .with_snapshot_id(3055729675574597000) + .with_parent_snapshot_id(Some(3051729675574597000)) + .with_timestamp_ms(1555100955770) + .with_sequence_number(1) + .with_manifest_list("s3://a/b/2.avro") + .with_schema_id(1) + .with_summary(Summary { + operation: Operation::Append, + other: HashMap::default(), + }) + .build(), + }; + + test_serde_json(json, update); + } + + #[test] + fn test_remove_snapshots() { + let json = r#" +{ + "action": "remove-snapshots", + "snapshot-ids": [ + 1, + 2 + ] +} + "#; + + let update = TableUpdate::RemoveSnapshots { + snapshot_ids: vec![1, 2], + }; + test_serde_json(json, update); + } + + #[test] + fn test_remove_snapshot_ref() { + let json = r#" +{ + "action": "remove-snapshot-ref", + "ref-name": "snapshot-ref" +} + "#; + + let update = TableUpdate::RemoveSnapshotRef { + ref_name: "snapshot-ref".to_string(), + }; + test_serde_json(json, update); + } + + #[test] + fn test_set_snapshot_ref_tag() { + let json = r#" +{ + "action": "set-snapshot-ref", + "type": "tag", + "ref-name": "hank", + "snapshot-id": 1, + "max-ref-age-ms": 1 +} + "#; + + let update = TableUpdate::SetSnapshotRef { + ref_name: "hank".to_string(), + reference: SnapshotReference { + snapshot_id: 1, + retention: SnapshotRetention::Tag { max_ref_age_ms: 1 }, + }, + }; + + test_serde_json(json, update); + } + + #[test] + fn test_set_snapshot_ref_branch() { + let json = r#" +{ + "action": "set-snapshot-ref", + "type": "branch", + "ref-name": "hank", + "snapshot-id": 1, + "min-snapshots-to-keep": 2, + "max-snapshot-age-ms": 3, + "max-ref-age-ms": 4 +} + "#; + + let update = TableUpdate::SetSnapshotRef { + ref_name: "hank".to_string(), + reference: SnapshotReference { + snapshot_id: 1, + retention: SnapshotRetention::Branch { + min_snapshots_to_keep: Some(2), + max_snapshot_age_ms: Some(3), + max_ref_age_ms: Some(4), + }, + }, + }; + + test_serde_json(json, update); + } + + #[test] + fn test_set_properties() { + let json = r#" +{ + "action": "set-properties", + "updates": { + "prop1": "v1", + "prop2": "v2" + } +} + "#; + + let update = TableUpdate::SetProperties { + updates: vec![ + ("prop1".to_string(), "v1".to_string()), + ("prop2".to_string(), "v2".to_string()), + ] + .into_iter() + .collect(), + }; + + test_serde_json(json, update); + } + + #[test] + fn test_remove_properties() { + let json = r#" +{ + "action": "remove-properties", + "removals": [ + "prop1", + "prop2" + ] +} + "#; + + let update = TableUpdate::RemoveProperties { + removals: vec!["prop1".to_string(), "prop2".to_string()], + }; + + test_serde_json(json, update); + } + + #[test] + fn test_set_location() { + let json = r#" +{ + "action": "set-location", + "location": "s3://bucket/warehouse/tbl_location" +} + "#; + + let update = TableUpdate::SetLocation { + location: "s3://bucket/warehouse/tbl_location".to_string(), + }; + + test_serde_json(json, update); + } +} diff --git a/libs/iceberg/src/error.rs b/libs/iceberg/src/error.rs new file mode 100644 index 0000000..c851402 --- /dev/null +++ b/libs/iceberg/src/error.rs @@ -0,0 +1,429 @@ +// 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. + +use std::backtrace::{Backtrace, BacktraceStatus}; +use std::fmt; +use std::fmt::Debug; +use std::fmt::Display; +use std::fmt::Formatter; + +/// Result that is a wrapper of `Result` +pub type Result = std::result::Result; + +/// ErrorKind is all kinds of Error of iceberg. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ErrorKind { + /// Iceberg don't know what happened here, and no actions other than + /// just returning it back. For example, iceberg returns an internal + /// service error. + Unexpected, + + /// Iceberg data is invalid. + /// + /// This error is returned when we try to read a table from iceberg but + /// failed to parse it's metadata or data file correctly. + /// + /// The table could be invalid or corrupted. + DataInvalid, + /// Iceberg feature is not supported. + /// + /// This error is returned when given iceberg feature is not supported. + FeatureUnsupported, +} + +impl ErrorKind { + /// Convert self into static str. + pub fn into_static(self) -> &'static str { + self.into() + } +} + +impl From for &'static str { + fn from(v: ErrorKind) -> &'static str { + match v { + ErrorKind::Unexpected => "Unexpected", + ErrorKind::DataInvalid => "DataInvalid", + ErrorKind::FeatureUnsupported => "FeatureUnsupported", + } + } +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.into_static()) + } +} + +/// Error is the error struct returned by all iceberg functions. +/// +/// ## Display +/// +/// Error can be displayed in two ways: +/// +/// - Via `Display`: like `err.to_string()` or `format!("{err}")` +/// +/// Error will be printed in a single line: +/// +/// ```shell +/// Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error" +/// ``` +/// +/// - Via `Debug`: like `format!("{err:?}")` +/// +/// Error will be printed in multi lines with more details and backtraces (if captured): +/// +/// ```shell +/// Unexpected => something wrong happened +/// +/// Context: +/// path: /path/to/file +/// called: send_async +/// +/// Source: networking error +/// +/// Backtrace: +/// 0: iceberg::error::Error::new +/// at ./src/error.rs:197:24 +/// 1: iceberg::error::tests::generate_error +/// at ./src/error.rs:241:9 +/// 2: iceberg::error::tests::test_error_debug_with_backtrace::{{closure}} +/// at ./src/error.rs:305:41 +/// ... +/// ``` +pub struct Error { + kind: ErrorKind, + message: String, + + context: Vec<(&'static str, String)>, + + source: Option, + backtrace: Backtrace, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.kind)?; + + if !self.context.is_empty() { + write!(f, ", context: {{ ")?; + write!( + f, + "{}", + self.context + .iter() + .map(|(k, v)| format!("{k}: {v}")) + .collect::>() + .join(", ") + )?; + write!(f, " }}")?; + } + + if !self.message.is_empty() { + write!(f, " => {}", self.message)?; + } + + if let Some(source) = &self.source { + write!(f, ", source: {source}")?; + } + + Ok(()) + } +} + +impl Debug for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // If alternate has been specified, we will print like Debug. + if f.alternate() { + let mut de = f.debug_struct("Error"); + de.field("kind", &self.kind); + de.field("message", &self.message); + de.field("context", &self.context); + de.field("source", &self.source); + de.field("backtrace", &self.backtrace); + return de.finish(); + } + + write!(f, "{}", self.kind)?; + if !self.message.is_empty() { + write!(f, " => {}", self.message)?; + } + writeln!(f)?; + + if !self.context.is_empty() { + writeln!(f)?; + writeln!(f, "Context:")?; + for (k, v) in self.context.iter() { + writeln!(f, " {k}: {v}")?; + } + } + if let Some(source) = &self.source { + writeln!(f)?; + writeln!(f, "Source: {source:#}")?; + } + + if self.backtrace.status() == BacktraceStatus::Captured { + writeln!(f)?; + writeln!(f, "Backtrace:")?; + writeln!(f, "{}", self.backtrace)?; + } + + Ok(()) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|v| v.as_ref()) + } +} + +impl Error { + /// Create a new Error with error kind and message. + pub fn new(kind: ErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + context: Vec::default(), + + source: None, + // `Backtrace::capture()` will check if backtrace has been enabled + // internally. It's zero cost if backtrace is disabled. + backtrace: Backtrace::capture(), + } + } + + /// Add more context in error. + pub fn with_context(mut self, key: &'static str, value: impl Into) -> Self { + self.context.push((key, value.into())); + self + } + + /// Set source for error. + /// + /// # Notes + /// + /// If the source has been set, we will raise a panic here. + pub fn with_source(mut self, src: impl Into) -> Self { + debug_assert!(self.source.is_none(), "the source error has been set"); + + self.source = Some(src.into()); + self + } + + /// Set the backtrace for error. + /// + /// This function is served as testing purpose and not intended to be called + /// by users. + #[cfg(test)] + fn with_backtrace(mut self, backtrace: Backtrace) -> Self { + self.backtrace = backtrace; + self + } + + /// Return error's kind. + /// + /// Users can use this method to check error's kind and take actions. + pub fn kind(&self) -> ErrorKind { + self.kind + } + + /// Return error's message. + #[inline] + pub fn message(&self) -> &str { + self.message.as_str() + } +} + +macro_rules! define_from_err { + ($source: path, $error_kind: path, $msg: expr) => { + impl std::convert::From<$source> for crate::error::Error { + fn from(v: $source) -> Self { + Self::new($error_kind, $msg).with_source(v) + } + } + }; +} + +define_from_err!( + std::str::Utf8Error, + ErrorKind::Unexpected, + "handling invalid utf-8 characters" +); + +define_from_err!( + std::array::TryFromSliceError, + ErrorKind::DataInvalid, + "failed to convert byte slive to array" +); + +define_from_err!( + std::num::TryFromIntError, + ErrorKind::DataInvalid, + "failed to convert integer" +); + +define_from_err!( + chrono::ParseError, + ErrorKind::DataInvalid, + "Failed to parse string to date or time" +); + +define_from_err!( + uuid::Error, + ErrorKind::DataInvalid, + "Failed to convert between uuid und iceberg value" +); + +define_from_err!( + apache_avro::Error, + ErrorKind::DataInvalid, + "Failure in conversion with avro" +); + +define_from_err!( + opendal::Error, + ErrorKind::Unexpected, + "Failure in doing io operation" +); + +define_from_err!( + url::ParseError, + ErrorKind::DataInvalid, + "Failed to parse url" +); + +define_from_err!( + reqwest::Error, + ErrorKind::Unexpected, + "Failed to execute http request" +); + +define_from_err!( + serde_json::Error, + ErrorKind::DataInvalid, + "Failed to parse json string" +); + +define_from_err!( + rust_decimal::Error, + ErrorKind::DataInvalid, + "Failed to convert decimal literal to rust decimal" +); + +define_from_err!(std::io::Error, ErrorKind::Unexpected, "IO Operation failed"); + +/// Helper macro to check arguments. +/// +/// +/// Example: +/// +/// Following example check `a > 0`, otherwise returns an error. +/// ```ignore +/// use iceberg::check; +/// ensure_data_valid!(a > 0, "{} is not positive.", a); +/// ``` +#[macro_export] +macro_rules! ensure_data_valid { + ($cond: expr, $fmt: literal, $($arg:tt)*) => { + if !$cond { + return Err($crate::error::Error::new($crate::error::ErrorKind::DataInvalid, format!($fmt, $($arg)*))) + } + }; +} + +#[cfg(test)] +mod tests { + use anyhow::anyhow; + use pretty_assertions::assert_eq; + + use super::*; + + fn generate_error_with_backtrace_disabled() -> Error { + Error::new( + ErrorKind::Unexpected, + "something wrong happened".to_string(), + ) + .with_context("path", "/path/to/file".to_string()) + .with_context("called", "send_async".to_string()) + .with_source(anyhow!("networking error")) + .with_backtrace(Backtrace::disabled()) + } + + fn generate_error_with_backtrace_enabled() -> Error { + Error::new( + ErrorKind::Unexpected, + "something wrong happened".to_string(), + ) + .with_context("path", "/path/to/file".to_string()) + .with_context("called", "send_async".to_string()) + .with_source(anyhow!("networking error")) + .with_backtrace(Backtrace::force_capture()) + } + + #[test] + fn test_error_display_without_backtrace() { + let s = format!("{}", generate_error_with_backtrace_disabled()); + assert_eq!( + s, + r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"# + ) + } + + #[test] + fn test_error_display_with_backtrace() { + let s = format!("{}", generate_error_with_backtrace_enabled()); + assert_eq!( + s, + r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"# + ) + } + + #[test] + fn test_error_debug_without_backtrace() { + let s = format!("{:?}", generate_error_with_backtrace_disabled()); + assert_eq!( + s, + r#"Unexpected => something wrong happened + +Context: + path: /path/to/file + called: send_async + +Source: networking error +"# + ) + } + + /// Backtrace contains build information, so we just assert the header of error content. + #[test] + fn test_error_debug_with_backtrace() { + let s = format!("{:?}", generate_error_with_backtrace_enabled()); + + let expected = r#"Unexpected => something wrong happened + +Context: + path: /path/to/file + called: send_async + +Source: networking error + +Backtrace: + 0:"#; + assert_eq!(&s[..expected.len()], expected,); + } +} diff --git a/libs/iceberg/src/expr/mod.rs b/libs/iceberg/src/expr/mod.rs new file mode 100644 index 0000000..aef1444 --- /dev/null +++ b/libs/iceberg/src/expr/mod.rs @@ -0,0 +1,42 @@ +// 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. + +//! This module contains expressions. + +mod term; +pub use term::*; +mod predicate; +pub use predicate::*; + +/// Predicate operators used in expressions. +#[allow(missing_docs)] +pub enum PredicateOperator { + IsNull, + NotNull, + IsNan, + NotNan, + LessThan, + LessThanOrEq, + GreaterThan, + GreaterThanOrEq, + Eq, + NotEq, + In, + NotIn, + StartsWith, + NotStartsWith, +} diff --git a/libs/iceberg/src/expr/predicate.rs b/libs/iceberg/src/expr/predicate.rs new file mode 100644 index 0000000..9d6bf86 --- /dev/null +++ b/libs/iceberg/src/expr/predicate.rs @@ -0,0 +1,93 @@ +// 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. + +//! This module contains predicate expressions. +//! Predicate expressions are used to filter data, and evaluates to a boolean value. For example, +//! `a > 10` is a predicate expression, and it evaluates to `true` if `a` is greater than `10`, + +use crate::expr::{BoundReference, PredicateOperator, UnboundReference}; +use crate::spec::Literal; +use std::collections::HashSet; + +/// Logical expression, such as `AND`, `OR`, `NOT`. +pub struct LogicalExpression { + inputs: [Box; N], +} + +/// Unary predicate, for example, `a IS NULL`. +pub struct UnaryExpression { + /// Operator of this predicate, must be single operand operator. + op: PredicateOperator, + /// Term of this predicate, for example, `a` in `a IS NULL`. + term: T, +} + +/// Binary predicate, for example, `a > 10`. +pub struct BinaryExpression { + /// Operator of this predicate, must be binary operator, such as `=`, `>`, `<`, etc. + op: PredicateOperator, + /// Term of this predicate, for example, `a` in `a > 10`. + term: T, + /// Literal of this predicate, for example, `10` in `a > 10`. + literal: Literal, +} + +/// Set predicates, for example, `a in (1, 2, 3)`. +pub struct SetExpression { + /// Operator of this predicate, must be set operator, such as `IN`, `NOT IN`, etc. + op: PredicateOperator, + /// Term of this predicate, for example, `a` in `a in (1, 2, 3)`. + term: T, + /// Literals of this predicate, for example, `(1, 2, 3)` in `a in (1, 2, 3)`. + literals: HashSet, +} + +/// Unbound predicate expression before binding to a schema. +pub enum UnboundPredicate { + /// And predicate, for example, `a > 10 AND b < 20`. + And(LogicalExpression), + /// Or predicate, for example, `a > 10 OR b < 20`. + Or(LogicalExpression), + /// Not predicate, for example, `NOT (a > 10)`. + Not(LogicalExpression), + /// Unary expression, for example, `a IS NULL`. + Unary(UnaryExpression), + /// Binary expression, for example, `a > 10`. + Binary(BinaryExpression), + /// Set predicates, for example, `a in (1, 2, 3)`. + Set(SetExpression), +} + +/// Bound predicate expression after binding to a schema. +pub enum BoundPredicate { + /// An expression always evaluates to true. + AlwaysTrue, + /// An expression always evaluates to false. + AlwaysFalse, + /// An expression combined by `AND`, for example, `a > 10 AND b < 20`. + And(LogicalExpression), + /// An expression combined by `OR`, for example, `a > 10 OR b < 20`. + Or(LogicalExpression), + /// An expression combined by `NOT`, for example, `NOT (a > 10)`. + Not(LogicalExpression), + /// Unary expression, for example, `a IS NULL`. + Unary(UnaryExpression), + /// Binary expression, for example, `a > 10`. + Binary(BinaryExpression), + /// Set predicates, for example, `a in (1, 2, 3)`. + Set(SetExpression), +} diff --git a/libs/iceberg/src/expr/term.rs b/libs/iceberg/src/expr/term.rs new file mode 100644 index 0000000..5a81ecd --- /dev/null +++ b/libs/iceberg/src/expr/term.rs @@ -0,0 +1,37 @@ +// 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. + +//! Term definition. + +use crate::spec::NestedFieldRef; + +/// Unbound term before binding to a schema. +pub type UnboundTerm = UnboundReference; + +/// A named reference in an unbound expression. +/// For example, `a` in `a > 10`. +pub struct UnboundReference { + name: String, +} + +/// A named reference in a bound expression after binding to a schema. +pub struct BoundReference { + field: NestedFieldRef, +} + +/// Bound term after binding to a schema. +pub type BoundTerm = BoundReference; diff --git a/libs/iceberg/src/io.rs b/libs/iceberg/src/io.rs new file mode 100644 index 0000000..3a7c85f --- /dev/null +++ b/libs/iceberg/src/io.rs @@ -0,0 +1,508 @@ +// 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. + +//! File io implementation. +//! +//! # How to build `FileIO` +//! +//! We provided a `FileIOBuilder` to build `FileIO` from scratch. For example: +//! ```rust +//! use iceberg::io::{FileIOBuilder, S3_REGION}; +//! +//! let file_io = FileIOBuilder::new("s3") +//! .with_prop(S3_REGION, "us-east-1") +//! .build() +//! .unwrap(); +//! ``` +//! +//! Or you can pass a path to ask `FileIO` to infer schema for you: +//! ```rust +//! use iceberg::io::{FileIO, S3_REGION}; +//! let file_io = FileIO::from_path("s3://bucket/a") +//! .unwrap() +//! .with_prop(S3_REGION, "us-east-1") +//! .build() +//! .unwrap(); +//! ``` +//! +//! # How to use `FileIO` +//! +//! Currently `FileIO` provides simple methods for file operations: +//! +//! - `delete`: Delete file. +//! - `is_exist`: Check if file exists. +//! - `new_input`: Create input file for reading. +//! - `new_output`: Create output file for writing. + +use std::{collections::HashMap, sync::Arc}; + +use crate::{error::Result, Error, ErrorKind}; +use futures::{AsyncRead, AsyncSeek, AsyncWrite}; +use once_cell::sync::Lazy; +use opendal::{Operator, Scheme}; +use url::Url; + +/// Following are arguments for [s3 file io](https://py.iceberg.apache.org/configuration/#s3). +/// S3 endopint. +pub const S3_ENDPOINT: &str = "s3.endpoint"; +/// S3 access key id. +pub const S3_ACCESS_KEY_ID: &str = "s3.access-key-id"; +/// S3 secret access key. +pub const S3_SECRET_ACCESS_KEY: &str = "s3.secret-access-key"; +/// S3 region. +pub const S3_REGION: &str = "s3.region"; + +/// A mapping from iceberg s3 configuration key to [`opendal::Operator`] configuration key. +static S3_CONFIG_MAPPING: Lazy> = Lazy::new(|| { + let mut m = HashMap::with_capacity(4); + m.insert(S3_ENDPOINT, "endpoint"); + m.insert(S3_ACCESS_KEY_ID, "access_key_id"); + m.insert(S3_SECRET_ACCESS_KEY, "secret_access_key"); + m.insert(S3_REGION, "region"); + + m +}); + +const DEFAULT_ROOT_PATH: &str = "/"; + +/// FileIO implementation, used to manipulate files in underlying storage. +/// +/// # Note +/// +/// All path passed to `FileIO` must be absolute path starting with scheme string used to construct `FileIO`. +/// For example, if you construct `FileIO` with `s3a` scheme, then all path passed to `FileIO` must start with `s3a://`. +#[derive(Clone, Debug)] +pub struct FileIO { + inner: Arc, +} + +/// Builder for [`FileIO`]. +#[derive(Debug)] +pub struct FileIOBuilder { + /// This is used to infer scheme of operator. + /// + /// If this is `None`, then [`FileIOBuilder::build`](FileIOBuilder::build) will build a local file io. + scheme_str: Option, + /// Arguments for operator. + props: HashMap, +} + +impl FileIOBuilder { + /// Creates a new builder with scheme. + pub fn new(scheme_str: impl ToString) -> Self { + Self { + scheme_str: Some(scheme_str.to_string()), + props: HashMap::default(), + } + } + + /// Creates a new builder for local file io. + pub fn new_fs_io() -> Self { + Self { + scheme_str: None, + props: HashMap::default(), + } + } + + /// Add argument for operator. + pub fn with_prop(mut self, key: impl ToString, value: impl ToString) -> Self { + self.props.insert(key.to_string(), value.to_string()); + self + } + + /// Add argument for operator. + pub fn with_props( + mut self, + args: impl IntoIterator, + ) -> Self { + self.props + .extend(args.into_iter().map(|e| (e.0.to_string(), e.1.to_string()))); + self + } + + /// Builds [`FileIO`]. + pub fn build(self) -> Result { + let storage = Storage::build(self)?; + Ok(FileIO { + inner: Arc::new(storage), + }) + } +} + +impl FileIO { + /// Try to infer file io scheme from path. + /// + /// If it's a valid url, for example http://example.org, url scheme will be used. + /// If it's not a valid url, will try to detect if it's a file path. + /// + /// Otherwise will return parsing error. + pub fn from_path(path: impl AsRef) -> Result { + let url = Url::parse(path.as_ref()) + .map_err(Error::from) + .or_else(|e| { + Url::from_file_path(path.as_ref()).map_err(|_| { + Error::new( + ErrorKind::DataInvalid, + "Input is neither a valid url nor path", + ) + .with_context("input", path.as_ref().to_string()) + .with_source(e) + }) + })?; + + Ok(FileIOBuilder::new(url.scheme())) + } + + /// Deletes file. + pub async fn delete(&self, path: impl AsRef) -> Result<()> { + let (op, relative_path) = self.inner.create_operator(&path)?; + Ok(op.delete(relative_path).await?) + } + + /// Check file exists. + pub async fn is_exist(&self, path: impl AsRef) -> Result { + let (op, relative_path) = self.inner.create_operator(&path)?; + Ok(op.is_exist(relative_path).await?) + } + + /// Creates input file. + pub fn new_input(&self, path: impl AsRef) -> Result { + let (op, relative_path) = self.inner.create_operator(&path)?; + let path = path.as_ref().to_string(); + let relative_path_pos = path.len() - relative_path.len(); + Ok(InputFile { + op, + path, + relative_path_pos, + }) + } + + /// Creates output file. + pub fn new_output(&self, path: impl AsRef) -> Result { + let (op, relative_path) = self.inner.create_operator(&path)?; + let path = path.as_ref().to_string(); + let relative_path_pos = path.len() - relative_path.len(); + Ok(OutputFile { + op, + path, + relative_path_pos, + }) + } +} + +/// Input file is used for reading from files. +#[derive(Debug)] +pub struct InputFile { + op: Operator, + // Absolution path of file. + path: String, + // Relative path of file to uri, starts at [`relative_path_pos`] + relative_path_pos: usize, +} + +/// Trait for reading file. +pub trait FileRead: AsyncRead + AsyncSeek {} + +impl FileRead for T where T: AsyncRead + AsyncSeek {} + +impl InputFile { + /// Absolute path to root uri. + pub fn location(&self) -> &str { + &self.path + } + + /// Check if file exists. + pub async fn exists(&self) -> Result { + Ok(self + .op + .is_exist(&self.path[self.relative_path_pos..]) + .await?) + } + + /// Creates [`InputStream`] for reading. + pub async fn reader(&self) -> Result { + Ok(self.op.reader(&self.path[self.relative_path_pos..]).await?) + } +} + +/// Trait for writing file. +pub trait FileWrite: AsyncWrite {} + +impl FileWrite for T where T: AsyncWrite {} + +/// Output file is used for writing to files.. +#[derive(Debug)] +pub struct OutputFile { + op: Operator, + // Absolution path of file. + path: String, + // Relative path of file to uri, starts at [`relative_path_pos`] + relative_path_pos: usize, +} + +impl OutputFile { + /// Relative path to root uri. + pub fn location(&self) -> &str { + &self.path + } + + /// Checks if file exists. + pub async fn exists(&self) -> Result { + Ok(self + .op + .is_exist(&self.path[self.relative_path_pos..]) + .await?) + } + + /// Converts into [`InputFile`]. + pub fn to_input_file(self) -> InputFile { + InputFile { + op: self.op, + path: self.path, + relative_path_pos: self.relative_path_pos, + } + } + + /// Creates output file for writing. + pub async fn writer(&self) -> Result { + Ok(self.op.writer(&self.path[self.relative_path_pos..]).await?) + } +} + +// We introduce this because I don't want to handle unsupported `Scheme` in every method. +#[derive(Debug)] +enum Storage { + LocalFs { + op: Operator, + }, + S3 { + scheme_str: String, + props: HashMap, + }, +} + +impl Storage { + /// Creates operator from path. + /// + /// # Arguments + /// + /// * path: It should be *absolute* path starting with scheme string used to construct [`FileIO`]. + /// + /// # Returns + /// + /// The return value consists of two parts: + /// + /// * An [`opendal::Operator`] instance used to operate on file. + /// * Relative path to the root uri of [`opendal::Operator`]. + /// + fn create_operator<'a>(&self, path: &'a impl AsRef) -> Result<(Operator, &'a str)> { + let path = path.as_ref(); + match self { + Storage::LocalFs { op } => { + if let Some(stripped) = path.strip_prefix("file:/") { + Ok((op.clone(), stripped)) + } else { + Ok((op.clone(), &path[1..])) + } + } + Storage::S3 { scheme_str, props } => { + let mut props = props.clone(); + let url = Url::parse(path)?; + let bucket = url.host_str().ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Invalid s3 url: {}, missing bucket", path), + ) + })?; + + props.insert("bucket".to_string(), bucket.to_string()); + + let prefix = format!("{}://{}/", scheme_str, bucket); + if path.starts_with(&prefix) { + Ok((Operator::via_map(Scheme::S3, props)?, &path[prefix.len()..])) + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("Invalid s3 url: {}, should start with {}", path, prefix), + )) + } + } + } + } + + /// Parse scheme. + fn parse_scheme(scheme: &str) -> Result { + match scheme { + "file" | "" => Ok(Scheme::Fs), + "s3" | "s3a" => Ok(Scheme::S3), + s => Ok(s.parse::()?), + } + } + + /// Convert iceberg config to opendal config. + fn build(file_io_builder: FileIOBuilder) -> Result { + let scheme_str = file_io_builder.scheme_str.unwrap_or("".to_string()); + let scheme = Self::parse_scheme(&scheme_str)?; + let mut new_props = HashMap::default(); + new_props.insert("root".to_string(), DEFAULT_ROOT_PATH.to_string()); + + match scheme { + Scheme::Fs => Ok(Self::LocalFs { + op: Operator::via_map(Scheme::Fs, new_props)?, + }), + Scheme::S3 => { + for prop in file_io_builder.props { + if let Some(op_key) = S3_CONFIG_MAPPING.get(prop.0.as_str()) { + new_props.insert(op_key.to_string(), prop.1); + } + } + + Ok(Self::S3 { + scheme_str, + props: new_props, + }) + } + _ => Err(Error::new( + ErrorKind::FeatureUnsupported, + format!("Constructing file io from scheme: {scheme} not supported now",), + )), + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use std::{fs::File, path::Path}; + + use futures::io::AllowStdIo; + use futures::{AsyncReadExt, AsyncWriteExt}; + + use tempfile::TempDir; + + use super::{FileIO, FileIOBuilder}; + + fn create_local_file_io() -> FileIO { + FileIOBuilder::new_fs_io().build().unwrap() + } + + fn write_to_file>(s: &str, path: P) { + let mut f = File::create(path).unwrap(); + write!(f, "{s}").unwrap(); + } + + async fn read_from_file>(path: P) -> String { + let mut f = AllowStdIo::new(File::open(path).unwrap()); + let mut s = String::new(); + f.read_to_string(&mut s).await.unwrap(); + s + } + + #[tokio::test] + async fn test_local_input_file() { + let tmp_dir = TempDir::new().unwrap(); + + let file_name = "a.txt"; + let content = "Iceberg loves rust."; + + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + write_to_file(content, &full_path); + + let file_io = create_local_file_io(); + let input_file = file_io.new_input(&full_path).unwrap(); + + assert!(input_file.exists().await.unwrap()); + // Remove heading slash + assert_eq!(&full_path, input_file.location()); + let read_content = read_from_file(full_path).await; + + assert_eq!(content, &read_content); + } + + #[tokio::test] + async fn test_delete_local_file() { + let tmp_dir = TempDir::new().unwrap(); + + let file_name = "a.txt"; + let content = "Iceberg loves rust."; + + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + write_to_file(content, &full_path); + + let file_io = create_local_file_io(); + assert!(file_io.is_exist(&full_path).await.unwrap()); + file_io.delete(&full_path).await.unwrap(); + assert!(!file_io.is_exist(&full_path).await.unwrap()); + } + + #[tokio::test] + async fn test_delete_non_exist_file() { + let tmp_dir = TempDir::new().unwrap(); + + let file_name = "a.txt"; + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + + let file_io = create_local_file_io(); + assert!(!file_io.is_exist(&full_path).await.unwrap()); + assert!(file_io.delete(&full_path).await.is_ok()); + } + + #[tokio::test] + async fn test_local_output_file() { + let tmp_dir = TempDir::new().unwrap(); + + let file_name = "a.txt"; + let content = "Iceberg loves rust."; + + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + + let file_io = create_local_file_io(); + let output_file = file_io.new_output(&full_path).unwrap(); + + assert!(!output_file.exists().await.unwrap()); + { + let mut writer = output_file.writer().await.unwrap(); + writer.write_all(content.as_bytes()).await.unwrap(); + writer.close().await.unwrap(); + } + + assert_eq!(&full_path, output_file.location()); + + let read_content = read_from_file(full_path).await; + + assert_eq!(content, &read_content); + } + + #[test] + fn test_create_file_from_path() { + let io = FileIO::from_path("/tmp/a").unwrap(); + assert_eq!("file", io.scheme_str.unwrap().as_str()); + + let io = FileIO::from_path("file:/tmp/b").unwrap(); + assert_eq!("file", io.scheme_str.unwrap().as_str()); + + let io = FileIO::from_path("file:///tmp/c").unwrap(); + assert_eq!("file", io.scheme_str.unwrap().as_str()); + + let io = FileIO::from_path("s3://bucket/a").unwrap(); + assert_eq!("s3", io.scheme_str.unwrap().as_str()); + + let io = FileIO::from_path("tmp/||c"); + assert!(io.is_err()); + } +} diff --git a/libs/iceberg/src/lib.rs b/libs/iceberg/src/lib.rs new file mode 100644 index 0000000..9ceadca --- /dev/null +++ b/libs/iceberg/src/lib.rs @@ -0,0 +1,55 @@ +// 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. + +//! Native Rust implementation of Apache Iceberg + +#![deny(missing_docs)] + +#[macro_use] +extern crate derive_builder; + +mod error; +pub use error::Error; +pub use error::ErrorKind; +pub use error::Result; + +mod catalog; + +pub use catalog::Catalog; +pub use catalog::Namespace; +pub use catalog::NamespaceIdent; +pub use catalog::TableCommit; +pub use catalog::TableCreation; +pub use catalog::TableIdent; +pub use catalog::TableRequirement; +pub use catalog::TableUpdate; + +#[allow(dead_code)] +pub mod table; + +mod avro; +pub mod io; +pub mod spec; + +mod scan; + +#[allow(dead_code)] +pub mod expr; +pub mod transaction; +pub mod transform; + +pub mod writer; diff --git a/libs/iceberg/src/scan.rs b/libs/iceberg/src/scan.rs new file mode 100644 index 0000000..0a3b9a9 --- /dev/null +++ b/libs/iceberg/src/scan.rs @@ -0,0 +1,448 @@ +// 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. + +//! Table scan api. + +use crate::io::FileIO; +use crate::spec::{DataContentType, ManifestEntryRef, SchemaRef, SnapshotRef, TableMetadataRef}; +use crate::table::Table; +use crate::{Error, ErrorKind}; +use arrow_array::RecordBatch; +use futures::stream::{iter, BoxStream}; +use futures::StreamExt; + +/// Builder to create table scan. +pub struct TableScanBuilder<'a> { + table: &'a Table, + // Empty column names means to select all columns + column_names: Vec, + snapshot_id: Option, +} + +impl<'a> TableScanBuilder<'a> { + pub fn new(table: &'a Table) -> Self { + Self { + table, + column_names: vec![], + snapshot_id: None, + } + } + + /// Select all columns. + pub fn select_all(mut self) -> Self { + self.column_names.clear(); + self + } + + /// Select some columns of the table. + pub fn select(mut self, column_names: impl IntoIterator) -> Self { + self.column_names = column_names + .into_iter() + .map(|item| item.to_string()) + .collect(); + self + } + + /// Set the snapshot to scan. When not set, it uses current snapshot. + pub fn snapshot_id(mut self, snapshot_id: i64) -> Self { + self.snapshot_id = Some(snapshot_id); + self + } + + /// Build the table scan. + pub fn build(self) -> crate::Result { + let snapshot = match self.snapshot_id { + Some(snapshot_id) => self + .table + .metadata() + .snapshot_by_id(snapshot_id) + .ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Snapshot with id {} not found", snapshot_id), + ) + })? + .clone(), + None => self + .table + .metadata() + .current_snapshot() + .ok_or_else(|| { + Error::new( + ErrorKind::FeatureUnsupported, + "Can't scan table without snapshots", + ) + })? + .clone(), + }; + + let schema = snapshot.schema(self.table.metadata())?; + + // Check that all column names exist in the schema. + if !self.column_names.is_empty() { + for column_name in &self.column_names { + if schema.field_by_name(column_name).is_none() { + return Err(Error::new( + ErrorKind::DataInvalid, + format!("Column {} not found in table.", column_name), + )); + } + } + } + + Ok(TableScan { + snapshot, + file_io: self.table.file_io().clone(), + table_metadata: self.table.metadata_ref(), + column_names: self.column_names, + schema, + }) + } +} + +/// Table scan. +#[derive(Debug)] +#[allow(dead_code)] +pub struct TableScan { + snapshot: SnapshotRef, + table_metadata: TableMetadataRef, + file_io: FileIO, + column_names: Vec, + schema: SchemaRef, +} + +/// A stream of [`FileScanTask`]. +pub type FileScanTaskStream = BoxStream<'static, crate::Result>; + +impl TableScan { + /// Returns a stream of file scan tasks. + pub async fn plan_files(&self) -> crate::Result { + let manifest_list = self + .snapshot + .load_manifest_list(&self.file_io, &self.table_metadata) + .await?; + + // Generate data file stream + let mut file_scan_tasks = Vec::with_capacity(manifest_list.entries().len()); + for manifest_list_entry in manifest_list.entries().iter() { + // Data file + let manifest = manifest_list_entry.load_manifest(&self.file_io).await?; + + for manifest_entry in manifest.entries().iter().filter(|e| e.is_alive()) { + match manifest_entry.content_type() { + DataContentType::EqualityDeletes | DataContentType::PositionDeletes => { + return Err(Error::new( + ErrorKind::FeatureUnsupported, + "Delete files are not supported yet.", + )); + } + DataContentType::Data => { + file_scan_tasks.push(Ok(FileScanTask { + data_file: manifest_entry.clone(), + start: 0, + length: manifest_entry.file_size_in_bytes(), + })); + } + } + } + } + + Ok(iter(file_scan_tasks).boxed()) + } +} + +/// A task to scan part of file. +#[derive(Debug)] +#[allow(dead_code)] +pub struct FileScanTask { + data_file: ManifestEntryRef, + start: u64, + length: u64, +} + +/// A stream of arrow record batches. +pub type ArrowRecordBatchStream = BoxStream<'static, crate::Result>; + +impl FileScanTask { + /// Returns a stream of arrow record batches. + pub async fn execute(&self) -> crate::Result { + todo!() + } +} + +#[cfg(test)] +mod tests { + use crate::io::{FileIO, OutputFile}; + use crate::spec::{ + DataContentType, DataFile, DataFileFormat, FormatVersion, Literal, Manifest, + ManifestContentType, ManifestEntry, ManifestListWriter, ManifestMetadata, ManifestStatus, + ManifestWriter, Struct, TableMetadata, EMPTY_SNAPSHOT_ID, + }; + use crate::table::Table; + use crate::TableIdent; + use futures::TryStreamExt; + use std::fs; + use tempfile::TempDir; + use tera::{Context, Tera}; + use uuid::Uuid; + + struct TableTestFixture { + table_location: String, + table: Table, + } + + impl TableTestFixture { + fn new() -> Self { + let tmp_dir = TempDir::new().unwrap(); + let table_location = tmp_dir.path().join("table1"); + let manifest_list1_location = table_location.join("metadata/manifests_list_1.avro"); + let manifest_list2_location = table_location.join("metadata/manifests_list_2.avro"); + let table_metadata1_location = table_location.join("metadata/v1.json"); + + let file_io = FileIO::from_path(table_location.as_os_str().to_str().unwrap()) + .unwrap() + .build() + .unwrap(); + + let table_metadata = { + let template_json_str = fs::read_to_string(format!( + "{}/testdata/example_table_metadata_v2.json", + env!("CARGO_MANIFEST_DIR") + )) + .unwrap(); + let mut context = Context::new(); + context.insert("table_location", &table_location); + context.insert("manifest_list_1_location", &manifest_list1_location); + context.insert("manifest_list_2_location", &manifest_list2_location); + context.insert("table_metadata_1_location", &table_metadata1_location); + + let metadata_json = Tera::one_off(&template_json_str, &context, false).unwrap(); + serde_json::from_str::(&metadata_json).unwrap() + }; + + let table = Table::builder() + .metadata(table_metadata) + .identifier(TableIdent::from_strs(["db", "table1"]).unwrap()) + .file_io(file_io) + .metadata_location(table_metadata1_location.as_os_str().to_str().unwrap()) + .build(); + + Self { + table_location: table_location.to_str().unwrap().to_string(), + table, + } + } + + fn next_manifest_file(&self) -> OutputFile { + self.table + .file_io() + .new_output(format!( + "{}/metadata/manifest_{}.avro", + self.table_location, + Uuid::new_v4() + )) + .unwrap() + } + } + + #[test] + fn test_table_scan_columns() { + let table = TableTestFixture::new().table; + + let table_scan = table.scan().select(["x", "y"]).build().unwrap(); + assert_eq!(vec!["x", "y"], table_scan.column_names); + + let table_scan = table + .scan() + .select(["x", "y"]) + .select(["z"]) + .build() + .unwrap(); + assert_eq!(vec!["z"], table_scan.column_names); + } + + #[test] + fn test_select_all() { + let table = TableTestFixture::new().table; + + let table_scan = table.scan().select_all().build().unwrap(); + assert!(table_scan.column_names.is_empty()); + } + + #[test] + fn test_select_no_exist_column() { + let table = TableTestFixture::new().table; + + let table_scan = table.scan().select(["x", "y", "z", "a"]).build(); + assert!(table_scan.is_err()); + } + + #[test] + fn test_table_scan_default_snapshot_id() { + let table = TableTestFixture::new().table; + + let table_scan = table.scan().build().unwrap(); + assert_eq!( + table.metadata().current_snapshot().unwrap().snapshot_id(), + table_scan.snapshot.snapshot_id() + ); + } + + #[test] + fn test_table_scan_non_exist_snapshot_id() { + let table = TableTestFixture::new().table; + + let table_scan = table.scan().snapshot_id(1024).build(); + assert!(table_scan.is_err()); + } + + #[test] + fn test_table_scan_with_snapshot_id() { + let table = TableTestFixture::new().table; + + let table_scan = table + .scan() + .snapshot_id(3051729675574597004) + .build() + .unwrap(); + assert_eq!(table_scan.snapshot.snapshot_id(), 3051729675574597004); + } + + #[tokio::test] + async fn test_plan_files_no_deletions() { + let fixture = TableTestFixture::new(); + + let current_snapshot = fixture.table.metadata().current_snapshot().unwrap(); + let parent_snapshot = current_snapshot + .parent_snapshot(fixture.table.metadata()) + .unwrap(); + let current_schema = current_snapshot.schema(fixture.table.metadata()).unwrap(); + let current_partition_spec = fixture.table.metadata().default_partition_spec().unwrap(); + + // Write data files + let data_file_manifest = ManifestWriter::new( + fixture.next_manifest_file(), + current_snapshot.snapshot_id(), + vec![], + ) + .write(Manifest::new( + ManifestMetadata::builder() + .schema((*current_schema).clone()) + .content(ManifestContentType::Data) + .format_version(FormatVersion::V2) + .partition_spec((**current_partition_spec).clone()) + .schema_id(current_schema.schema_id()) + .build(), + vec![ + ManifestEntry::builder() + .status(ManifestStatus::Added) + .data_file( + DataFile::builder() + .content(DataContentType::Data) + .file_path(format!("{}/1.parquet", &fixture.table_location)) + .file_format(DataFileFormat::Parquet) + .file_size_in_bytes(100) + .record_count(1) + .partition(Struct::from_iter([Some(Literal::long(100))])) + .build(), + ) + .build(), + ManifestEntry::builder() + .status(ManifestStatus::Deleted) + .snapshot_id(parent_snapshot.snapshot_id()) + .sequence_number(parent_snapshot.sequence_number()) + .file_sequence_number(parent_snapshot.sequence_number()) + .data_file( + DataFile::builder() + .content(DataContentType::Data) + .file_path(format!("{}/2.parquet", &fixture.table_location)) + .file_format(DataFileFormat::Parquet) + .file_size_in_bytes(100) + .record_count(1) + .partition(Struct::from_iter([Some(Literal::long(200))])) + .build(), + ) + .build(), + ManifestEntry::builder() + .status(ManifestStatus::Existing) + .snapshot_id(parent_snapshot.snapshot_id()) + .sequence_number(parent_snapshot.sequence_number()) + .file_sequence_number(parent_snapshot.sequence_number()) + .data_file( + DataFile::builder() + .content(DataContentType::Data) + .file_path(format!("{}/3.parquet", &fixture.table_location)) + .file_format(DataFileFormat::Parquet) + .file_size_in_bytes(100) + .record_count(1) + .partition(Struct::from_iter([Some(Literal::long(300))])) + .build(), + ) + .build(), + ], + )) + .await + .unwrap(); + + // Write to manifest list + let mut manifest_list_write = ManifestListWriter::v2( + fixture + .table + .file_io() + .new_output(current_snapshot.manifest_list()) + .unwrap(), + current_snapshot.snapshot_id(), + current_snapshot + .parent_snapshot_id() + .unwrap_or(EMPTY_SNAPSHOT_ID), + current_snapshot.sequence_number(), + ); + manifest_list_write + .add_manifest_entries(vec![data_file_manifest].into_iter()) + .unwrap(); + manifest_list_write.close().await.unwrap(); + + // Create table scan for current snapshot and plan files + let table_scan = fixture.table.scan().build().unwrap(); + let mut tasks = table_scan + .plan_files() + .await + .unwrap() + .try_fold(vec![], |mut acc, task| async move { + acc.push(task); + Ok(acc) + }) + .await + .unwrap(); + + assert_eq!(tasks.len(), 2); + + tasks.sort_by_key(|t| t.data_file.file_path().to_string()); + + // Check first task is added data file + assert_eq!( + tasks[0].data_file.file_path(), + format!("{}/1.parquet", &fixture.table_location) + ); + + // Check second task is existing data file + assert_eq!( + tasks[1].data_file.file_path(), + format!("{}/3.parquet", &fixture.table_location) + ); + } +} diff --git a/libs/iceberg/src/spec/datatypes.rs b/libs/iceberg/src/spec/datatypes.rs new file mode 100644 index 0000000..172cb64 --- /dev/null +++ b/libs/iceberg/src/spec/datatypes.rs @@ -0,0 +1,1056 @@ +// 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. + +/*! + * Data Types +*/ +use crate::ensure_data_valid; +use crate::error::Result; +use crate::spec::datatypes::_decimal::{MAX_PRECISION, REQUIRED_LENGTH}; +use ::serde::de::{MapAccess, Visitor}; +use serde::de::{Error, IntoDeserializer}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value as JsonValue; +use std::convert::identity; +use std::sync::Arc; +use std::sync::OnceLock; +use std::{collections::HashMap, fmt, ops::Index}; + +use super::values::Literal; + +/// Field name for list type. +pub(crate) const LIST_FILED_NAME: &str = "element"; +pub(crate) const MAP_KEY_FIELD_NAME: &str = "key"; +pub(crate) const MAP_VALUE_FIELD_NAME: &str = "value"; + +pub(crate) const MAX_DECIMAL_BYTES: u32 = 24; +pub(crate) const MAX_DECIMAL_PRECISION: u32 = 38; + +mod _decimal { + use lazy_static::lazy_static; + + use crate::spec::{MAX_DECIMAL_BYTES, MAX_DECIMAL_PRECISION}; + + lazy_static! { + // Max precision of bytes, starts from 1 + pub(super) static ref MAX_PRECISION: [u32; MAX_DECIMAL_BYTES as usize] = { + let mut ret: [u32; 24] = [0; 24]; + for (i, prec) in ret.iter_mut().enumerate() { + *prec = 2f64.powi((8 * (i + 1) - 1) as i32).log10().floor() as u32; + } + + ret + }; + + // Required bytes of precision, starts from 1 + pub(super) static ref REQUIRED_LENGTH: [u32; MAX_DECIMAL_PRECISION as usize] = { + let mut ret: [u32; MAX_DECIMAL_PRECISION as usize] = [0; MAX_DECIMAL_PRECISION as usize]; + + for (i, required_len) in ret.iter_mut().enumerate() { + for j in 0..MAX_PRECISION.len() { + if MAX_PRECISION[j] >= ((i+1) as u32) { + *required_len = (j+1) as u32; + break; + } + } + } + + ret + }; + + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// All data types are either primitives or nested types, which are maps, lists, or structs. +pub enum Type { + /// Primitive types + Primitive(PrimitiveType), + /// Struct type + Struct(StructType), + /// List type. + List(ListType), + /// Map type + Map(MapType), +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Type::Primitive(primitive) => write!(f, "{}", primitive), + Type::Struct(s) => write!(f, "{}", s), + Type::List(_) => write!(f, "list"), + Type::Map(_) => write!(f, "map"), + } + } +} + +impl Type { + /// Whether the type is primitive type. + #[inline(always)] + pub fn is_primitive(&self) -> bool { + matches!(self, Type::Primitive(_)) + } + + /// Whether the type is struct type. + #[inline(always)] + pub fn is_struct(&self) -> bool { + matches!(self, Type::Struct(_)) + } + + /// Return max precision for decimal given [`num_bytes`] bytes. + #[inline(always)] + pub fn decimal_max_precision(num_bytes: u32) -> Result { + ensure_data_valid!( + num_bytes > 0 && num_bytes <= MAX_DECIMAL_BYTES, + "Decimal length larger than {MAX_DECIMAL_BYTES} is not supported: {num_bytes}", + ); + Ok(MAX_PRECISION[num_bytes as usize - 1]) + } + + /// Returns minimum bytes required for decimal with [`precision`]. + #[inline(always)] + pub fn decimal_required_bytes(precision: u32) -> Result { + ensure_data_valid!(precision > 0 && precision <= MAX_DECIMAL_PRECISION, "Decimals with precision larger than {MAX_DECIMAL_PRECISION} are not supported: {precision}",); + Ok(REQUIRED_LENGTH[precision as usize - 1]) + } + + /// Creates decimal type. + #[inline(always)] + pub fn decimal(precision: u32, scale: u32) -> Result { + ensure_data_valid!(precision > 0 && precision <= MAX_DECIMAL_PRECISION, "Decimals with precision larger than {MAX_DECIMAL_PRECISION} are not supported: {precision}",); + Ok(Type::Primitive(PrimitiveType::Decimal { precision, scale })) + } +} + +impl From for Type { + fn from(value: PrimitiveType) -> Self { + Self::Primitive(value) + } +} + +impl From for Type { + fn from(value: StructType) -> Self { + Type::Struct(value) + } +} + +impl From for Type { + fn from(value: ListType) -> Self { + Type::List(value) + } +} + +impl From for Type { + fn from(value: MapType) -> Self { + Type::Map(value) + } +} + +/// Primitive data types +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase", remote = "Self")] +pub enum PrimitiveType { + /// True or False + Boolean, + /// 32-bit signed integer + Int, + /// 64-bit signed integer + Long, + /// 32-bit IEEE 754 floating bit. + Float, + /// 64-bit IEEE 754 floating bit. + Double, + /// Fixed point decimal + Decimal { + /// Precision + precision: u32, + /// Scale + scale: u32, + }, + /// Calendar date without timezone or time. + Date, + /// Time of day without date or timezone. + Time, + /// Timestamp without timezone + Timestamp, + /// Timestamp with timezone + Timestamptz, + /// Arbitrary-length character sequences encoded in utf-8 + String, + /// Universally Unique Identifiers + Uuid, + /// Fixed length byte array + Fixed(u64), + /// Arbitrary-length byte array. + Binary, +} + +impl Serialize for Type { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let type_serde = _serde::SerdeType::from(self); + type_serde.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Type { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let type_serde = _serde::SerdeType::deserialize(deserializer)?; + Ok(Type::from(type_serde)) + } +} + +impl<'de> Deserialize<'de> for PrimitiveType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.starts_with("decimal") { + deserialize_decimal(s.into_deserializer()) + } else if s.starts_with("fixed") { + deserialize_fixed(s.into_deserializer()) + } else { + PrimitiveType::deserialize(s.into_deserializer()) + } + } +} + +impl Serialize for PrimitiveType { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + PrimitiveType::Decimal { precision, scale } => { + serialize_decimal(precision, scale, serializer) + } + PrimitiveType::Fixed(l) => serialize_fixed(l, serializer), + _ => PrimitiveType::serialize(self, serializer), + } + } +} + +fn deserialize_decimal<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let (precision, scale) = s + .trim_start_matches(r"decimal(") + .trim_end_matches(')') + .split_once(',') + .ok_or_else(|| D::Error::custom("Decimal requires precision and scale: {s}"))?; + + Ok(PrimitiveType::Decimal { + precision: precision.trim().parse().map_err(D::Error::custom)?, + scale: scale.trim().parse().map_err(D::Error::custom)?, + }) +} + +fn serialize_decimal( + precision: &u32, + scale: &u32, + serializer: S, +) -> std::result::Result +where + S: Serializer, +{ + serializer.serialize_str(&format!("decimal({precision},{scale})")) +} + +fn deserialize_fixed<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let fixed = String::deserialize(deserializer)? + .trim_start_matches(r"fixed[") + .trim_end_matches(']') + .to_owned(); + + fixed + .parse() + .map(PrimitiveType::Fixed) + .map_err(D::Error::custom) +} + +fn serialize_fixed(value: &u64, serializer: S) -> std::result::Result +where + S: Serializer, +{ + serializer.serialize_str(&format!("fixed[{value}]")) +} + +impl fmt::Display for PrimitiveType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PrimitiveType::Boolean => write!(f, "boolean"), + PrimitiveType::Int => write!(f, "int"), + PrimitiveType::Long => write!(f, "long"), + PrimitiveType::Float => write!(f, "float"), + PrimitiveType::Double => write!(f, "double"), + PrimitiveType::Decimal { precision, scale } => { + write!(f, "decimal({},{})", precision, scale) + } + PrimitiveType::Date => write!(f, "date"), + PrimitiveType::Time => write!(f, "time"), + PrimitiveType::Timestamp => write!(f, "timestamp"), + PrimitiveType::Timestamptz => write!(f, "timestamptz"), + PrimitiveType::String => write!(f, "string"), + PrimitiveType::Uuid => write!(f, "uuid"), + PrimitiveType::Fixed(size) => write!(f, "fixed({})", size), + PrimitiveType::Binary => write!(f, "binary"), + } + } +} + +/// DataType for a specific struct +#[derive(Debug, Serialize, Clone)] +#[serde(rename = "struct", tag = "type")] +pub struct StructType { + /// Struct fields + fields: Vec, + /// Lookup for index by field id + #[serde(skip_serializing)] + id_lookup: OnceLock>, + #[serde(skip_serializing)] + name_lookup: OnceLock>, +} + +impl<'de> Deserialize<'de> for StructType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + Type, + Fields, + } + + struct StructTypeVisitor; + + impl<'de> Visitor<'de> for StructTypeVisitor { + type Value = StructType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct") + } + + fn visit_map(self, mut map: V) -> std::result::Result + where + V: MapAccess<'de>, + { + let mut fields = None; + while let Some(key) = map.next_key()? { + match key { + Field::Type => (), + Field::Fields => { + if fields.is_some() { + return Err(serde::de::Error::duplicate_field("fields")); + } + fields = Some(map.next_value()?); + } + } + } + let fields: Vec = + fields.ok_or_else(|| de::Error::missing_field("fields"))?; + + Ok(StructType::new(fields)) + } + } + + const FIELDS: &[&str] = &["type", "fields"]; + deserializer.deserialize_struct("struct", FIELDS, StructTypeVisitor) + } +} + +impl StructType { + /// Creates a struct type with the given fields. + pub fn new(fields: Vec) -> Self { + Self { + fields, + id_lookup: OnceLock::new(), + name_lookup: OnceLock::new(), + } + } + + /// Get struct field with certain id + pub fn field_by_id(&self, id: i32) -> Option<&NestedFieldRef> { + self.field_id_to_index(id).map(|idx| &self.fields[idx]) + } + + fn field_id_to_index(&self, field_id: i32) -> Option { + self.id_lookup + .get_or_init(|| { + HashMap::from_iter(self.fields.iter().enumerate().map(|(i, x)| (x.id, i))) + }) + .get(&field_id) + .copied() + } + + /// Get struct field with certain field name + pub fn field_by_name(&self, name: &str) -> Option<&NestedFieldRef> { + self.field_name_to_index(name).map(|idx| &self.fields[idx]) + } + + fn field_name_to_index(&self, name: &str) -> Option { + self.name_lookup + .get_or_init(|| { + HashMap::from_iter( + self.fields + .iter() + .enumerate() + .map(|(i, x)| (x.name.clone(), i)), + ) + }) + .get(name) + .copied() + } + + /// Get fields. + pub fn fields(&self) -> &[NestedFieldRef] { + &self.fields + } +} + +impl PartialEq for StructType { + fn eq(&self, other: &Self) -> bool { + self.fields == other.fields + } +} + +impl Eq for StructType {} + +impl Index for StructType { + type Output = NestedField; + + fn index(&self, index: usize) -> &Self::Output { + &self.fields[index] + } +} + +impl fmt::Display for StructType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "struct<")?; + for field in &self.fields { + write!(f, "{}", field.field_type)?; + } + write!(f, ">") + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)] +#[serde(from = "SerdeNestedField", into = "SerdeNestedField")] +/// A struct is a tuple of typed values. Each field in the tuple is named and has an integer id that is unique in the table schema. +/// Each field can be either optional or required, meaning that values can (or cannot) be null. Fields may be any type. +/// Fields may have an optional comment or doc string. Fields can have default values. +pub struct NestedField { + /// Id unique in table schema + pub id: i32, + /// Field Name + pub name: String, + /// Optional or required + pub required: bool, + /// Datatype + pub field_type: Box, + /// Fields may have an optional comment or doc string. + pub doc: Option, + /// Used to populate the field’s value for all records that were written before the field was added to the schema + pub initial_default: Option, + /// Used to populate the field’s value for any records written after the field was added to the schema, if the writer does not supply the field’s value + pub write_default: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "kebab-case")] +struct SerdeNestedField { + pub id: i32, + pub name: String, + pub required: bool, + #[serde(rename = "type")] + pub field_type: Box, + #[serde(skip_serializing_if = "Option::is_none")] + pub doc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_default: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub write_default: Option, +} + +impl From for NestedField { + fn from(value: SerdeNestedField) -> Self { + NestedField { + id: value.id, + name: value.name, + required: value.required, + initial_default: value.initial_default.and_then(|x| { + Literal::try_from_json(x, &value.field_type) + .ok() + .and_then(identity) + }), + write_default: value.write_default.and_then(|x| { + Literal::try_from_json(x, &value.field_type) + .ok() + .and_then(identity) + }), + field_type: value.field_type, + doc: value.doc, + } + } +} + +impl From for SerdeNestedField { + fn from(value: NestedField) -> Self { + let initial_default = value.initial_default.map(|x| x.try_into_json(&value.field_type).expect("We should have checked this in NestedField::with_initial_default, it can't be converted to json value")); + let write_default = value.write_default.map(|x| x.try_into_json(&value.field_type).expect("We should have checked this in NestedField::with_write_default, it can't be converted to json value")); + SerdeNestedField { + id: value.id, + name: value.name, + required: value.required, + field_type: value.field_type, + doc: value.doc, + initial_default, + write_default, + } + } +} + +/// Reference to nested field. +pub type NestedFieldRef = Arc; + +impl NestedField { + /// Construct a required field. + pub fn required(id: i32, name: impl ToString, field_type: Type) -> Self { + Self { + id, + name: name.to_string(), + required: true, + field_type: Box::new(field_type), + doc: None, + initial_default: None, + write_default: None, + } + } + + /// Construct an optional field. + pub fn optional(id: i32, name: impl ToString, field_type: Type) -> Self { + Self { + id, + name: name.to_string(), + required: false, + field_type: Box::new(field_type), + doc: None, + initial_default: None, + write_default: None, + } + } + + /// Construct list type's element field. + pub fn list_element(id: i32, field_type: Type, required: bool) -> Self { + if required { + Self::required(id, LIST_FILED_NAME, field_type) + } else { + Self::optional(id, LIST_FILED_NAME, field_type) + } + } + + /// Construct map type's key field. + pub fn map_key_element(id: i32, field_type: Type) -> Self { + Self::required(id, MAP_KEY_FIELD_NAME, field_type) + } + + /// Construct map type's value field. + pub fn map_value_element(id: i32, field_type: Type, required: bool) -> Self { + if required { + Self::required(id, MAP_VALUE_FIELD_NAME, field_type) + } else { + Self::optional(id, MAP_VALUE_FIELD_NAME, field_type) + } + } + + /// Set the field's doc. + pub fn with_doc(mut self, doc: impl ToString) -> Self { + self.doc = Some(doc.to_string()); + self + } + + /// Set the field's initial default value. + pub fn with_initial_default(mut self, value: Literal) -> Self { + self.initial_default = Some(value); + self + } + + /// Set the field's initial default value. + pub fn with_write_default(mut self, value: Literal) -> Self { + self.write_default = Some(value); + self + } +} + +impl fmt::Display for NestedField { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: ", self.id)?; + write!(f, "{}: ", self.name)?; + if self.required { + write!(f, "required ")?; + } else { + write!(f, "optional ")?; + } + write!(f, "{} ", self.field_type)?; + if let Some(doc) = &self.doc { + write!(f, "{}", doc)?; + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A list is a collection of values with some element type. The element field has an integer id that is unique in the table schema. +/// Elements can be either optional or required. Element types may be any type. +pub struct ListType { + /// Element field of list type. + pub element_field: NestedFieldRef, +} + +/// Module for type serialization/deserialization. +pub(super) mod _serde { + use crate::spec::datatypes::Type::Map; + use crate::spec::datatypes::{ + ListType, MapType, NestedField, NestedFieldRef, PrimitiveType, StructType, Type, + }; + use serde_derive::{Deserialize, Serialize}; + use std::borrow::Cow; + + /// List type for serialization and deserialization + #[derive(Serialize, Deserialize)] + #[serde(untagged)] + pub(super) enum SerdeType<'a> { + #[serde(rename_all = "kebab-case")] + List { + r#type: String, + element_id: i32, + element_required: bool, + element: Cow<'a, Type>, + }, + Struct { + r#type: String, + fields: Cow<'a, Vec>, + }, + #[serde(rename_all = "kebab-case")] + Map { + r#type: String, + key_id: i32, + key: Cow<'a, Type>, + value_id: i32, + value_required: bool, + value: Cow<'a, Type>, + }, + Primitive(PrimitiveType), + } + + impl<'a> From> for Type { + fn from(value: SerdeType) -> Self { + match value { + SerdeType::List { + r#type: _, + element_id, + element_required, + element, + } => Self::List(ListType { + element_field: NestedField::list_element( + element_id, + element.into_owned(), + element_required, + ) + .into(), + }), + SerdeType::Map { + r#type: _, + key_id, + key, + value_id, + value_required, + value, + } => Map(MapType { + key_field: NestedField::map_key_element(key_id, key.into_owned()).into(), + value_field: NestedField::map_value_element( + value_id, + value.into_owned(), + value_required, + ) + .into(), + }), + SerdeType::Struct { r#type: _, fields } => { + Self::Struct(StructType::new(fields.into_owned())) + } + SerdeType::Primitive(p) => Self::Primitive(p), + } + } + } + + impl<'a> From<&'a Type> for SerdeType<'a> { + fn from(value: &'a Type) -> Self { + match value { + Type::List(list) => SerdeType::List { + r#type: "list".to_string(), + element_id: list.element_field.id, + element_required: list.element_field.required, + element: Cow::Borrowed(&list.element_field.field_type), + }, + Type::Map(map) => SerdeType::Map { + r#type: "map".to_string(), + key_id: map.key_field.id, + key: Cow::Borrowed(&map.key_field.field_type), + value_id: map.value_field.id, + value_required: map.value_field.required, + value: Cow::Borrowed(&map.value_field.field_type), + }, + Type::Struct(s) => SerdeType::Struct { + r#type: "struct".to_string(), + fields: Cow::Borrowed(&s.fields), + }, + Type::Primitive(p) => SerdeType::Primitive(p.clone()), + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A map is a collection of key-value pairs with a key type and a value type. +/// Both the key field and value field each have an integer id that is unique in the table schema. +/// Map keys are required and map values can be either optional or required. +/// Both map keys and map values may be any type, including nested types. +pub struct MapType { + /// Field for key. + pub key_field: NestedFieldRef, + /// Field for value. + pub value_field: NestedFieldRef, +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use uuid::Uuid; + + use crate::spec::values::PrimitiveLiteral; + + use super::*; + + fn check_type_serde(json: &str, expected_type: Type) { + let desered_type: Type = serde_json::from_str(json).unwrap(); + assert_eq!(desered_type, expected_type); + + let sered_json = serde_json::to_string(&expected_type).unwrap(); + let parsed_json_value = serde_json::from_str::(&sered_json).unwrap(); + let raw_json_value = serde_json::from_str::(json).unwrap(); + + assert_eq!(parsed_json_value, raw_json_value); + } + + #[test] + fn decimal() { + let record = r#" + { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "id", + "required": true, + "type": "decimal(9,2)" + } + ] + } + "#; + + check_type_serde( + record, + Type::Struct(StructType { + fields: vec![NestedField::required( + 1, + "id", + Type::Primitive(PrimitiveType::Decimal { + precision: 9, + scale: 2, + }), + ) + .into()], + id_lookup: OnceLock::default(), + name_lookup: OnceLock::default(), + }), + ) + } + + #[test] + fn fixed() { + let record = r#" + { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "id", + "required": true, + "type": "fixed[8]" + } + ] + } + "#; + + check_type_serde( + record, + Type::Struct(StructType { + fields: vec![NestedField::required( + 1, + "id", + Type::Primitive(PrimitiveType::Fixed(8)), + ) + .into()], + id_lookup: OnceLock::default(), + name_lookup: OnceLock::default(), + }), + ) + } + + #[test] + fn struct_type() { + let record = r#" + { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "id", + "required": true, + "type": "uuid", + "initial-default": "0db3e2a8-9d1d-42b9-aa7b-74ebe558dceb", + "write-default": "ec5911be-b0a7-458c-8438-c9a3e53cffae" + }, { + "id": 2, + "name": "data", + "required": false, + "type": "int" + } + ] + } + "#; + + check_type_serde( + record, + Type::Struct(StructType { + fields: vec![ + NestedField::required(1, "id", Type::Primitive(PrimitiveType::Uuid)) + .with_initial_default(Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::parse_str("0db3e2a8-9d1d-42b9-aa7b-74ebe558dceb").unwrap(), + ))) + .with_write_default(Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::parse_str("ec5911be-b0a7-458c-8438-c9a3e53cffae").unwrap(), + ))) + .into(), + NestedField::optional(2, "data", Type::Primitive(PrimitiveType::Int)).into(), + ], + id_lookup: HashMap::from([(1, 0), (2, 1)]).into(), + name_lookup: HashMap::from([("id".to_string(), 0), ("data".to_string(), 1)]).into(), + }), + ) + } + + #[test] + fn test_deeply_nested_struct() { + let record = r#" +{ + "type": "struct", + "fields": [ + { + "id": 1, + "name": "id", + "required": true, + "type": "uuid", + "initial-default": "0db3e2a8-9d1d-42b9-aa7b-74ebe558dceb", + "write-default": "ec5911be-b0a7-458c-8438-c9a3e53cffae" + }, + { + "id": 2, + "name": "data", + "required": false, + "type": "int" + }, + { + "id": 3, + "name": "address", + "required": true, + "type": { + "type": "struct", + "fields": [ + { + "id": 4, + "name": "street", + "required": true, + "type": "string" + }, + { + "id": 5, + "name": "province", + "required": false, + "type": "string" + }, + { + "id": 6, + "name": "zip", + "required": true, + "type": "int" + } + ] + } + } + ] +} +"#; + + let struct_type = Type::Struct(StructType::new(vec![ + NestedField::required(1, "id", Type::Primitive(PrimitiveType::Uuid)) + .with_initial_default(Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::parse_str("0db3e2a8-9d1d-42b9-aa7b-74ebe558dceb").unwrap(), + ))) + .with_write_default(Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::parse_str("ec5911be-b0a7-458c-8438-c9a3e53cffae").unwrap(), + ))) + .into(), + NestedField::optional(2, "data", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::required( + 3, + "address", + Type::Struct(StructType::new(vec![ + NestedField::required(4, "street", Type::Primitive(PrimitiveType::String)) + .into(), + NestedField::optional(5, "province", Type::Primitive(PrimitiveType::String)) + .into(), + NestedField::required(6, "zip", Type::Primitive(PrimitiveType::Int)).into(), + ])), + ) + .into(), + ])); + + check_type_serde(record, struct_type) + } + + #[test] + fn list() { + let record = r#" + { + "type": "list", + "element-id": 3, + "element-required": true, + "element": "string" + } + "#; + + check_type_serde( + record, + Type::List(ListType { + element_field: NestedField::list_element( + 3, + Type::Primitive(PrimitiveType::String), + true, + ) + .into(), + }), + ); + } + + #[test] + fn map() { + let record = r#" + { + "type": "map", + "key-id": 4, + "key": "string", + "value-id": 5, + "value-required": false, + "value": "double" + } + "#; + + check_type_serde( + record, + Type::Map(MapType { + key_field: NestedField::map_key_element(4, Type::Primitive(PrimitiveType::String)) + .into(), + value_field: NestedField::map_value_element( + 5, + Type::Primitive(PrimitiveType::Double), + false, + ) + .into(), + }), + ); + } + + #[test] + fn map_int() { + let record = r#" + { + "type": "map", + "key-id": 4, + "key": "int", + "value-id": 5, + "value-required": false, + "value": "string" + } + "#; + + check_type_serde( + record, + Type::Map(MapType { + key_field: NestedField::map_key_element(4, Type::Primitive(PrimitiveType::Int)) + .into(), + value_field: NestedField::map_value_element( + 5, + Type::Primitive(PrimitiveType::String), + false, + ) + .into(), + }), + ); + } + + #[test] + fn test_decimal_precision() { + let expected_max_precision = [ + 2, 4, 6, 9, 11, 14, 16, 18, 21, 23, 26, 28, 31, 33, 35, 38, 40, 43, 45, 47, 50, 52, 55, + 57, + ]; + for (i, max_precision) in expected_max_precision.iter().enumerate() { + assert_eq!( + *max_precision, + Type::decimal_max_precision(i as u32 + 1).unwrap(), + "Failed calculate max precision for {i}" + ); + } + + assert_eq!(5, Type::decimal_required_bytes(10).unwrap()); + assert_eq!(16, Type::decimal_required_bytes(38).unwrap()); + } +} diff --git a/libs/iceberg/src/spec/manifest.rs b/libs/iceberg/src/spec/manifest.rs new file mode 100644 index 0000000..e3c989f --- /dev/null +++ b/libs/iceberg/src/spec/manifest.rs @@ -0,0 +1,2007 @@ +// 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. + +//! Manifest for Iceberg. +use self::_const_schema::{manifest_schema_v1, manifest_schema_v2}; + +use super::{ + FieldSummary, FormatVersion, ManifestContentType, ManifestListEntry, PartitionSpec, Schema, + SchemaId, Struct, INITIAL_SEQUENCE_NUMBER, +}; +use super::{Literal, UNASSIGNED_SEQUENCE_NUMBER}; +use crate::error::Result; +use crate::io::OutputFile; +use crate::spec::PartitionField; +use crate::{Error, ErrorKind}; +use apache_avro::{from_value, to_value, Reader as AvroReader, Writer as AvroWriter}; +use futures::AsyncWriteExt; +use serde_json::to_vec; +use std::cmp::min; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use typed_builder::TypedBuilder; + +/// A manifest contains metadata and a list of entries. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Manifest { + metadata: ManifestMetadata, + entries: Vec, +} + +impl Manifest { + /// Parse manifest metadata and entries from bytes of avro file. + pub(crate) fn try_from_avro_bytes(bs: &[u8]) -> Result<(ManifestMetadata, Vec)> { + let reader = AvroReader::new(bs)?; + + // Parse manifest metadata + let meta = reader.user_metadata(); + let metadata = ManifestMetadata::parse(meta)?; + + // Parse manifest entries + let partition_type = metadata.partition_spec.partition_type(&metadata.schema)?; + + let entries = match metadata.format_version { + FormatVersion::V1 => { + let schema = manifest_schema_v1(partition_type.clone())?; + let reader = AvroReader::with_schema(&schema, bs)?; + reader + .into_iter() + .map(|value| { + from_value::<_serde::ManifestEntryV1>(&value?)? + .try_into(&partition_type, &metadata.schema) + }) + .collect::>>()? + } + FormatVersion::V2 => { + let schema = manifest_schema_v2(partition_type.clone())?; + let reader = AvroReader::with_schema(&schema, bs)?; + reader + .into_iter() + .map(|value| { + from_value::<_serde::ManifestEntryV2>(&value?)? + .try_into(&partition_type, &metadata.schema) + }) + .collect::>>()? + } + }; + + Ok((metadata, entries)) + } + + /// Parse manifest from bytes of avro file. + pub fn parse_avro(bs: &[u8]) -> Result { + let (metadata, entries) = Self::try_from_avro_bytes(bs)?; + Ok(Self::new(metadata, entries)) + } + + /// Entries slice. + pub fn entries(&self) -> &[ManifestEntryRef] { + &self.entries + } + + /// Constructor from [`ManifestMetadata`] and [`ManifestEntry`]s. + pub fn new(metadata: ManifestMetadata, entries: Vec) -> Self { + Self { + metadata, + entries: entries.into_iter().map(Arc::new).collect(), + } + } +} + +/// A manifest writer. +pub struct ManifestWriter { + output: OutputFile, + + snapshot_id: i64, + + added_files: u32, + added_rows: u64, + existing_files: u32, + existing_rows: u64, + deleted_files: u32, + deleted_rows: u64, + + min_seq_num: Option, + + key_metadata: Vec, + + field_summary: HashMap, +} + +impl ManifestWriter { + /// Create a new manifest writer. + pub fn new(output: OutputFile, snapshot_id: i64, key_metadata: Vec) -> Self { + Self { + output, + snapshot_id, + added_files: 0, + added_rows: 0, + existing_files: 0, + existing_rows: 0, + deleted_files: 0, + deleted_rows: 0, + min_seq_num: None, + key_metadata, + field_summary: HashMap::new(), + } + } + + fn update_field_summary(&mut self, entry: &ManifestEntry) { + // Update field summary + for (&k, &v) in &entry.data_file.null_value_counts { + let field_summary = self.field_summary.entry(k).or_default(); + if v > 0 { + field_summary.contains_null = true; + } + } + + for (&k, &v) in &entry.data_file.nan_value_counts { + let field_summary = self.field_summary.entry(k).or_default(); + if v > 0 { + field_summary.contains_nan = Some(true); + } + if v == 0 { + field_summary.contains_nan = Some(false); + } + } + + for (&k, v) in &entry.data_file.lower_bounds { + let field_summary = self.field_summary.entry(k).or_default(); + if let Some(cur) = &field_summary.lower_bound { + if v < cur { + field_summary.lower_bound = Some(v.clone()); + } + } else { + field_summary.lower_bound = Some(v.clone()); + } + } + + for (&k, v) in &entry.data_file.upper_bounds { + let field_summary = self.field_summary.entry(k).or_default(); + if let Some(cur) = &field_summary.upper_bound { + if v > cur { + field_summary.upper_bound = Some(v.clone()); + } + } else { + field_summary.upper_bound = Some(v.clone()); + } + } + } + + fn get_field_summary_vec(&mut self, spec_fields: &[PartitionField]) -> Vec { + let mut partition_summary = Vec::with_capacity(self.field_summary.len()); + for field in spec_fields { + let entry = self + .field_summary + .remove(&field.source_id) + .unwrap_or_default(); + partition_summary.push(entry); + } + partition_summary + } + + /// Write a manifest entry. + pub async fn write(mut self, manifest: Manifest) -> Result { + // Create the avro writer + let partition_type = manifest + .metadata + .partition_spec + .partition_type(&manifest.metadata.schema)?; + let table_schema = &manifest.metadata.schema; + let avro_schema = match manifest.metadata.format_version { + FormatVersion::V1 => manifest_schema_v1(partition_type.clone())?, + FormatVersion::V2 => manifest_schema_v2(partition_type.clone())?, + }; + let mut avro_writer = AvroWriter::new(&avro_schema, Vec::new()); + avro_writer.add_user_metadata( + "schema".to_string(), + to_vec(table_schema).map_err(|err| { + Error::new(ErrorKind::DataInvalid, "Fail to serialize table schema") + .with_source(err) + })?, + )?; + avro_writer.add_user_metadata( + "schema-id".to_string(), + table_schema.schema_id().to_string(), + )?; + avro_writer.add_user_metadata( + "partition-spec".to_string(), + to_vec(&manifest.metadata.partition_spec.fields).map_err(|err| { + Error::new(ErrorKind::DataInvalid, "Fail to serialize partition spec") + .with_source(err) + })?, + )?; + avro_writer.add_user_metadata( + "partition-spec-id".to_string(), + manifest.metadata.partition_spec.spec_id.to_string(), + )?; + avro_writer.add_user_metadata( + "format-version".to_string(), + (manifest.metadata.format_version as u8).to_string(), + )?; + if manifest.metadata.format_version == FormatVersion::V2 { + avro_writer + .add_user_metadata("content".to_string(), manifest.metadata.content.to_string())?; + } + + // Write manifest entries + for entry in manifest.entries { + if (entry.status == ManifestStatus::Deleted || entry.status == ManifestStatus::Existing) + && (entry.sequence_number.is_none() || entry.file_sequence_number.is_none()) + { + return Err(Error::new( + ErrorKind::DataInvalid, + "Manifest entry with status Existing or Deleted should have sequence number", + )); + } + + match entry.status { + ManifestStatus::Added => { + self.added_files += 1; + self.added_rows += entry.data_file.record_count; + } + ManifestStatus::Deleted => { + self.deleted_files += 1; + self.deleted_rows += entry.data_file.record_count; + } + ManifestStatus::Existing => { + self.existing_files += 1; + self.existing_rows += entry.data_file.record_count; + } + } + + if entry.is_alive() { + if let Some(seq_num) = entry.sequence_number { + self.min_seq_num = Some(self.min_seq_num.map_or(seq_num, |v| min(v, seq_num))); + } + } + + self.update_field_summary(&entry); + + let value = match manifest.metadata.format_version { + FormatVersion::V1 => to_value(_serde::ManifestEntryV1::try_from( + (*entry).clone(), + &partition_type, + )?)? + .resolve(&avro_schema)?, + FormatVersion::V2 => to_value(_serde::ManifestEntryV2::try_from( + (*entry).clone(), + &partition_type, + )?)? + .resolve(&avro_schema)?, + }; + + avro_writer.append(value)?; + } + + let length = avro_writer.flush()?; + let content = avro_writer.into_inner()?; + let mut writer = self.output.writer().await?; + writer.write_all(&content).await.map_err(|err| { + Error::new(ErrorKind::Unexpected, "Fail to write Manifest Entry").with_source(err) + })?; + writer.close().await.map_err(|err| { + Error::new(ErrorKind::Unexpected, "Fail to write Manifest Entry").with_source(err) + })?; + + let partition_summary = + self.get_field_summary_vec(&manifest.metadata.partition_spec.fields); + + Ok(ManifestListEntry { + manifest_path: self.output.location().to_string(), + manifest_length: length as i64, + partition_spec_id: manifest.metadata.partition_spec.spec_id, + content: manifest.metadata.content, + // sequence_number and min_sequence_number with UNASSIGNED_SEQUENCE_NUMBER will be replace with + // real sequence number in `ManifestListWriter`. + sequence_number: UNASSIGNED_SEQUENCE_NUMBER, + min_sequence_number: self.min_seq_num.unwrap_or(UNASSIGNED_SEQUENCE_NUMBER), + added_snapshot_id: self.snapshot_id, + added_data_files_count: Some(self.added_files), + existing_data_files_count: Some(self.existing_files), + deleted_data_files_count: Some(self.deleted_files), + added_rows_count: Some(self.added_rows), + existing_rows_count: Some(self.existing_rows), + deleted_rows_count: Some(self.deleted_rows), + partitions: partition_summary, + key_metadata: self.key_metadata, + }) + } +} + +/// This is a helper module that defines the schema field of the manifest list entry. +mod _const_schema { + use std::sync::Arc; + + use apache_avro::Schema as AvroSchema; + use once_cell::sync::Lazy; + + use crate::{ + avro::schema_to_avro_schema, + spec::{ + ListType, MapType, NestedField, NestedFieldRef, PrimitiveType, Schema, StructType, Type, + }, + Error, + }; + + static STATUS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 0, + "status", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + + static SNAPSHOT_ID_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 1, + "snapshot_id", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static SNAPSHOT_ID_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 1, + "snapshot_id", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static SEQUENCE_NUMBER: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 3, + "sequence_number", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static FILE_SEQUENCE_NUMBER: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 4, + "file_sequence_number", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static CONTENT: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 134, + "content", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + + static FILE_PATH: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 100, + "file_path", + Type::Primitive(PrimitiveType::String), + )) + }) + }; + + static FILE_FORMAT: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 101, + "file_format", + Type::Primitive(PrimitiveType::String), + )) + }) + }; + + static RECORD_COUNT: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 103, + "record_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static FILE_SIZE_IN_BYTES: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 104, + "file_size_in_bytes", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + // Deprecated. Always write a default in v1. Do not write in v2. + static BLOCK_SIZE_IN_BYTES: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 105, + "block_size_in_bytes", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + + static COLUMN_SIZES: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 108, + "column_sizes", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 117, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 118, + "value", + Type::Primitive(PrimitiveType::Long), + )), + }), + )) + }) + }; + + static VALUE_COUNTS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 109, + "value_counts", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 119, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 120, + "value", + Type::Primitive(PrimitiveType::Long), + )), + }), + )) + }) + }; + + static NULL_VALUE_COUNTS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 110, + "null_value_counts", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 121, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 122, + "value", + Type::Primitive(PrimitiveType::Long), + )), + }), + )) + }) + }; + + static NAN_VALUE_COUNTS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 137, + "nan_value_counts", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 138, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 139, + "value", + Type::Primitive(PrimitiveType::Long), + )), + }), + )) + }) + }; + + static LOWER_BOUNDS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 125, + "lower_bounds", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 126, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 127, + "value", + Type::Primitive(PrimitiveType::Binary), + )), + }), + )) + }) + }; + + static UPPER_BOUNDS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 128, + "upper_bounds", + Type::Map(MapType { + key_field: Arc::new(NestedField::required( + 129, + "key", + Type::Primitive(PrimitiveType::Int), + )), + value_field: Arc::new(NestedField::required( + 130, + "value", + Type::Primitive(PrimitiveType::Binary), + )), + }), + )) + }) + }; + + static KEY_METADATA: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 131, + "key_metadata", + Type::Primitive(PrimitiveType::Binary), + )) + }) + }; + + static SPLIT_OFFSETS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 132, + "split_offsets", + Type::List(ListType { + element_field: Arc::new(NestedField::required( + 133, + "element", + Type::Primitive(PrimitiveType::Long), + )), + }), + )) + }) + }; + + static EQUALITY_IDS: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 135, + "equality_ids", + Type::List(ListType { + element_field: Arc::new(NestedField::required( + 136, + "element", + Type::Primitive(PrimitiveType::Int), + )), + }), + )) + }) + }; + + static SORT_ORDER_ID: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 140, + "sort_order_id", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + + pub(super) fn manifest_schema_v2(partition_type: StructType) -> Result { + let fields = vec![ + STATUS.clone(), + SNAPSHOT_ID_V2.clone(), + SEQUENCE_NUMBER.clone(), + FILE_SEQUENCE_NUMBER.clone(), + Arc::new(NestedField::required( + 2, + "data_file", + Type::Struct(StructType::new(vec![ + CONTENT.clone(), + FILE_PATH.clone(), + FILE_FORMAT.clone(), + Arc::new(NestedField::required( + 102, + "partition", + Type::Struct(partition_type), + )), + RECORD_COUNT.clone(), + FILE_SIZE_IN_BYTES.clone(), + COLUMN_SIZES.clone(), + VALUE_COUNTS.clone(), + NULL_VALUE_COUNTS.clone(), + NAN_VALUE_COUNTS.clone(), + LOWER_BOUNDS.clone(), + UPPER_BOUNDS.clone(), + KEY_METADATA.clone(), + SPLIT_OFFSETS.clone(), + EQUALITY_IDS.clone(), + SORT_ORDER_ID.clone(), + ])), + )), + ]; + let schema = Schema::builder().with_fields(fields).build()?; + schema_to_avro_schema("manifest_entry", &schema) + } + + pub(super) fn manifest_schema_v1(partition_type: StructType) -> Result { + let fields = vec![ + STATUS.clone(), + SNAPSHOT_ID_V1.clone(), + Arc::new(NestedField::required( + 2, + "data_file", + Type::Struct(StructType::new(vec![ + FILE_PATH.clone(), + FILE_FORMAT.clone(), + Arc::new(NestedField::required( + 102, + "partition", + Type::Struct(partition_type), + )), + RECORD_COUNT.clone(), + FILE_SIZE_IN_BYTES.clone(), + BLOCK_SIZE_IN_BYTES.clone(), + COLUMN_SIZES.clone(), + VALUE_COUNTS.clone(), + NULL_VALUE_COUNTS.clone(), + NAN_VALUE_COUNTS.clone(), + LOWER_BOUNDS.clone(), + UPPER_BOUNDS.clone(), + KEY_METADATA.clone(), + SPLIT_OFFSETS.clone(), + SORT_ORDER_ID.clone(), + ])), + )), + ]; + let schema = Schema::builder().with_fields(fields).build()?; + schema_to_avro_schema("manifest_entry", &schema) + } +} + +/// Meta data of a manifest that is stored in the key-value metadata of the Avro file +#[derive(Debug, PartialEq, Clone, Eq, TypedBuilder)] +pub struct ManifestMetadata { + /// The table schema at the time the manifest + /// was written + schema: Schema, + /// ID of the schema used to write the manifest as a string + schema_id: SchemaId, + /// The partition spec used to write the manifest + partition_spec: PartitionSpec, + /// Table format version number of the manifest as a string + format_version: FormatVersion, + /// Type of content files tracked by the manifest: “data” or “deletes” + content: ManifestContentType, +} + +impl ManifestMetadata { + /// Parse from metadata in avro file. + pub fn parse(meta: &HashMap>) -> Result { + let schema = { + let bs = meta.get("schema").ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + "schema is required in manifest metadata but not found", + ) + })?; + serde_json::from_slice::(bs).map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + "Fail to parse schema in manifest metadata", + ) + .with_source(err) + })? + }; + let schema_id: i32 = meta + .get("schema-id") + .map(|bs| { + String::from_utf8_lossy(bs).parse().map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + "Fail to parse schema id in manifest metadata", + ) + .with_source(err) + }) + }) + .transpose()? + .unwrap_or(0); + let partition_spec = { + let fields = { + let bs = meta.get("partition-spec").ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + "partition-spec is required in manifest metadata but not found", + ) + })?; + serde_json::from_slice::>(bs).map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + "Fail to parse partition spec in manifest metadata", + ) + .with_source(err) + })? + }; + let spec_id = meta + .get("partition-spec-id") + .map(|bs| { + String::from_utf8_lossy(bs).parse().map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + "Fail to parse partition spec id in manifest metadata", + ) + .with_source(err) + }) + }) + .transpose()? + .unwrap_or(0); + PartitionSpec { spec_id, fields } + }; + let format_version = if let Some(bs) = meta.get("format-version") { + serde_json::from_slice::(bs).map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + "Fail to parse format version in manifest metadata", + ) + .with_source(err) + })? + } else { + FormatVersion::V1 + }; + let content = if let Some(v) = meta.get("content") { + let v = String::from_utf8_lossy(v); + v.parse()? + } else { + ManifestContentType::Data + }; + Ok(ManifestMetadata { + schema, + schema_id, + partition_spec, + format_version, + content, + }) + } +} + +/// Reference to [`ManifestEntry`]. +pub type ManifestEntryRef = Arc; + +/// A manifest is an immutable Avro file that lists data files or delete +/// files, along with each file’s partition data tuple, metrics, and tracking +/// information. +#[derive(Debug, PartialEq, Eq, Clone, TypedBuilder)] +pub struct ManifestEntry { + /// field: 0 + /// + /// Used to track additions and deletions. + status: ManifestStatus, + /// field id: 1 + /// + /// Snapshot id where the file was added, or deleted if status is 2. + /// Inherited when null. + #[builder(default, setter(strip_option))] + snapshot_id: Option, + /// field id: 3 + /// + /// Data sequence number of the file. + /// Inherited when null and status is 1 (added). + #[builder(default, setter(strip_option))] + sequence_number: Option, + /// field id: 4 + /// + /// File sequence number indicating when the file was added. + /// Inherited when null and status is 1 (added). + #[builder(default, setter(strip_option))] + file_sequence_number: Option, + /// field id: 2 + /// + /// File path, partition tuple, metrics, … + data_file: DataFile, +} + +impl ManifestEntry { + /// Check if this manifest entry is deleted. + pub fn is_alive(&self) -> bool { + matches!( + self.status, + ManifestStatus::Added | ManifestStatus::Existing + ) + } + + /// Content type of this manifest entry. + pub fn content_type(&self) -> DataContentType { + self.data_file.content + } + + /// Data file path of this manifest entry. + pub fn file_path(&self) -> &str { + &self.data_file.file_path + } + + /// Inherit data from manifest list, such as snapshot id, sequence number. + pub(crate) fn inherit_data(&mut self, snapshot_entry: &ManifestListEntry) { + if self.snapshot_id.is_none() { + self.snapshot_id = Some(snapshot_entry.added_snapshot_id); + } + + if self.sequence_number.is_none() + && (self.status == ManifestStatus::Added + || snapshot_entry.sequence_number == INITIAL_SEQUENCE_NUMBER) + { + self.sequence_number = Some(snapshot_entry.sequence_number); + } + + if self.file_sequence_number.is_none() + && (self.status == ManifestStatus::Added + || snapshot_entry.sequence_number == INITIAL_SEQUENCE_NUMBER) + { + self.file_sequence_number = Some(snapshot_entry.sequence_number); + } + } + + /// Data sequence number. + #[inline] + pub fn sequence_number(&self) -> Option { + self.sequence_number + } + + /// File size in bytes. + #[inline] + pub fn file_size_in_bytes(&self) -> u64 { + self.data_file.file_size_in_bytes + } +} + +/// Used to track additions and deletions in ManifestEntry. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ManifestStatus { + /// Value: 0 + Existing = 0, + /// Value: 1 + Added = 1, + /// Value: 2 + /// + /// Deletes are informational only and not used in scans. + Deleted = 2, +} + +impl TryFrom for ManifestStatus { + type Error = Error; + + fn try_from(v: i32) -> Result { + match v { + 0 => Ok(ManifestStatus::Existing), + 1 => Ok(ManifestStatus::Added), + 2 => Ok(ManifestStatus::Deleted), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("manifest status {} is invalid", v), + )), + } + } +} + +/// Data file carries data file path, partition tuple, metrics, … +#[derive(Debug, PartialEq, Clone, Eq, TypedBuilder)] +pub struct DataFile { + /// field id: 134 + /// + /// Type of content stored by the data file: data, equality deletes, + /// or position deletes (all v1 files are data files) + content: DataContentType, + /// field id: 100 + /// + /// Full URI for the file with FS scheme + file_path: String, + /// field id: 101 + /// + /// String file format name, avro, orc or parquet + file_format: DataFileFormat, + /// field id: 102 + /// + /// Partition data tuple, schema based on the partition spec output using + /// partition field ids for the struct field ids + partition: Struct, + /// field id: 103 + /// + /// Number of records in this file + record_count: u64, + /// field id: 104 + /// + /// Total file size in bytes + file_size_in_bytes: u64, + /// field id: 108 + /// key field id: 117 + /// value field id: 118 + /// + /// Map from column id to the total size on disk of all regions that + /// store the column. Does not include bytes necessary to read other + /// columns, like footers. Leave null for row-oriented formats (Avro) + #[builder(default)] + column_sizes: HashMap, + /// field id: 109 + /// key field id: 119 + /// value field id: 120 + /// + /// Map from column id to number of values in the column (including null + /// and NaN values) + #[builder(default)] + value_counts: HashMap, + /// field id: 110 + /// key field id: 121 + /// value field id: 122 + /// + /// Map from column id to number of null values in the column + #[builder(default)] + null_value_counts: HashMap, + /// field id: 137 + /// key field id: 138 + /// value field id: 139 + /// + /// Map from column id to number of NaN values in the column + #[builder(default)] + nan_value_counts: HashMap, + /// field id: 125 + /// key field id: 126 + /// value field id: 127 + /// + /// Map from column id to lower bound in the column serialized as binary. + /// Each value must be less than or equal to all non-null, non-NaN values + /// in the column for the file. + /// + /// Reference: + /// + /// - [Binary single-value serialization](https://iceberg.apache.org/spec/#binary-single-value-serialization) + #[builder(default)] + lower_bounds: HashMap, + /// field id: 128 + /// key field id: 129 + /// value field id: 130 + /// + /// Map from column id to upper bound in the column serialized as binary. + /// Each value must be greater than or equal to all non-null, non-Nan + /// values in the column for the file. + /// + /// Reference: + /// + /// - [Binary single-value serialization](https://iceberg.apache.org/spec/#binary-single-value-serialization) + #[builder(default)] + upper_bounds: HashMap, + /// field id: 131 + /// + /// Implementation-specific key metadata for encryption + #[builder(default)] + key_metadata: Vec, + /// field id: 132 + /// element field id: 133 + /// + /// Split offsets for the data file. For example, all row group offsets + /// in a Parquet file. Must be sorted ascending + #[builder(default)] + split_offsets: Vec, + /// field id: 135 + /// element field id: 136 + /// + /// Field ids used to determine row equality in equality delete files. + /// Required when content is EqualityDeletes and should be null + /// otherwise. Fields with ids listed in this column must be present + /// in the delete file + #[builder(default)] + equality_ids: Vec, + /// field id: 140 + /// + /// ID representing sort order for this file. + /// + /// If sort order ID is missing or unknown, then the order is assumed to + /// be unsorted. Only data files and equality delete files should be + /// written with a non-null order id. Position deletes are required to be + /// sorted by file and position, not a table order, and should set sort + /// order id to null. Readers must ignore sort order id for position + /// delete files. + #[builder(default, setter(strip_option))] + sort_order_id: Option, +} + +/// Type of content stored by the data file: data, equality deletes, or +/// position deletes (all v1 files are data files) +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DataContentType { + /// value: 0 + Data = 0, + /// value: 1 + PositionDeletes = 1, + /// value: 2 + EqualityDeletes = 2, +} + +impl TryFrom for DataContentType { + type Error = Error; + + fn try_from(v: i32) -> Result { + match v { + 0 => Ok(DataContentType::Data), + 1 => Ok(DataContentType::PositionDeletes), + 2 => Ok(DataContentType::EqualityDeletes), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("data content type {} is invalid", v), + )), + } + } +} + +/// Format of this data. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DataFileFormat { + /// Avro file format: + Avro, + /// Orc file format: + Orc, + /// Parquet file format: + Parquet, +} + +impl FromStr for DataFileFormat { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "avro" => Ok(Self::Avro), + "orc" => Ok(Self::Orc), + "parquet" => Ok(Self::Parquet), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("Unsupported data file format: {}", s), + )), + } + } +} + +impl ToString for DataFileFormat { + fn to_string(&self) -> String { + match self { + DataFileFormat::Avro => "avro", + DataFileFormat::Orc => "orc", + DataFileFormat::Parquet => "parquet", + } + .to_string() + } +} + +mod _serde { + use std::collections::HashMap; + + use serde_bytes::ByteBuf; + use serde_derive::{Deserialize, Serialize}; + use serde_with::serde_as; + + use crate::spec::Literal; + use crate::spec::RawLiteral; + use crate::spec::Schema; + use crate::spec::Struct; + use crate::spec::StructType; + use crate::spec::Type; + use crate::Error; + use crate::ErrorKind; + + use super::ManifestEntry; + + #[derive(Serialize, Deserialize)] + pub(super) struct ManifestEntryV2 { + status: i32, + snapshot_id: Option, + sequence_number: Option, + file_sequence_number: Option, + data_file: DataFile, + } + + impl ManifestEntryV2 { + pub fn try_from(value: ManifestEntry, partition_type: &StructType) -> Result { + Ok(Self { + status: value.status as i32, + snapshot_id: value.snapshot_id, + sequence_number: value.sequence_number, + file_sequence_number: value.file_sequence_number, + data_file: DataFile::try_from(value.data_file, partition_type, false)?, + }) + } + + pub fn try_into( + self, + partition_type: &StructType, + schema: &Schema, + ) -> Result { + Ok(ManifestEntry { + status: self.status.try_into()?, + snapshot_id: self.snapshot_id, + sequence_number: self.sequence_number, + file_sequence_number: self.file_sequence_number, + data_file: self.data_file.try_into(partition_type, schema)?, + }) + } + } + + #[derive(Serialize, Deserialize)] + pub(super) struct ManifestEntryV1 { + status: i32, + pub snapshot_id: i64, + data_file: DataFile, + } + + impl ManifestEntryV1 { + pub fn try_from(value: ManifestEntry, partition_type: &StructType) -> Result { + Ok(Self { + status: value.status as i32, + snapshot_id: value.snapshot_id.unwrap_or_default(), + data_file: DataFile::try_from(value.data_file, partition_type, true)?, + }) + } + + pub fn try_into( + self, + partition_type: &StructType, + schema: &Schema, + ) -> Result { + Ok(ManifestEntry { + status: self.status.try_into()?, + snapshot_id: Some(self.snapshot_id), + sequence_number: Some(0), + file_sequence_number: Some(0), + data_file: self.data_file.try_into(partition_type, schema)?, + }) + } + } + + #[serde_as] + #[derive(Serialize, Deserialize)] + pub(super) struct DataFile { + #[serde(default)] + content: i32, + file_path: String, + file_format: String, + partition: RawLiteral, + record_count: i64, + file_size_in_bytes: i64, + #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] + block_size_in_bytes: Option, + column_sizes: Option>, + value_counts: Option>, + null_value_counts: Option>, + nan_value_counts: Option>, + lower_bounds: Option>, + upper_bounds: Option>, + key_metadata: Option, + split_offsets: Option>, + #[serde(default)] + equality_ids: Option>, + sort_order_id: Option, + } + + impl DataFile { + pub fn try_from( + value: super::DataFile, + partition_type: &StructType, + is_version_1: bool, + ) -> Result { + let block_size_in_bytes = if is_version_1 { Some(0) } else { None }; + Ok(Self { + content: value.content as i32, + file_path: value.file_path, + file_format: value.file_format.to_string(), + partition: RawLiteral::try_from( + Literal::Struct(value.partition), + &Type::Struct(partition_type.clone()), + )?, + record_count: value.record_count.try_into()?, + file_size_in_bytes: value.file_size_in_bytes.try_into()?, + block_size_in_bytes, + column_sizes: Some(to_i64_entry(value.column_sizes)?), + value_counts: Some(to_i64_entry(value.value_counts)?), + null_value_counts: Some(to_i64_entry(value.null_value_counts)?), + nan_value_counts: Some(to_i64_entry(value.nan_value_counts)?), + lower_bounds: Some(to_bytes_entry(value.lower_bounds)), + upper_bounds: Some(to_bytes_entry(value.upper_bounds)), + key_metadata: Some(serde_bytes::ByteBuf::from(value.key_metadata)), + split_offsets: Some(value.split_offsets), + equality_ids: Some(value.equality_ids), + sort_order_id: value.sort_order_id, + }) + } + pub fn try_into( + self, + partition_type: &StructType, + schema: &Schema, + ) -> Result { + let partition = self + .partition + .try_into(&Type::Struct(partition_type.clone()))? + .map(|v| { + if let Literal::Struct(v) = v { + Ok(v) + } else { + Err(Error::new( + ErrorKind::DataInvalid, + "partition value is not a struct", + )) + } + }) + .transpose()? + .unwrap_or(Struct::empty()); + Ok(super::DataFile { + content: self.content.try_into()?, + file_path: self.file_path, + file_format: self.file_format.parse()?, + partition, + record_count: self.record_count.try_into()?, + file_size_in_bytes: self.file_size_in_bytes.try_into()?, + column_sizes: self + .column_sizes + .map(parse_i64_entry) + .transpose()? + .unwrap_or_default(), + value_counts: self + .value_counts + .map(parse_i64_entry) + .transpose()? + .unwrap_or_default(), + null_value_counts: self + .null_value_counts + .map(parse_i64_entry) + .transpose()? + .unwrap_or_default(), + nan_value_counts: self + .nan_value_counts + .map(parse_i64_entry) + .transpose()? + .unwrap_or_default(), + lower_bounds: self + .lower_bounds + .map(|v| parse_bytes_entry(v, schema)) + .transpose()? + .unwrap_or_default(), + upper_bounds: self + .upper_bounds + .map(|v| parse_bytes_entry(v, schema)) + .transpose()? + .unwrap_or_default(), + key_metadata: self.key_metadata.map(|v| v.to_vec()).unwrap_or_default(), + split_offsets: self.split_offsets.unwrap_or_default(), + equality_ids: self.equality_ids.unwrap_or_default(), + sort_order_id: self.sort_order_id, + }) + } + } + + #[serde_as] + #[derive(Serialize, Deserialize)] + #[cfg_attr(test, derive(Debug, PartialEq, Eq))] + struct BytesEntry { + key: i32, + value: serde_bytes::ByteBuf, + } + + fn parse_bytes_entry( + v: Vec, + schema: &Schema, + ) -> Result, Error> { + let mut m = HashMap::with_capacity(v.len()); + for entry in v { + // We ignore the entry if the field is not found in the schema, due to schema evolution. + if let Some(field) = schema.field_by_id(entry.key) { + let data_type = &field.field_type; + m.insert(entry.key, Literal::try_from_bytes(&entry.value, data_type)?); + } + } + Ok(m) + } + + fn to_bytes_entry(v: HashMap) -> Vec { + v.into_iter() + .map(|e| BytesEntry { + key: e.0, + value: Into::::into(e.1), + }) + .collect() + } + + #[derive(Serialize, Deserialize)] + #[cfg_attr(test, derive(Debug, PartialEq, Eq))] + struct I64Entry { + key: i32, + value: i64, + } + + fn parse_i64_entry(v: Vec) -> Result, Error> { + let mut m = HashMap::with_capacity(v.len()); + for entry in v { + // We ignore the entry if it's value is negative since these entries are supposed to be used for + // counting, which should never be negative. + if let Ok(v) = entry.value.try_into() { + m.insert(entry.key, v); + } + } + Ok(m) + } + + fn to_i64_entry(entries: HashMap) -> Result, Error> { + entries + .iter() + .map(|e| { + Ok(I64Entry { + key: *e.0, + value: (*e.1).try_into()?, + }) + }) + .collect() + } + + #[cfg(test)] + mod tests { + use crate::spec::manifest::_serde::{parse_i64_entry, I64Entry}; + use std::collections::HashMap; + + #[test] + fn test_parse_negative_manifest_entry() { + let entries = vec![ + I64Entry { key: 1, value: -1 }, + I64Entry { key: 2, value: 3 }, + ]; + + let ret = parse_i64_entry(entries).unwrap(); + + let expected_ret = HashMap::from([(2, 3)]); + assert_eq!(ret, expected_ret, "Negative i64 entry should be ignored!"); + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::TempDir; + + use super::*; + use crate::io::FileIOBuilder; + use crate::spec::NestedField; + use crate::spec::PrimitiveType; + use crate::spec::Struct; + use crate::spec::Transform; + use crate::spec::Type; + use std::sync::Arc; + + #[tokio::test] + async fn test_parse_manifest_v2_unpartition() { + let manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 0, + schema: Schema::builder() + .with_fields(vec![ + // id v_int v_long v_float v_double v_varchar v_bool v_date v_timestamp v_decimal v_ts_ntz + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "v_int", + Type::Primitive(PrimitiveType::Int), + )), + Arc::new(NestedField::optional( + 3, + "v_long", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 4, + "v_float", + Type::Primitive(PrimitiveType::Float), + )), + Arc::new(NestedField::optional( + 5, + "v_double", + Type::Primitive(PrimitiveType::Double), + )), + Arc::new(NestedField::optional( + 6, + "v_varchar", + Type::Primitive(PrimitiveType::String), + )), + Arc::new(NestedField::optional( + 7, + "v_bool", + Type::Primitive(PrimitiveType::Boolean), + )), + Arc::new(NestedField::optional( + 8, + "v_date", + Type::Primitive(PrimitiveType::Date), + )), + Arc::new(NestedField::optional( + 9, + "v_timestamp", + Type::Primitive(PrimitiveType::Timestamptz), + )), + Arc::new(NestedField::optional( + 10, + "v_decimal", + Type::Primitive(PrimitiveType::Decimal { + precision: 36, + scale: 10, + }), + )), + Arc::new(NestedField::optional( + 11, + "v_ts_ntz", + Type::Primitive(PrimitiveType::Timestamp), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V2, + }, + entries: vec![ + Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: None, + sequence_number: None, + file_sequence_number: None, + data_file: DataFile { + content: DataContentType::Data, + file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-ba56fbfa-f2ff-40c9-bb27-565ad6dc2be8-00000.parquet".to_string(), + file_format: DataFileFormat::Parquet, + partition: Struct::empty(), + record_count: 1, + file_size_in_bytes: 5442, + column_sizes: HashMap::from([(0,73),(6,34),(2,73),(7,61),(3,61),(5,62),(9,79),(10,73),(1,61),(4,73),(8,73)]), + value_counts: HashMap::from([(4,1),(5,1),(2,1),(0,1),(3,1),(6,1),(8,1),(1,1),(10,1),(7,1),(9,1)]), + null_value_counts: HashMap::from([(1,0),(6,0),(2,0),(8,0),(0,0),(3,0),(5,0),(9,0),(7,0),(4,0),(10,0)]), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::new(), + upper_bounds: HashMap::new(), + key_metadata: Vec::new(), + split_offsets: vec![4], + equality_ids: Vec::new(), + sort_order_id: None, + } + }) + ] + }; + + let writer = |output_file: OutputFile| ManifestWriter::new(output_file, 1, vec![]); + + test_manifest_read_write(manifest, writer).await; + } + + #[tokio::test] + async fn test_parse_manifest_v2_partition() { + let manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 0, + schema: Schema::builder() + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "v_int", + Type::Primitive(PrimitiveType::Int), + )), + Arc::new(NestedField::optional( + 3, + "v_long", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 4, + "v_float", + Type::Primitive(PrimitiveType::Float), + )), + Arc::new(NestedField::optional( + 5, + "v_double", + Type::Primitive(PrimitiveType::Double), + )), + Arc::new(NestedField::optional( + 6, + "v_varchar", + Type::Primitive(PrimitiveType::String), + )), + Arc::new(NestedField::optional( + 7, + "v_bool", + Type::Primitive(PrimitiveType::Boolean), + )), + Arc::new(NestedField::optional( + 8, + "v_date", + Type::Primitive(PrimitiveType::Date), + )), + Arc::new(NestedField::optional( + 9, + "v_timestamp", + Type::Primitive(PrimitiveType::Timestamptz), + )), + Arc::new(NestedField::optional( + 10, + "v_decimal", + Type::Primitive(PrimitiveType::Decimal { + precision: 36, + scale: 10, + }), + )), + Arc::new(NestedField::optional( + 11, + "v_ts_ntz", + Type::Primitive(PrimitiveType::Timestamp), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![ + PartitionField { + name: "v_int".to_string(), + transform: Transform::Identity, + source_id: 2, + field_id: 1000, + }, + PartitionField { + name: "v_long".to_string(), + transform: Transform::Identity, + source_id: 3, + field_id: 1001, + }, + ], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V2, + }, + entries: vec![Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: None, + sequence_number: None, + file_sequence_number: None, + data_file: DataFile { + content: DataContentType::Data, + file_format: DataFileFormat::Parquet, + file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(), + partition: Struct::from_iter( + vec![ + Some(Literal::int(1)), + Some(Literal::long(1000)), + ] + .into_iter() + ), + record_count: 1, + file_size_in_bytes: 5442, + column_sizes: HashMap::from([ + (0, 73), + (6, 34), + (2, 73), + (7, 61), + (3, 61), + (5, 62), + (9, 79), + (10, 73), + (1, 61), + (4, 73), + (8, 73) + ]), + value_counts: HashMap::from([ + (4, 1), + (5, 1), + (2, 1), + (0, 1), + (3, 1), + (6, 1), + (8, 1), + (1, 1), + (10, 1), + (7, 1), + (9, 1) + ]), + null_value_counts: HashMap::from([ + (1, 0), + (6, 0), + (2, 0), + (8, 0), + (0, 0), + (3, 0), + (5, 0), + (9, 0), + (7, 0), + (4, 0), + (10, 0) + ]), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::new(), + upper_bounds: HashMap::new(), + key_metadata: vec![], + split_offsets: vec![4], + equality_ids: vec![], + sort_order_id: None, + }, + })], + }; + + let writer = |output_file: OutputFile| ManifestWriter::new(output_file, 1, vec![]); + + let res = test_manifest_read_write(manifest, writer).await; + + assert_eq!(res.sequence_number, UNASSIGNED_SEQUENCE_NUMBER); + assert_eq!(res.min_sequence_number, UNASSIGNED_SEQUENCE_NUMBER); + } + + #[tokio::test] + async fn test_parse_manifest_v1_unpartition() { + let manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 1, + schema: Schema::builder() + .with_schema_id(1) + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Int), + )), + Arc::new(NestedField::optional( + 2, + "data", + Type::Primitive(PrimitiveType::String), + )), + Arc::new(NestedField::optional( + 3, + "comment", + Type::Primitive(PrimitiveType::String), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V1, + }, + entries: vec![Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: Some(0), + sequence_number: Some(0), + file_sequence_number: Some(0), + data_file: DataFile { + content: DataContentType::Data, + file_path: "s3://testbucket/iceberg_data/iceberg_ctl/iceberg_db/iceberg_tbl/data/00000-7-45268d71-54eb-476c-b42c-942d880c04a1-00001.parquet".to_string(), + file_format: DataFileFormat::Parquet, + partition: Struct::empty(), + record_count: 1, + file_size_in_bytes: 875, + column_sizes: HashMap::from([(1,47),(2,48),(3,52)]), + value_counts: HashMap::from([(1,1),(2,1),(3,1)]), + null_value_counts: HashMap::from([(1,0),(2,0),(3,0)]), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::from([(1,Literal::int(1)),(2,Literal::string("a")),(3,Literal::string("AC/DC"))]), + upper_bounds: HashMap::from([(1,Literal::int(1)),(2,Literal::string("a")),(3,Literal::string("AC/DC"))]), + key_metadata: vec![], + split_offsets: vec![4], + equality_ids: vec![], + sort_order_id: Some(0), + } + })], + }; + + let writer = + |output_file: OutputFile| ManifestWriter::new(output_file, 2966623707104393227, vec![]); + + test_manifest_read_write(manifest, writer).await; + } + + #[tokio::test] + async fn test_parse_manifest_v1_partition() { + let manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 0, + schema: Schema::builder() + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "data", + Type::Primitive(PrimitiveType::String), + )), + Arc::new(NestedField::optional( + 3, + "category", + Type::Primitive(PrimitiveType::String), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![PartitionField { + name: "category".to_string(), + transform: Transform::Identity, + source_id: 3, + field_id: 1000, + }], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V1, + }, + entries: vec![ + Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: Some(0), + sequence_number: Some(0), + file_sequence_number: Some(0), + data_file: DataFile { + content: DataContentType::Data, + file_path: "s3://testbucket/prod/db/sample/data/category=x/00010-1-d5c93668-1e52-41ac-92a6-bba590cbf249-00001.parquet".to_string(), + file_format: DataFileFormat::Parquet, + partition: Struct::from_iter( + vec![ + Some( + Literal::try_from_bytes(&[120], &Type::Primitive(PrimitiveType::String)) + .unwrap() + ), + ] + .into_iter() + ), + record_count: 1, + file_size_in_bytes: 874, + column_sizes: HashMap::from([(1, 46), (2, 48), (3, 48)]), + value_counts: HashMap::from([(1, 1), (2, 1), (3, 1)]), + null_value_counts: HashMap::from([(1, 0), (2, 0), (3, 0)]), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::string("a")), + (3, Literal::string("x")) + ]), + upper_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::string("a")), + (3, Literal::string("x")) + ]), + key_metadata: vec![], + split_offsets: vec![4], + equality_ids: vec![], + sort_order_id: Some(0), + }, + }) + ] + }; + + let writer = |output_file: OutputFile| ManifestWriter::new(output_file, 1, vec![]); + + let entry = test_manifest_read_write(manifest, writer).await; + + assert_eq!(entry.partitions.len(), 1); + assert_eq!(entry.partitions[0].lower_bound, Some(Literal::string("x"))); + assert_eq!(entry.partitions[0].upper_bound, Some(Literal::string("x"))); + } + + #[tokio::test] + async fn test_parse_manifest_with_schema_evolution() { + let manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 0, + schema: Schema::builder() + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "v_int", + Type::Primitive(PrimitiveType::Int), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V2, + }, + entries: vec![Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: None, + sequence_number: None, + file_sequence_number: None, + data_file: DataFile { + content: DataContentType::Data, + file_format: DataFileFormat::Parquet, + file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(), + partition: Struct::empty(), + record_count: 1, + file_size_in_bytes: 5442, + column_sizes: HashMap::from([ + (1, 61), + (2, 73), + (3, 61), + ]), + value_counts: HashMap::default(), + null_value_counts: HashMap::default(), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::int(2)), + (3, Literal::string("x")) + ]), + upper_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::int(2)), + (3, Literal::string("x")) + ]), + key_metadata: vec![], + split_offsets: vec![4], + equality_ids: vec![], + sort_order_id: None, + }, + })], + }; + + let writer = |output_file: OutputFile| ManifestWriter::new(output_file, 1, vec![]); + + let (avro_bytes, _) = write_manifest(&manifest, writer).await; + + // The parse should succeed. + let actual_manifest = Manifest::parse_avro(avro_bytes.as_slice()).unwrap(); + + // Compared with original manifest, the lower_bounds and upper_bounds no longer has data for field 3, and + // other parts should be same. + let expected_manifest = Manifest { + metadata: ManifestMetadata { + schema_id: 0, + schema: Schema::builder() + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "v_int", + Type::Primitive(PrimitiveType::Int), + )), + ]) + .build() + .unwrap(), + partition_spec: PartitionSpec { + spec_id: 0, + fields: vec![], + }, + content: ManifestContentType::Data, + format_version: FormatVersion::V2, + }, + entries: vec![Arc::new(ManifestEntry { + status: ManifestStatus::Added, + snapshot_id: None, + sequence_number: None, + file_sequence_number: None, + data_file: DataFile { + content: DataContentType::Data, + file_format: DataFileFormat::Parquet, + file_path: "s3a://icebergdata/demo/s1/t1/data/00000-0-378b56f5-5c52-4102-a2c2-f05f8a7cbe4a-00000.parquet".to_string(), + partition: Struct::empty(), + record_count: 1, + file_size_in_bytes: 5442, + column_sizes: HashMap::from([ + (1, 61), + (2, 73), + (3, 61), + ]), + value_counts: HashMap::default(), + null_value_counts: HashMap::default(), + nan_value_counts: HashMap::new(), + lower_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::int(2)), + ]), + upper_bounds: HashMap::from([ + (1, Literal::long(1)), + (2, Literal::int(2)), + ]), + key_metadata: vec![], + split_offsets: vec![4], + equality_ids: vec![], + sort_order_id: None, + }, + })], + }; + + assert_eq!(actual_manifest, expected_manifest); + } + + async fn test_manifest_read_write( + manifest: Manifest, + writer_builder: impl FnOnce(OutputFile) -> ManifestWriter, + ) -> ManifestListEntry { + let (bs, res) = write_manifest(&manifest, writer_builder).await; + let actual_manifest = Manifest::parse_avro(bs.as_slice()).unwrap(); + + assert_eq!(actual_manifest, manifest); + res + } + + /// Utility method which writes out a manifest and returns the bytes. + async fn write_manifest( + manifest: &Manifest, + writer_builder: impl FnOnce(OutputFile) -> ManifestWriter, + ) -> (Vec, ManifestListEntry) { + let temp_dir = TempDir::new().unwrap(); + let path = temp_dir.path().join("test_manifest.avro"); + let io = FileIOBuilder::new_fs_io().build().unwrap(); + let output_file = io.new_output(path.to_str().unwrap()).unwrap(); + let writer = writer_builder(output_file); + let res = writer.write(manifest.clone()).await.unwrap(); + + // Verify manifest + (fs::read(path).expect("read_file must succeed"), res) + } +} diff --git a/libs/iceberg/src/spec/manifest_list.rs b/libs/iceberg/src/spec/manifest_list.rs new file mode 100644 index 0000000..6dc4839 --- /dev/null +++ b/libs/iceberg/src/spec/manifest_list.rs @@ -0,0 +1,1468 @@ +// 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. + +//! ManifestList for Iceberg. + +use std::{collections::HashMap, str::FromStr}; + +use crate::io::FileIO; +use crate::{io::OutputFile, spec::Literal, Error, ErrorKind}; +use apache_avro::{from_value, types::Value, Reader, Writer}; +use futures::{AsyncReadExt, AsyncWriteExt}; + +use self::{ + _const_schema::{MANIFEST_LIST_AVRO_SCHEMA_V1, MANIFEST_LIST_AVRO_SCHEMA_V2}, + _serde::{ManifestListEntryV1, ManifestListEntryV2}, +}; + +use super::{FormatVersion, Manifest, StructType}; +use crate::error::Result; + +/// Placeholder for sequence number. The field with this value must be replaced with the actual sequence number before it write. +pub const UNASSIGNED_SEQUENCE_NUMBER: i64 = -1; + +/// Snapshots are embedded in table metadata, but the list of manifests for a +/// snapshot are stored in a separate manifest list file. +/// +/// A new manifest list is written for each attempt to commit a snapshot +/// because the list of manifests always changes to produce a new snapshot. +/// When a manifest list is written, the (optimistic) sequence number of the +/// snapshot is written for all new manifest files tracked by the list. +/// +/// A manifest list includes summary metadata that can be used to avoid +/// scanning all of the manifests in a snapshot when planning a table scan. +/// This includes the number of added, existing, and deleted files, and a +/// summary of values for each field of the partition spec used to write the +/// manifest. +#[derive(Debug, Clone, PartialEq)] +pub struct ManifestList { + /// Entries in a manifest list. + entries: Vec, +} + +impl ManifestList { + /// Parse manifest list from bytes. + pub fn parse_with_version( + bs: &[u8], + version: FormatVersion, + partition_type_provider: impl Fn(i32) -> Result>, + ) -> Result { + match version { + FormatVersion::V1 => { + let reader = Reader::with_schema(&MANIFEST_LIST_AVRO_SCHEMA_V1, bs)?; + let values = Value::Array(reader.collect::, _>>()?); + from_value::<_serde::ManifestListV1>(&values)?.try_into(partition_type_provider) + } + FormatVersion::V2 => { + let reader = Reader::with_schema(&MANIFEST_LIST_AVRO_SCHEMA_V2, bs)?; + let values = Value::Array(reader.collect::, _>>()?); + from_value::<_serde::ManifestListV2>(&values)?.try_into(partition_type_provider) + } + } + } + + /// Get the entries in the manifest list. + pub fn entries(&self) -> &[ManifestListEntry] { + &self.entries + } +} + +/// A manifest list writer. +pub struct ManifestListWriter { + format_version: FormatVersion, + output_file: OutputFile, + avro_writer: Writer<'static, Vec>, + sequence_number: i64, + snapshot_id: i64, +} + +impl std::fmt::Debug for ManifestListWriter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ManifestListWriter") + .field("format_version", &self.format_version) + .field("output_file", &self.output_file) + .field("avro_writer", &self.avro_writer.schema()) + .finish_non_exhaustive() + } +} + +impl ManifestListWriter { + /// Construct a v1 [`ManifestListWriter`] that writes to a provided [`OutputFile`]. + pub fn v1(output_file: OutputFile, snapshot_id: i64, parent_snapshot_id: i64) -> Self { + let metadata = HashMap::from_iter([ + ("snapshot-id".to_string(), snapshot_id.to_string()), + ( + "parent-snapshot-id".to_string(), + parent_snapshot_id.to_string(), + ), + ("format-version".to_string(), "1".to_string()), + ]); + Self::new(FormatVersion::V1, output_file, metadata, 0, snapshot_id) + } + + /// Construct a v2 [`ManifestListWriter`] that writes to a provided [`OutputFile`]. + pub fn v2( + output_file: OutputFile, + snapshot_id: i64, + parent_snapshot_id: i64, + sequence_number: i64, + ) -> Self { + let metadata = HashMap::from_iter([ + ("snapshot-id".to_string(), snapshot_id.to_string()), + ( + "parent-snapshot-id".to_string(), + parent_snapshot_id.to_string(), + ), + ("sequence-number".to_string(), sequence_number.to_string()), + ("format-version".to_string(), "2".to_string()), + ]); + Self::new( + FormatVersion::V2, + output_file, + metadata, + sequence_number, + snapshot_id, + ) + } + + fn new( + format_version: FormatVersion, + output_file: OutputFile, + metadata: HashMap, + sequence_number: i64, + snapshot_id: i64, + ) -> Self { + let avro_schema = match format_version { + FormatVersion::V1 => &MANIFEST_LIST_AVRO_SCHEMA_V1, + FormatVersion::V2 => &MANIFEST_LIST_AVRO_SCHEMA_V2, + }; + let mut avro_writer = Writer::new(avro_schema, Vec::new()); + for (key, value) in metadata { + avro_writer + .add_user_metadata(key, value) + .expect("Avro metadata should be added to the writer before the first record."); + } + Self { + format_version, + output_file, + avro_writer, + sequence_number, + snapshot_id, + } + } + + /// Append manifest entries to be written. + pub fn add_manifest_entries( + &mut self, + manifest_entries: impl Iterator, + ) -> Result<()> { + match self.format_version { + FormatVersion::V1 => { + for manifest_entry in manifest_entries { + let manifest_entry: ManifestListEntryV1 = manifest_entry.try_into()?; + self.avro_writer.append_ser(manifest_entry)?; + } + } + FormatVersion::V2 => { + for mut manifest_entry in manifest_entries { + if manifest_entry.sequence_number == UNASSIGNED_SEQUENCE_NUMBER { + if manifest_entry.added_snapshot_id != self.snapshot_id { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Found unassigned sequence number for a manifest from snapshot {}.", + manifest_entry.added_snapshot_id + ), + )); + } + manifest_entry.sequence_number = self.sequence_number; + } + if manifest_entry.min_sequence_number == UNASSIGNED_SEQUENCE_NUMBER { + if manifest_entry.added_snapshot_id != self.snapshot_id { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Found unassigned sequence number for a manifest from snapshot {}.", + manifest_entry.added_snapshot_id + ), + )); + } + manifest_entry.min_sequence_number = self.sequence_number; + } + let manifest_entry: ManifestListEntryV2 = manifest_entry.try_into()?; + self.avro_writer.append_ser(manifest_entry)?; + } + } + } + Ok(()) + } + + /// Write the manifest list to the output file. + pub async fn close(self) -> Result<()> { + let data = self.avro_writer.into_inner()?; + let mut writer = self.output_file.writer().await?; + writer.write_all(&data).await?; + writer.close().await?; + Ok(()) + } +} + +/// This is a helper module that defines the schema field of the manifest list entry. +mod _const_schema { + use std::sync::Arc; + + use apache_avro::Schema as AvroSchema; + use once_cell::sync::Lazy; + + use crate::{ + avro::schema_to_avro_schema, + spec::{ListType, NestedField, NestedFieldRef, PrimitiveType, Schema, StructType, Type}, + }; + + static MANIFEST_PATH: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 500, + "manifest_path", + Type::Primitive(PrimitiveType::String), + )) + }) + }; + static MANIFEST_LENGTH: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 501, + "manifest_length", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static PARTITION_SPEC_ID: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 502, + "partition_spec_id", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static CONTENT: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 517, + "content", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static SEQUENCE_NUMBER: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 515, + "sequence_number", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static MIN_SEQUENCE_NUMBER: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 516, + "min_sequence_number", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static ADDED_SNAPSHOT_ID: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 503, + "added_snapshot_id", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static ADDED_FILES_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 504, + "added_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static ADDED_FILES_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 504, + "added_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static EXISTING_FILES_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 505, + "existing_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static EXISTING_FILES_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 505, + "existing_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static DELETED_FILES_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 506, + "deleted_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static DELETED_FILES_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 506, + "deleted_data_files_count", + Type::Primitive(PrimitiveType::Int), + )) + }) + }; + static ADDED_ROWS_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 512, + "added_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static ADDED_ROWS_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 512, + "added_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static EXISTING_ROWS_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 513, + "existing_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static EXISTING_ROWS_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 513, + "existing_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static DELETED_ROWS_COUNT_V2: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::required( + 514, + "deleted_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static DELETED_ROWS_COUNT_V1: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 514, + "deleted_rows_count", + Type::Primitive(PrimitiveType::Long), + )) + }) + }; + static PARTITIONS: Lazy = { + Lazy::new(|| { + // element type + let fields = vec![ + Arc::new(NestedField::required( + 509, + "contains_null", + Type::Primitive(PrimitiveType::Boolean), + )), + Arc::new(NestedField::optional( + 518, + "contains_nan", + Type::Primitive(PrimitiveType::Boolean), + )), + Arc::new(NestedField::optional( + 510, + "lower_bound", + Type::Primitive(PrimitiveType::Binary), + )), + Arc::new(NestedField::optional( + 511, + "upper_bound", + Type::Primitive(PrimitiveType::Binary), + )), + ]; + let element_field = Arc::new(NestedField::required( + 508, + "r_508", + Type::Struct(StructType::new(fields)), + )); + Arc::new(NestedField::optional( + 507, + "partitions", + Type::List(ListType { element_field }), + )) + }) + }; + static KEY_METADATA: Lazy = { + Lazy::new(|| { + Arc::new(NestedField::optional( + 519, + "key_metadata", + Type::Primitive(PrimitiveType::Binary), + )) + }) + }; + + static V1_SCHEMA: Lazy = { + Lazy::new(|| { + let fields = vec![ + MANIFEST_PATH.clone(), + MANIFEST_LENGTH.clone(), + PARTITION_SPEC_ID.clone(), + ADDED_SNAPSHOT_ID.clone(), + ADDED_FILES_COUNT_V1.clone().to_owned(), + EXISTING_FILES_COUNT_V1.clone(), + DELETED_FILES_COUNT_V1.clone(), + ADDED_ROWS_COUNT_V1.clone(), + EXISTING_ROWS_COUNT_V1.clone(), + DELETED_ROWS_COUNT_V1.clone(), + PARTITIONS.clone(), + KEY_METADATA.clone(), + ]; + Schema::builder().with_fields(fields).build().unwrap() + }) + }; + + static V2_SCHEMA: Lazy = { + Lazy::new(|| { + let fields = vec![ + MANIFEST_PATH.clone(), + MANIFEST_LENGTH.clone(), + PARTITION_SPEC_ID.clone(), + CONTENT.clone(), + SEQUENCE_NUMBER.clone(), + MIN_SEQUENCE_NUMBER.clone(), + ADDED_SNAPSHOT_ID.clone(), + ADDED_FILES_COUNT_V2.clone(), + EXISTING_FILES_COUNT_V2.clone(), + DELETED_FILES_COUNT_V2.clone(), + ADDED_ROWS_COUNT_V2.clone(), + EXISTING_ROWS_COUNT_V2.clone(), + DELETED_ROWS_COUNT_V2.clone(), + PARTITIONS.clone(), + KEY_METADATA.clone(), + ]; + Schema::builder().with_fields(fields).build().unwrap() + }) + }; + + pub(super) static MANIFEST_LIST_AVRO_SCHEMA_V1: Lazy = + Lazy::new(|| schema_to_avro_schema("manifest_file", &V1_SCHEMA).unwrap()); + + pub(super) static MANIFEST_LIST_AVRO_SCHEMA_V2: Lazy = + Lazy::new(|| schema_to_avro_schema("manifest_file", &V2_SCHEMA).unwrap()); +} + +/// Entry in a manifest list. +#[derive(Debug, PartialEq, Clone)] +pub struct ManifestListEntry { + /// field: 500 + /// + /// Location of the manifest file + pub manifest_path: String, + /// field: 501 + /// + /// Length of the manifest file in bytes + pub manifest_length: i64, + /// field: 502 + /// + /// ID of a partition spec used to write the manifest; must be listed + /// in table metadata partition-specs + pub partition_spec_id: i32, + /// field: 517 + /// + /// The type of files tracked by the manifest, either data or delete + /// files; 0 for all v1 manifests + pub content: ManifestContentType, + /// field: 515 + /// + /// The sequence number when the manifest was added to the table; use 0 + /// when reading v1 manifest lists + pub sequence_number: i64, + /// field: 516 + /// + /// The minimum data sequence number of all live data or delete files in + /// the manifest; use 0 when reading v1 manifest lists + pub min_sequence_number: i64, + /// field: 503 + /// + /// ID of the snapshot where the manifest file was added + pub added_snapshot_id: i64, + /// field: 504 + /// + /// Number of entries in the manifest that have status ADDED, when null + /// this is assumed to be non-zero + pub added_data_files_count: Option, + /// field: 505 + /// + /// Number of entries in the manifest that have status EXISTING (0), + /// when null this is assumed to be non-zero + pub existing_data_files_count: Option, + /// field: 506 + /// + /// Number of entries in the manifest that have status DELETED (2), + /// when null this is assumed to be non-zero + pub deleted_data_files_count: Option, + /// field: 512 + /// + /// Number of rows in all of files in the manifest that have status + /// ADDED, when null this is assumed to be non-zero + pub added_rows_count: Option, + /// field: 513 + /// + /// Number of rows in all of files in the manifest that have status + /// EXISTING, when null this is assumed to be non-zero + pub existing_rows_count: Option, + /// field: 514 + /// + /// Number of rows in all of files in the manifest that have status + /// DELETED, when null this is assumed to be non-zero + pub deleted_rows_count: Option, + /// field: 507 + /// element_field: 508 + /// + /// A list of field summaries for each partition field in the spec. Each + /// field in the list corresponds to a field in the manifest file’s + /// partition spec. + pub partitions: Vec, + /// field: 519 + /// + /// Implementation-specific key metadata for encryption + pub key_metadata: Vec, +} + +/// The type of files tracked by the manifest, either data or delete files; Data(0) for all v1 manifests +#[derive(Debug, PartialEq, Clone, Eq)] +pub enum ManifestContentType { + /// The manifest content is data. + Data = 0, + /// The manifest content is deletes. + Deletes = 1, +} + +impl FromStr for ManifestContentType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "data" => Ok(ManifestContentType::Data), + "deletes" => Ok(ManifestContentType::Deletes), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("Invalid manifest content type: {s}"), + )), + } + } +} + +impl ToString for ManifestContentType { + fn to_string(&self) -> String { + match self { + ManifestContentType::Data => "data".to_string(), + ManifestContentType::Deletes => "deletes".to_string(), + } + } +} + +impl TryFrom for ManifestContentType { + type Error = Error; + + fn try_from(value: i32) -> std::result::Result { + match value { + 0 => Ok(ManifestContentType::Data), + 1 => Ok(ManifestContentType::Deletes), + _ => Err(Error::new( + crate::ErrorKind::DataInvalid, + format!( + "Invalid manifest content type. Expected 0 or 1, got {}", + value + ), + )), + } + } +} + +impl ManifestListEntry { + /// Load [`Manifest`]. + /// + /// This method will also initialize inherited values of [`ManifestEntry`], such as `sequence_number`. + pub async fn load_manifest(&self, file_io: &FileIO) -> Result { + let mut avro = Vec::new(); + file_io + .new_input(&self.manifest_path)? + .reader() + .await? + .read_to_end(&mut avro) + .await?; + + let (metadata, mut entries) = Manifest::try_from_avro_bytes(&avro)?; + + // Let entries inherit values from the manifest list entry. + for entry in &mut entries { + entry.inherit_data(self); + } + + Ok(Manifest::new(metadata, entries)) + } +} + +/// Field summary for partition field in the spec. +/// +/// Each field in the list corresponds to a field in the manifest file’s partition spec. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct FieldSummary { + /// field: 509 + /// + /// Whether the manifest contains at least one partition with a null + /// value for the field + pub contains_null: bool, + /// field: 518 + /// Whether the manifest contains at least one partition with a NaN + /// value for the field + pub contains_nan: Option, + /// field: 510 + /// The minimum value for the field in the manifests + /// partitions. + pub lower_bound: Option, + /// field: 511 + /// The maximum value for the field in the manifests + /// partitions. + pub upper_bound: Option, +} + +/// This is a helper module that defines types to help with serialization/deserialization. +/// For deserialization the input first gets read into either the [ManifestListEntryV1] or [ManifestListEntryV2] struct +/// and then converted into the [ManifestListEntry] struct. Serialization works the other way around. +/// [ManifestListEntryV1] and [ManifestListEntryV2] are internal struct that are only used for serialization and deserialization. +pub(super) mod _serde { + use crate::{ + spec::{Literal, StructType, Type}, + Error, + }; + pub use serde_bytes::ByteBuf; + use serde_derive::{Deserialize, Serialize}; + + use super::ManifestListEntry; + use crate::error::Result; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(transparent)] + pub(crate) struct ManifestListV2 { + entries: Vec, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(transparent)] + pub(crate) struct ManifestListV1 { + entries: Vec, + } + + impl ManifestListV2 { + /// Converts the [ManifestListV2] into a [ManifestList]. + /// The convert of [entries] need the partition_type info so use this function instead of [std::TryFrom] trait. + pub fn try_into( + self, + partition_type_provider: impl Fn(i32) -> Result>, + ) -> Result { + Ok(super::ManifestList { + entries: self + .entries + .into_iter() + .map(|v| { + let partition_spec_id = v.partition_spec_id; + let manifest_path = v.manifest_path.clone(); + v.try_into(partition_type_provider(partition_spec_id)?.as_ref()) + .map_err(|err| { + err.with_context("manifest file path", manifest_path) + .with_context( + "partition spec id", + partition_spec_id.to_string(), + ) + }) + }) + .collect::>>()?, + }) + } + } + + impl TryFrom for ManifestListV2 { + type Error = Error; + + fn try_from(value: super::ManifestList) -> std::result::Result { + Ok(Self { + entries: value + .entries + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } + } + + impl ManifestListV1 { + /// Converts the [ManifestListV1] into a [ManifestList]. + /// The convert of [entries] need the partition_type info so use this function instead of [std::TryFrom] trait. + pub fn try_into( + self, + partition_type_provider: impl Fn(i32) -> Result>, + ) -> Result { + Ok(super::ManifestList { + entries: self + .entries + .into_iter() + .map(|v| { + let partition_spec_id = v.partition_spec_id; + let manifest_path = v.manifest_path.clone(); + v.try_into(partition_type_provider(partition_spec_id)?.as_ref()) + .map_err(|err| { + err.with_context("manifest file path", manifest_path) + .with_context( + "partition spec id", + partition_spec_id.to_string(), + ) + }) + }) + .collect::>>()?, + }) + } + } + + impl TryFrom for ManifestListV1 { + type Error = Error; + + fn try_from(value: super::ManifestList) -> std::result::Result { + Ok(Self { + entries: value + .entries + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + pub(super) struct ManifestListEntryV1 { + pub manifest_path: String, + pub manifest_length: i64, + pub partition_spec_id: i32, + pub added_snapshot_id: i64, + pub added_data_files_count: Option, + pub existing_data_files_count: Option, + pub deleted_data_files_count: Option, + pub added_rows_count: Option, + pub existing_rows_count: Option, + pub deleted_rows_count: Option, + pub partitions: Option>, + pub key_metadata: Option, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + pub(super) struct ManifestListEntryV2 { + pub manifest_path: String, + pub manifest_length: i64, + pub partition_spec_id: i32, + pub content: i32, + pub sequence_number: i64, + pub min_sequence_number: i64, + pub added_snapshot_id: i64, + pub added_data_files_count: i32, + pub existing_data_files_count: i32, + pub deleted_data_files_count: i32, + pub added_rows_count: i64, + pub existing_rows_count: i64, + pub deleted_rows_count: i64, + pub partitions: Option>, + pub key_metadata: Option, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + pub(super) struct FieldSummary { + pub contains_null: bool, + pub contains_nan: Option, + pub lower_bound: Option, + pub upper_bound: Option, + } + + impl FieldSummary { + /// Converts the [FieldSummary] into a [super::FieldSummary]. + /// [lower_bound] and [upper_bound] are converted into [Literal]s need the type info so use + /// this function instead of [std::TryFrom] trait. + pub(crate) fn try_into(self, r#type: &Type) -> Result { + Ok(super::FieldSummary { + contains_null: self.contains_null, + contains_nan: self.contains_nan, + lower_bound: self + .lower_bound + .map(|v| Literal::try_from_bytes(&v, r#type)) + .transpose()?, + upper_bound: self + .upper_bound + .map(|v| Literal::try_from_bytes(&v, r#type)) + .transpose()?, + }) + } + } + + fn try_convert_to_field_summary( + partitions: Option>, + partition_type: Option<&StructType>, + ) -> Result> { + if let Some(partitions) = partitions { + if let Some(partition_type) = partition_type { + let partition_types = partition_type.fields(); + if partitions.len() != partition_types.len() { + return Err(Error::new( + crate::ErrorKind::DataInvalid, + format!( + "Invalid partition spec. Expected {} fields, got {}", + partition_types.len(), + partitions.len() + ), + )); + } + partitions + .into_iter() + .zip(partition_types) + .map(|(v, field)| v.try_into(&field.field_type)) + .collect::>>() + } else { + Err(Error::new( + crate::ErrorKind::DataInvalid, + "Invalid partition spec. Partition type is required", + )) + } + } else { + Ok(Vec::new()) + } + } + + impl ManifestListEntryV2 { + /// Converts the [ManifestListEntryV2] into a [ManifestListEntry]. + /// The convert of [partitions] need the partition_type info so use this function instead of [std::TryFrom] trait. + pub fn try_into(self, partition_type: Option<&StructType>) -> Result { + let partitions = try_convert_to_field_summary(self.partitions, partition_type)?; + Ok(ManifestListEntry { + manifest_path: self.manifest_path, + manifest_length: self.manifest_length, + partition_spec_id: self.partition_spec_id, + content: self.content.try_into()?, + sequence_number: self.sequence_number, + min_sequence_number: self.min_sequence_number, + added_snapshot_id: self.added_snapshot_id, + added_data_files_count: Some(self.added_data_files_count.try_into()?), + existing_data_files_count: Some(self.existing_data_files_count.try_into()?), + deleted_data_files_count: Some(self.deleted_data_files_count.try_into()?), + added_rows_count: Some(self.added_rows_count.try_into()?), + existing_rows_count: Some(self.existing_rows_count.try_into()?), + deleted_rows_count: Some(self.deleted_rows_count.try_into()?), + partitions, + key_metadata: self.key_metadata.map(|b| b.into_vec()).unwrap_or_default(), + }) + } + } + + impl ManifestListEntryV1 { + /// Converts the [ManifestListEntryV1] into a [ManifestListEntry]. + /// The convert of [partitions] need the partition_type info so use this function instead of [std::TryFrom] trait. + pub fn try_into(self, partition_type: Option<&StructType>) -> Result { + let partitions = try_convert_to_field_summary(self.partitions, partition_type)?; + Ok(ManifestListEntry { + manifest_path: self.manifest_path, + manifest_length: self.manifest_length, + partition_spec_id: self.partition_spec_id, + added_snapshot_id: self.added_snapshot_id, + added_data_files_count: self + .added_data_files_count + .map(TryInto::try_into) + .transpose()?, + existing_data_files_count: self + .existing_data_files_count + .map(TryInto::try_into) + .transpose()?, + deleted_data_files_count: self + .deleted_data_files_count + .map(TryInto::try_into) + .transpose()?, + added_rows_count: self.added_rows_count.map(TryInto::try_into).transpose()?, + existing_rows_count: self + .existing_rows_count + .map(TryInto::try_into) + .transpose()?, + deleted_rows_count: self.deleted_rows_count.map(TryInto::try_into).transpose()?, + partitions, + key_metadata: self.key_metadata.map(|b| b.into_vec()).unwrap_or_default(), + // as ref: https://iceberg.apache.org/spec/#partitioning + // use 0 when reading v1 manifest lists + content: super::ManifestContentType::Data, + sequence_number: 0, + min_sequence_number: 0, + }) + } + } + + fn convert_to_serde_field_summary( + partitions: Vec, + ) -> Option> { + if partitions.is_empty() { + None + } else { + Some( + partitions + .into_iter() + .map(|v| FieldSummary { + contains_null: v.contains_null, + contains_nan: v.contains_nan, + lower_bound: v.lower_bound.map(|v| v.into()), + upper_bound: v.upper_bound.map(|v| v.into()), + }) + .collect(), + ) + } + } + + fn convert_to_serde_key_metadata(key_metadata: Vec) -> Option { + if key_metadata.is_empty() { + None + } else { + Some(ByteBuf::from(key_metadata)) + } + } + + impl TryFrom for ManifestListEntryV2 { + type Error = Error; + + fn try_from(value: ManifestListEntry) -> std::result::Result { + let partitions = convert_to_serde_field_summary(value.partitions); + let key_metadata = convert_to_serde_key_metadata(value.key_metadata); + Ok(Self { + manifest_path: value.manifest_path, + manifest_length: value.manifest_length, + partition_spec_id: value.partition_spec_id, + content: value.content as i32, + sequence_number: value.sequence_number, + min_sequence_number: value.min_sequence_number, + added_snapshot_id: value.added_snapshot_id, + added_data_files_count: value + .added_data_files_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "added_data_files_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + existing_data_files_count: value + .existing_data_files_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "existing_data_files_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + deleted_data_files_count: value + .deleted_data_files_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "deleted_data_files_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + added_rows_count: value + .added_rows_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "added_rows_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + existing_rows_count: value + .existing_rows_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "existing_rows_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + deleted_rows_count: value + .deleted_rows_count + .ok_or_else(|| { + Error::new( + crate::ErrorKind::DataInvalid, + "deleted_rows_count in ManifestListEntryV2 should be require", + ) + })? + .try_into()?, + partitions, + key_metadata, + }) + } + } + + impl TryFrom for ManifestListEntryV1 { + type Error = Error; + + fn try_from(value: ManifestListEntry) -> std::result::Result { + let partitions = convert_to_serde_field_summary(value.partitions); + let key_metadata = convert_to_serde_key_metadata(value.key_metadata); + Ok(Self { + manifest_path: value.manifest_path, + manifest_length: value.manifest_length, + partition_spec_id: value.partition_spec_id, + added_snapshot_id: value.added_snapshot_id, + added_data_files_count: value + .added_data_files_count + .map(TryInto::try_into) + .transpose()?, + existing_data_files_count: value + .existing_data_files_count + .map(TryInto::try_into) + .transpose()?, + deleted_data_files_count: value + .deleted_data_files_count + .map(TryInto::try_into) + .transpose()?, + added_rows_count: value.added_rows_count.map(TryInto::try_into).transpose()?, + existing_rows_count: value + .existing_rows_count + .map(TryInto::try_into) + .transpose()?, + deleted_rows_count: value + .deleted_rows_count + .map(TryInto::try_into) + .transpose()?, + partitions, + key_metadata, + }) + } + } +} + +#[cfg(test)] +mod test { + use std::{collections::HashMap, fs, sync::Arc}; + + use tempfile::TempDir; + + use crate::{ + io::FileIOBuilder, + spec::{ + manifest_list::{_serde::ManifestListV1, UNASSIGNED_SEQUENCE_NUMBER}, + FieldSummary, Literal, ManifestContentType, ManifestList, ManifestListEntry, + ManifestListWriter, NestedField, PrimitiveType, StructType, Type, + }, + }; + + use super::_serde::ManifestListV2; + + #[tokio::test] + async fn test_parse_manifest_list_v1() { + let manifest_list = ManifestList { + entries: vec![ + ManifestListEntry { + manifest_path: "/opt/bitnami/spark/warehouse/db/table/metadata/10d28031-9739-484c-92db-cdf2975cead4-m0.avro".to_string(), + manifest_length: 5806, + partition_spec_id: 0, + content: ManifestContentType::Data, + sequence_number: 0, + min_sequence_number: 0, + added_snapshot_id: 1646658105718557341, + added_data_files_count: Some(3), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![], + key_metadata: vec![], + } + ] + }; + + let file_io = FileIOBuilder::new_fs_io().build().unwrap(); + + let tmp_dir = TempDir::new().unwrap(); + let file_name = "simple_manifest_list_v1.avro"; + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + + let mut writer = ManifestListWriter::v1( + file_io.new_output(full_path.clone()).unwrap(), + 1646658105718557341, + 1646658105718557341, + ); + + writer + .add_manifest_entries(manifest_list.entries.clone().into_iter()) + .unwrap(); + writer.close().await.unwrap(); + + let bs = fs::read(full_path).expect("read_file must succeed"); + + let parsed_manifest_list = + ManifestList::parse_with_version(&bs, crate::spec::FormatVersion::V1, |_id| Ok(None)) + .unwrap(); + + assert_eq!(manifest_list, parsed_manifest_list); + } + + #[tokio::test] + async fn test_parse_manifest_list_v2() { + let manifest_list = ManifestList { + entries: vec![ + ManifestListEntry { + manifest_path: "s3a://icebergdata/demo/s1/t1/metadata/05ffe08b-810f-49b3-a8f4-e88fc99b254a-m0.avro".to_string(), + manifest_length: 6926, + partition_spec_id: 1, + content: ManifestContentType::Data, + sequence_number: 1, + min_sequence_number: 1, + added_snapshot_id: 377075049360453639, + added_data_files_count: Some(1), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::long(1)), upper_bound: Some(Literal::long(1))}], + key_metadata: vec![], + }, + ManifestListEntry { + manifest_path: "s3a://icebergdata/demo/s1/t1/metadata/05ffe08b-810f-49b3-a8f4-e88fc99b254a-m1.avro".to_string(), + manifest_length: 6926, + partition_spec_id: 2, + content: ManifestContentType::Data, + sequence_number: 1, + min_sequence_number: 1, + added_snapshot_id: 377075049360453639, + added_data_files_count: Some(1), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::float(1.1)), upper_bound: Some(Literal::float(2.1))}], + key_metadata: vec![], + } + ] + }; + + let file_io = FileIOBuilder::new_fs_io().build().unwrap(); + + let tmp_dir = TempDir::new().unwrap(); + let file_name = "simple_manifest_list_v1.avro"; + let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name); + + let mut writer = ManifestListWriter::v2( + file_io.new_output(full_path.clone()).unwrap(), + 1646658105718557341, + 1646658105718557341, + 1, + ); + + writer + .add_manifest_entries(manifest_list.entries.clone().into_iter()) + .unwrap(); + writer.close().await.unwrap(); + + let bs = fs::read(full_path).expect("read_file must succeed"); + + let parsed_manifest_list = + ManifestList::parse_with_version(&bs, crate::spec::FormatVersion::V2, |id| { + Ok(HashMap::from([ + ( + 1, + StructType::new(vec![Arc::new(NestedField::required( + 1, + "test", + Type::Primitive(PrimitiveType::Long), + ))]), + ), + ( + 2, + StructType::new(vec![Arc::new(NestedField::required( + 1, + "test", + Type::Primitive(PrimitiveType::Float), + ))]), + ), + ]) + .get(&id) + .cloned()) + }) + .unwrap(); + + assert_eq!(manifest_list, parsed_manifest_list); + } + + #[test] + fn test_serialize_manifest_list_v1() { + let manifest_list:ManifestListV1 = ManifestList { + entries: vec![ManifestListEntry { + manifest_path: "/opt/bitnami/spark/warehouse/db/table/metadata/10d28031-9739-484c-92db-cdf2975cead4-m0.avro".to_string(), + manifest_length: 5806, + partition_spec_id: 0, + content: ManifestContentType::Data, + sequence_number: 0, + min_sequence_number: 0, + added_snapshot_id: 1646658105718557341, + added_data_files_count: Some(3), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![], + key_metadata: vec![], + }] + }.try_into().unwrap(); + let result = serde_json::to_string(&manifest_list).unwrap(); + assert_eq!( + result, + r#"[{"manifest_path":"/opt/bitnami/spark/warehouse/db/table/metadata/10d28031-9739-484c-92db-cdf2975cead4-m0.avro","manifest_length":5806,"partition_spec_id":0,"added_snapshot_id":1646658105718557341,"added_data_files_count":3,"existing_data_files_count":0,"deleted_data_files_count":0,"added_rows_count":3,"existing_rows_count":0,"deleted_rows_count":0,"partitions":null,"key_metadata":null}]"# + ); + } + + #[test] + fn test_serialize_manifest_list_v2() { + let manifest_list:ManifestListV2 = ManifestList { + entries: vec![ManifestListEntry { + manifest_path: "s3a://icebergdata/demo/s1/t1/metadata/05ffe08b-810f-49b3-a8f4-e88fc99b254a-m0.avro".to_string(), + manifest_length: 6926, + partition_spec_id: 1, + content: ManifestContentType::Data, + sequence_number: 1, + min_sequence_number: 1, + added_snapshot_id: 377075049360453639, + added_data_files_count: Some(1), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::long(1)), upper_bound: Some(Literal::long(1))}], + key_metadata: vec![], + }] + }.try_into().unwrap(); + let result = serde_json::to_string(&manifest_list).unwrap(); + assert_eq!( + result, + r#"[{"manifest_path":"s3a://icebergdata/demo/s1/t1/metadata/05ffe08b-810f-49b3-a8f4-e88fc99b254a-m0.avro","manifest_length":6926,"partition_spec_id":1,"content":0,"sequence_number":1,"min_sequence_number":1,"added_snapshot_id":377075049360453639,"added_data_files_count":1,"existing_data_files_count":0,"deleted_data_files_count":0,"added_rows_count":3,"existing_rows_count":0,"deleted_rows_count":0,"partitions":[{"contains_null":false,"contains_nan":false,"lower_bound":[1,0,0,0,0,0,0,0],"upper_bound":[1,0,0,0,0,0,0,0]}],"key_metadata":null}]"# + ); + } + + #[tokio::test] + async fn test_manifest_list_writer_v1() { + let expected_manifest_list = ManifestList { + entries: vec![ManifestListEntry { + manifest_path: "/opt/bitnami/spark/warehouse/db/table/metadata/10d28031-9739-484c-92db-cdf2975cead4-m0.avro".to_string(), + manifest_length: 5806, + partition_spec_id: 1, + content: ManifestContentType::Data, + sequence_number: 0, + min_sequence_number: 0, + added_snapshot_id: 1646658105718557341, + added_data_files_count: Some(3), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::long(1)), upper_bound: Some(Literal::long(1))}], + key_metadata: vec![], + }] + }; + + let temp_dir = TempDir::new().unwrap(); + let path = temp_dir.path().join("manifest_list_v1.avro"); + let io = FileIOBuilder::new_fs_io().build().unwrap(); + let output_file = io.new_output(path.to_str().unwrap()).unwrap(); + + let mut writer = ManifestListWriter::v1(output_file, 1646658105718557341, 0); + writer + .add_manifest_entries(expected_manifest_list.entries.clone().into_iter()) + .unwrap(); + writer.close().await.unwrap(); + + let bs = fs::read(path).unwrap(); + + let partition_types = HashMap::from([( + 1, + StructType::new(vec![Arc::new(NestedField::required( + 1, + "test", + Type::Primitive(PrimitiveType::Long), + ))]), + )]); + + let manifest_list = + ManifestList::parse_with_version(&bs, crate::spec::FormatVersion::V1, move |id| { + Ok(partition_types.get(&id).cloned()) + }) + .unwrap(); + assert_eq!(manifest_list, expected_manifest_list); + + temp_dir.close().unwrap(); + } + + #[tokio::test] + async fn test_manifest_list_writer_v2() { + let snapshot_id = 377075049360453639; + let seq_num = 1; + let mut expected_manifest_list = ManifestList { + entries: vec![ManifestListEntry { + manifest_path: "s3a://icebergdata/demo/s1/t1/metadata/05ffe08b-810f-49b3-a8f4-e88fc99b254a-m0.avro".to_string(), + manifest_length: 6926, + partition_spec_id: 1, + content: ManifestContentType::Data, + sequence_number: UNASSIGNED_SEQUENCE_NUMBER, + min_sequence_number: UNASSIGNED_SEQUENCE_NUMBER, + added_snapshot_id: snapshot_id, + added_data_files_count: Some(1), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::long(1)), upper_bound: Some(Literal::long(1))}], + key_metadata: vec![], + }] + }; + + let temp_dir = TempDir::new().unwrap(); + let path = temp_dir.path().join("manifest_list_v2.avro"); + let io = FileIOBuilder::new_fs_io().build().unwrap(); + let output_file = io.new_output(path.to_str().unwrap()).unwrap(); + + let mut writer = ManifestListWriter::v2(output_file, snapshot_id, 0, seq_num); + writer + .add_manifest_entries(expected_manifest_list.entries.clone().into_iter()) + .unwrap(); + writer.close().await.unwrap(); + + let bs = fs::read(path).unwrap(); + let partition_types = HashMap::from([( + 1, + StructType::new(vec![Arc::new(NestedField::required( + 1, + "test", + Type::Primitive(PrimitiveType::Long), + ))]), + )]); + let manifest_list = + ManifestList::parse_with_version(&bs, crate::spec::FormatVersion::V2, move |id| { + Ok(partition_types.get(&id).cloned()) + }) + .unwrap(); + expected_manifest_list.entries[0].sequence_number = seq_num; + expected_manifest_list.entries[0].min_sequence_number = seq_num; + assert_eq!(manifest_list, expected_manifest_list); + + temp_dir.close().unwrap(); + } + + #[tokio::test] + async fn test_manifest_list_writer_v1_as_v2() { + let expected_manifest_list = ManifestList { + entries: vec![ManifestListEntry { + manifest_path: "/opt/bitnami/spark/warehouse/db/table/metadata/10d28031-9739-484c-92db-cdf2975cead4-m0.avro".to_string(), + manifest_length: 5806, + partition_spec_id: 1, + content: ManifestContentType::Data, + sequence_number: 0, + min_sequence_number: 0, + added_snapshot_id: 1646658105718557341, + added_data_files_count: Some(3), + existing_data_files_count: Some(0), + deleted_data_files_count: Some(0), + added_rows_count: Some(3), + existing_rows_count: Some(0), + deleted_rows_count: Some(0), + partitions: vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Literal::long(1)), upper_bound: Some(Literal::long(1))}], + key_metadata: vec![], + }] + }; + + let temp_dir = TempDir::new().unwrap(); + let path = temp_dir.path().join("manifest_list_v1.avro"); + let io = FileIOBuilder::new_fs_io().build().unwrap(); + let output_file = io.new_output(path.to_str().unwrap()).unwrap(); + + let mut writer = ManifestListWriter::v2(output_file, 1646658105718557341, 0, 1); + writer + .add_manifest_entries(expected_manifest_list.entries.clone().into_iter()) + .unwrap(); + writer.close().await.unwrap(); + + let bs = fs::read(path).unwrap(); + + let partition_types = HashMap::from([( + 1, + StructType::new(vec![Arc::new(NestedField::required( + 1, + "test", + Type::Primitive(PrimitiveType::Long), + ))]), + )]); + + let manifest_list = + ManifestList::parse_with_version(&bs, crate::spec::FormatVersion::V2, move |id| { + Ok(partition_types.get(&id).cloned()) + }) + .unwrap(); + assert_eq!(manifest_list, expected_manifest_list); + + temp_dir.close().unwrap(); + } +} diff --git a/libs/iceberg/src/spec/mod.rs b/libs/iceberg/src/spec/mod.rs new file mode 100644 index 0000000..199fc4a --- /dev/null +++ b/libs/iceberg/src/spec/mod.rs @@ -0,0 +1,40 @@ +// 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. + +//! Spec for Iceberg. + +mod datatypes; +mod manifest; +mod manifest_list; +mod partition; +mod schema; +mod snapshot; +mod sort; +mod table_metadata; +mod transform; +mod values; + +pub use datatypes::*; +pub use manifest::*; +pub use manifest_list::*; +pub use partition::*; +pub use schema::*; +pub use snapshot::*; +pub use sort::*; +pub use table_metadata::*; +pub use transform::*; +pub use values::*; diff --git a/libs/iceberg/src/spec/partition.rs b/libs/iceberg/src/spec/partition.rs new file mode 100644 index 0000000..9388820 --- /dev/null +++ b/libs/iceberg/src/spec/partition.rs @@ -0,0 +1,492 @@ +// 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. + +/*! + * Partitioning +*/ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use typed_builder::TypedBuilder; + +use crate::{Error, ErrorKind}; + +use super::{transform::Transform, NestedField, Schema, StructType}; + +/// Reference to [`PartitionSpec`]. +pub type PartitionSpecRef = Arc; +/// Partition fields capture the transform from table data to partition values. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, TypedBuilder)] +#[serde(rename_all = "kebab-case")] +pub struct PartitionField { + /// A source column id from the table’s schema + pub source_id: i32, + /// A partition field id that is used to identify a partition field and is unique within a partition spec. + /// In v2 table metadata, it is unique across all partition specs. + pub field_id: i32, + /// A partition name. + pub name: String, + /// A transform that is applied to the source column to produce a partition value. + pub transform: Transform, +} + +/// Partition spec that defines how to produce a tuple of partition values from a record. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default, Builder)] +#[serde(rename_all = "kebab-case")] +#[builder(setter(prefix = "with"))] +pub struct PartitionSpec { + /// Identifier for PartitionSpec + pub spec_id: i32, + /// Details of the partition spec + #[builder(setter(each(name = "with_partition_field")))] + pub fields: Vec, +} + +impl PartitionSpec { + /// Create partition spec builer + pub fn builder() -> PartitionSpecBuilder { + PartitionSpecBuilder::default() + } + + /// Returns if the partition spec is unpartitioned. + /// + /// A [`PartitionSpec`] is unpartitioned if it has no fields or all fields are [`Transform::Void`] transform. + pub fn is_unpartitioned(&self) -> bool { + self.fields.is_empty() + || self + .fields + .iter() + .all(|f| matches!(f.transform, Transform::Void)) + } + + /// Returns the partition type of this partition spec. + pub fn partition_type(&self, schema: &Schema) -> Result { + let mut fields = Vec::with_capacity(self.fields.len()); + for partition_field in &self.fields { + let field = schema + .field_by_id(partition_field.source_id) + .ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!( + "No column with source column id {} in schema {:?}", + partition_field.source_id, schema + ), + ) + })?; + let res_type = partition_field.transform.result_type(&field.field_type)?; + let field = + NestedField::optional(partition_field.field_id, &partition_field.name, res_type) + .into(); + fields.push(field); + } + Ok(StructType::new(fields)) + } +} + +/// Reference to [`UnboundPartitionSpec`]. +pub type UnboundPartitionSpecRef = Arc; +/// Unbound partition field can be built without a schema and later bound to a schema. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, TypedBuilder)] +#[serde(rename_all = "kebab-case")] +pub struct UnboundPartitionField { + /// A source column id from the table’s schema + pub source_id: i32, + /// A partition field id that is used to identify a partition field and is unique within a partition spec. + /// In v2 table metadata, it is unique across all partition specs. + #[builder(default, setter(strip_option))] + pub partition_id: Option, + /// A partition name. + pub name: String, + /// A transform that is applied to the source column to produce a partition value. + pub transform: Transform, +} + +/// Unbound partition spec can be built without a schema and later bound to a schema. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default, Builder)] +#[serde(rename_all = "kebab-case")] +#[builder(setter(prefix = "with"))] +pub struct UnboundPartitionSpec { + /// Identifier for PartitionSpec + #[builder(default, setter(strip_option))] + pub spec_id: Option, + /// Details of the partition spec + #[builder(setter(each(name = "with_unbound_partition_field")))] + pub fields: Vec, +} + +impl UnboundPartitionSpec { + /// Create unbound partition spec builer + pub fn builder() -> UnboundPartitionSpecBuilder { + UnboundPartitionSpecBuilder::default() + } +} + +#[cfg(test)] +mod tests { + use crate::spec::Type; + + use super::*; + + #[test] + fn test_partition_spec() { + let spec = r#" + { + "spec-id": 1, + "fields": [ { + "source-id": 4, + "field-id": 1000, + "name": "ts_day", + "transform": "day" + }, { + "source-id": 1, + "field-id": 1001, + "name": "id_bucket", + "transform": "bucket[16]" + }, { + "source-id": 2, + "field-id": 1002, + "name": "id_truncate", + "transform": "truncate[4]" + } ] + } + "#; + + let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap(); + assert_eq!(4, partition_spec.fields[0].source_id); + assert_eq!(1000, partition_spec.fields[0].field_id); + assert_eq!("ts_day", partition_spec.fields[0].name); + assert_eq!(Transform::Day, partition_spec.fields[0].transform); + + assert_eq!(1, partition_spec.fields[1].source_id); + assert_eq!(1001, partition_spec.fields[1].field_id); + assert_eq!("id_bucket", partition_spec.fields[1].name); + assert_eq!(Transform::Bucket(16), partition_spec.fields[1].transform); + + assert_eq!(2, partition_spec.fields[2].source_id); + assert_eq!(1002, partition_spec.fields[2].field_id); + assert_eq!("id_truncate", partition_spec.fields[2].name); + assert_eq!(Transform::Truncate(4), partition_spec.fields[2].transform); + } + + #[test] + fn test_is_unpartitioned() { + let partition_spec = PartitionSpec::builder() + .with_spec_id(1) + .with_fields(vec![]) + .build() + .unwrap(); + assert!( + partition_spec.is_unpartitioned(), + "Empty partition spec should be unpartitioned" + ); + + let partition_spec = PartitionSpec::builder() + .with_partition_field( + PartitionField::builder() + .source_id(1) + .field_id(1) + .name("id".to_string()) + .transform(Transform::Identity) + .build(), + ) + .with_partition_field( + PartitionField::builder() + .source_id(2) + .field_id(2) + .name("name".to_string()) + .transform(Transform::Void) + .build(), + ) + .with_spec_id(1) + .build() + .unwrap(); + assert!( + !partition_spec.is_unpartitioned(), + "Partition spec with one non void transform should not be unpartitioned" + ); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(1) + .with_partition_field( + PartitionField::builder() + .source_id(1) + .field_id(1) + .name("id".to_string()) + .transform(Transform::Void) + .build(), + ) + .with_partition_field( + PartitionField::builder() + .source_id(2) + .field_id(2) + .name("name".to_string()) + .transform(Transform::Void) + .build(), + ) + .build() + .unwrap(); + assert!( + partition_spec.is_unpartitioned(), + "Partition spec with all void field should be unpartitioned" + ); + } + + #[test] + fn test_unbound_partition_spec() { + let spec = r#" + { + "spec-id": 1, + "fields": [ { + "source-id": 4, + "partition-id": 1000, + "name": "ts_day", + "transform": "day" + }, { + "source-id": 1, + "partition-id": 1001, + "name": "id_bucket", + "transform": "bucket[16]" + }, { + "source-id": 2, + "partition-id": 1002, + "name": "id_truncate", + "transform": "truncate[4]" + } ] + } + "#; + + let partition_spec: UnboundPartitionSpec = serde_json::from_str(spec).unwrap(); + assert_eq!(Some(1), partition_spec.spec_id); + + assert_eq!(4, partition_spec.fields[0].source_id); + assert_eq!(Some(1000), partition_spec.fields[0].partition_id); + assert_eq!("ts_day", partition_spec.fields[0].name); + assert_eq!(Transform::Day, partition_spec.fields[0].transform); + + assert_eq!(1, partition_spec.fields[1].source_id); + assert_eq!(Some(1001), partition_spec.fields[1].partition_id); + assert_eq!("id_bucket", partition_spec.fields[1].name); + assert_eq!(Transform::Bucket(16), partition_spec.fields[1].transform); + + assert_eq!(2, partition_spec.fields[2].source_id); + assert_eq!(Some(1002), partition_spec.fields[2].partition_id); + assert_eq!("id_truncate", partition_spec.fields[2].name); + assert_eq!(Transform::Truncate(4), partition_spec.fields[2].transform); + + let spec = r#" + { + "fields": [ { + "source-id": 4, + "name": "ts_day", + "transform": "day" + } ] + } + "#; + let partition_spec: UnboundPartitionSpec = serde_json::from_str(spec).unwrap(); + assert_eq!(None, partition_spec.spec_id); + + assert_eq!(4, partition_spec.fields[0].source_id); + assert_eq!(None, partition_spec.fields[0].partition_id); + assert_eq!("ts_day", partition_spec.fields[0].name); + assert_eq!(Transform::Day, partition_spec.fields[0].transform); + } + + #[test] + fn test_partition_type() { + let spec = r#" + { + "spec-id": 1, + "fields": [ { + "source-id": 4, + "field-id": 1000, + "name": "ts_day", + "transform": "day" + }, { + "source-id": 1, + "field-id": 1001, + "name": "id_bucket", + "transform": "bucket[16]" + }, { + "source-id": 2, + "field-id": 1002, + "name": "id_truncate", + "transform": "truncate[4]" + } ] + } + "#; + + let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap(); + let schema = Schema::builder() + .with_fields(vec![ + NestedField::required(1, "id", Type::Primitive(crate::spec::PrimitiveType::Int)) + .into(), + NestedField::required( + 2, + "name", + Type::Primitive(crate::spec::PrimitiveType::String), + ) + .into(), + NestedField::required( + 3, + "ts", + Type::Primitive(crate::spec::PrimitiveType::Timestamp), + ) + .into(), + NestedField::required( + 4, + "ts_day", + Type::Primitive(crate::spec::PrimitiveType::Timestamp), + ) + .into(), + NestedField::required( + 5, + "id_bucket", + Type::Primitive(crate::spec::PrimitiveType::Int), + ) + .into(), + NestedField::required( + 6, + "id_truncate", + Type::Primitive(crate::spec::PrimitiveType::Int), + ) + .into(), + ]) + .build() + .unwrap(); + + let partition_type = partition_spec.partition_type(&schema).unwrap(); + assert_eq!(3, partition_type.fields().len()); + assert_eq!( + *partition_type.fields()[0], + NestedField::optional( + partition_spec.fields[0].field_id, + &partition_spec.fields[0].name, + Type::Primitive(crate::spec::PrimitiveType::Int) + ) + ); + assert_eq!( + *partition_type.fields()[1], + NestedField::optional( + partition_spec.fields[1].field_id, + &partition_spec.fields[1].name, + Type::Primitive(crate::spec::PrimitiveType::Int) + ) + ); + assert_eq!( + *partition_type.fields()[2], + NestedField::optional( + partition_spec.fields[2].field_id, + &partition_spec.fields[2].name, + Type::Primitive(crate::spec::PrimitiveType::String) + ) + ); + } + + #[test] + fn test_partition_empty() { + let spec = r#" + { + "spec-id": 1, + "fields": [] + } + "#; + + let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap(); + let schema = Schema::builder() + .with_fields(vec![ + NestedField::required(1, "id", Type::Primitive(crate::spec::PrimitiveType::Int)) + .into(), + NestedField::required( + 2, + "name", + Type::Primitive(crate::spec::PrimitiveType::String), + ) + .into(), + NestedField::required( + 3, + "ts", + Type::Primitive(crate::spec::PrimitiveType::Timestamp), + ) + .into(), + NestedField::required( + 4, + "ts_day", + Type::Primitive(crate::spec::PrimitiveType::Timestamp), + ) + .into(), + NestedField::required( + 5, + "id_bucket", + Type::Primitive(crate::spec::PrimitiveType::Int), + ) + .into(), + NestedField::required( + 6, + "id_truncate", + Type::Primitive(crate::spec::PrimitiveType::Int), + ) + .into(), + ]) + .build() + .unwrap(); + + let partition_type = partition_spec.partition_type(&schema).unwrap(); + assert_eq!(0, partition_type.fields().len()); + } + + #[test] + fn test_partition_error() { + let spec = r#" + { + "spec-id": 1, + "fields": [ { + "source-id": 4, + "field-id": 1000, + "name": "ts_day", + "transform": "day" + }, { + "source-id": 1, + "field-id": 1001, + "name": "id_bucket", + "transform": "bucket[16]" + }, { + "source-id": 2, + "field-id": 1002, + "name": "id_truncate", + "transform": "truncate[4]" + } ] + } + "#; + + let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap(); + let schema = Schema::builder() + .with_fields(vec![ + NestedField::required(1, "id", Type::Primitive(crate::spec::PrimitiveType::Int)) + .into(), + NestedField::required( + 2, + "name", + Type::Primitive(crate::spec::PrimitiveType::String), + ) + .into(), + ]) + .build() + .unwrap(); + + assert!(partition_spec.partition_type(&schema).is_err()); + } +} diff --git a/libs/iceberg/src/spec/schema.rs b/libs/iceberg/src/spec/schema.rs new file mode 100644 index 0000000..34e383f --- /dev/null +++ b/libs/iceberg/src/spec/schema.rs @@ -0,0 +1,1289 @@ +// 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. + +//! This module defines schema in iceberg. + +use crate::error::Result; +use crate::spec::datatypes::{ + ListType, MapType, NestedFieldRef, PrimitiveType, StructType, Type, LIST_FILED_NAME, + MAP_KEY_FIELD_NAME, MAP_VALUE_FIELD_NAME, +}; +use crate::{ensure_data_valid, Error, ErrorKind}; +use bimap::BiHashMap; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +use _serde::SchemaEnum; + +/// Type alias for schema id. +pub type SchemaId = i32; +/// Reference to [`Schema`]. +pub type SchemaRef = Arc; +const DEFAULT_SCHEMA_ID: SchemaId = 0; + +/// Defines schema in iceberg. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(try_from = "SchemaEnum", into = "SchemaEnum")] +pub struct Schema { + r#struct: StructType, + schema_id: SchemaId, + highest_field_id: i32, + identifier_field_ids: HashSet, + + alias_to_id: BiHashMap, + id_to_field: HashMap, + + name_to_id: HashMap, + id_to_name: HashMap, +} + +impl PartialEq for Schema { + fn eq(&self, other: &Self) -> bool { + self.r#struct == other.r#struct + && self.schema_id == other.schema_id + && self.identifier_field_ids == other.identifier_field_ids + } +} + +impl Eq for Schema {} + +/// Schema builder. +#[derive(Debug)] +pub struct SchemaBuilder { + schema_id: i32, + fields: Vec, + alias_to_id: BiHashMap, + identifier_field_ids: HashSet, +} + +impl SchemaBuilder { + /// Add fields to schema builder. + pub fn with_fields(mut self, fields: impl IntoIterator) -> Self { + self.fields.extend(fields); + self + } + + /// Set schema id. + pub fn with_schema_id(mut self, schema_id: i32) -> Self { + self.schema_id = schema_id; + self + } + + /// Set identifier field ids. + pub fn with_identifier_field_ids(mut self, ids: impl IntoIterator) -> Self { + self.identifier_field_ids.extend(ids); + self + } + + /// Set alias to filed id mapping. + pub fn with_alias(mut self, alias_to_id: BiHashMap) -> Self { + self.alias_to_id = alias_to_id; + self + } + + /// Builds the schema. + pub fn build(self) -> Result { + let highest_field_id = self.fields.iter().map(|f| f.id).max().unwrap_or(0); + + let r#struct = StructType::new(self.fields); + let id_to_field = index_by_id(&r#struct)?; + + Self::validate_identifier_ids( + &r#struct, + &id_to_field, + self.identifier_field_ids.iter().copied(), + )?; + + let (name_to_id, id_to_name) = { + let mut index = IndexByName::default(); + visit_struct(&r#struct, &mut index)?; + index.indexes() + }; + + Ok(Schema { + r#struct, + schema_id: self.schema_id, + highest_field_id, + identifier_field_ids: self.identifier_field_ids, + + alias_to_id: self.alias_to_id, + id_to_field, + + name_to_id, + id_to_name, + }) + } + + fn validate_identifier_ids( + r#struct: &StructType, + id_to_field: &HashMap, + identifier_field_ids: impl Iterator, + ) -> Result<()> { + let id_to_parent = index_parents(r#struct)?; + for identifier_field_id in identifier_field_ids { + let field = id_to_field.get(&identifier_field_id).ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!( + "Cannot add identifier field {identifier_field_id}: field does not exist" + ), + ) + })?; + ensure_data_valid!( + field.required, + "Cannot add identifier field: {} is an optional field", + field.name + ); + if let Type::Primitive(p) = field.field_type.as_ref() { + ensure_data_valid!( + !matches!(p, PrimitiveType::Double | PrimitiveType::Float), + "Cannot add identifier field {}: cannot be a float or double type", + field.name + ); + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Cannot add field {} as an identifier field: not a primitive type field", + field.name + ), + )); + } + + let mut cur_field_id = identifier_field_id; + while let Some(parent) = id_to_parent.get(&cur_field_id) { + let parent_field = id_to_field + .get(parent) + .expect("Field id should not disappear."); + ensure_data_valid!( + parent_field.field_type.is_struct(), + "Cannot add field {} as an identifier field: must not be nested in {:?}", + field.name, + parent_field + ); + ensure_data_valid!(parent_field.required, "Cannot add field {} as an identifier field: must not be nested in an optional field {}", field.name, parent_field); + cur_field_id = *parent; + } + } + + Ok(()) + } +} + +impl Schema { + /// Create a schema builder. + pub fn builder() -> SchemaBuilder { + SchemaBuilder { + schema_id: DEFAULT_SCHEMA_ID, + fields: vec![], + identifier_field_ids: HashSet::default(), + alias_to_id: BiHashMap::default(), + } + } + + /// Get field by field id. + pub fn field_by_id(&self, field_id: i32) -> Option<&NestedFieldRef> { + self.id_to_field.get(&field_id) + } + + /// Get field by field name. + /// + /// Both full name and short name could work here. + pub fn field_by_name(&self, field_name: &str) -> Option<&NestedFieldRef> { + self.name_to_id + .get(field_name) + .and_then(|id| self.field_by_id(*id)) + } + + /// Get field by alias. + pub fn field_by_alias(&self, alias: &str) -> Option<&NestedFieldRef> { + self.alias_to_id + .get_by_left(alias) + .and_then(|id| self.field_by_id(*id)) + } + + /// Returns [`highest_field_id`]. + #[inline] + pub fn highest_field_id(&self) -> i32 { + self.highest_field_id + } + + /// Returns [`schema_id`]. + #[inline] + pub fn schema_id(&self) -> i32 { + self.schema_id + } + + /// Returns [`r#struct`]. + #[inline] + pub fn as_struct(&self) -> &StructType { + &self.r#struct + } + + /// Get field id by full name. + pub fn field_id_by_name(&self, name: &str) -> Option { + self.name_to_id.get(name).copied() + } + + /// Get field id by full name. + pub fn name_by_field_id(&self, field_id: i32) -> Option<&str> { + self.id_to_name.get(&field_id).map(String::as_str) + } +} + +impl Display for Schema { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "table {{")?; + for field in self.as_struct().fields() { + writeln!(f, " {}", field)?; + } + writeln!(f, "}}") + } +} + +/// A post order schema visitor. +/// +/// For order of methods called, please refer to [`visit_schema`]. +pub trait SchemaVisitor { + /// Return type of this visitor. + type T; + + /// Called before struct field. + fn before_struct_field(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called after struct field. + fn after_struct_field(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called before list field. + fn before_list_element(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called after list field. + fn after_list_element(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called before map key field. + fn before_map_key(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called after map key field. + fn after_map_key(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called before map value field. + fn before_map_value(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + /// Called after map value field. + fn after_map_value(&mut self, _field: &NestedFieldRef) -> Result<()> { + Ok(()) + } + + /// Called after schema's type visited. + fn schema(&mut self, schema: &Schema, value: Self::T) -> Result; + /// Called after struct's field type visited. + fn field(&mut self, field: &NestedFieldRef, value: Self::T) -> Result; + /// Called after struct's fields visited. + fn r#struct(&mut self, r#struct: &StructType, results: Vec) -> Result; + /// Called after list fields visited. + fn list(&mut self, list: &ListType, value: Self::T) -> Result; + /// Called after map's key and value fields visited. + fn map(&mut self, map: &MapType, key_value: Self::T, value: Self::T) -> Result; + /// Called when see a primitive type. + fn primitive(&mut self, p: &PrimitiveType) -> Result; +} + +/// Visiting a type in post order. +pub fn visit_type(r#type: &Type, visitor: &mut V) -> Result { + match r#type { + Type::Primitive(p) => visitor.primitive(p), + Type::List(list) => { + visitor.before_list_element(&list.element_field)?; + let value = visit_type(&list.element_field.field_type, visitor)?; + visitor.after_list_element(&list.element_field)?; + visitor.list(list, value) + } + Type::Map(map) => { + let key_result = { + visitor.before_map_key(&map.key_field)?; + let ret = visit_type(&map.key_field.field_type, visitor)?; + visitor.after_map_key(&map.key_field)?; + ret + }; + + let value_result = { + visitor.before_map_value(&map.value_field)?; + let ret = visit_type(&map.value_field.field_type, visitor)?; + visitor.after_map_value(&map.value_field)?; + ret + }; + + visitor.map(map, key_result, value_result) + } + Type::Struct(s) => visit_struct(s, visitor), + } +} + +/// Visit struct type in post order. +pub fn visit_struct(s: &StructType, visitor: &mut V) -> Result { + let mut results = Vec::with_capacity(s.fields().len()); + for field in s.fields() { + visitor.before_struct_field(field)?; + let result = visit_type(&field.field_type, visitor)?; + visitor.after_struct_field(field)?; + let result = visitor.field(field, result)?; + results.push(result); + } + + visitor.r#struct(s, results) +} + +/// Visit schema in post order. +pub fn visit_schema(schema: &Schema, visitor: &mut V) -> Result { + let result = visit_struct(&schema.r#struct, visitor)?; + visitor.schema(schema, result) +} + +/// Creates an field id to field map. +pub fn index_by_id(r#struct: &StructType) -> Result> { + struct IndexById(HashMap); + + impl SchemaVisitor for IndexById { + type T = (); + + fn schema(&mut self, _schema: &Schema, _value: ()) -> Result<()> { + Ok(()) + } + + fn field(&mut self, field: &NestedFieldRef, _value: ()) -> Result<()> { + self.0.insert(field.id, field.clone()); + Ok(()) + } + + fn r#struct(&mut self, _struct: &StructType, _results: Vec) -> Result { + Ok(()) + } + + fn list(&mut self, list: &ListType, _value: Self::T) -> Result { + self.0 + .insert(list.element_field.id, list.element_field.clone()); + Ok(()) + } + + fn map(&mut self, map: &MapType, _key_value: Self::T, _value: Self::T) -> Result { + self.0.insert(map.key_field.id, map.key_field.clone()); + self.0.insert(map.value_field.id, map.value_field.clone()); + Ok(()) + } + + fn primitive(&mut self, _: &PrimitiveType) -> Result { + Ok(()) + } + } + + let mut index = IndexById(HashMap::new()); + visit_struct(r#struct, &mut index)?; + Ok(index.0) +} + +/// Creates a field id to parent field id map. +pub fn index_parents(r#struct: &StructType) -> Result> { + struct IndexByParent { + parents: Vec, + result: HashMap, + } + + impl SchemaVisitor for IndexByParent { + type T = (); + + fn before_struct_field(&mut self, field: &NestedFieldRef) -> Result<()> { + self.parents.push(field.id); + Ok(()) + } + + fn after_struct_field(&mut self, _field: &NestedFieldRef) -> Result<()> { + self.parents.pop(); + Ok(()) + } + + fn before_list_element(&mut self, field: &NestedFieldRef) -> Result<()> { + self.parents.push(field.id); + Ok(()) + } + + fn after_list_element(&mut self, _field: &NestedFieldRef) -> Result<()> { + self.parents.pop(); + Ok(()) + } + + fn before_map_key(&mut self, field: &NestedFieldRef) -> Result<()> { + self.parents.push(field.id); + Ok(()) + } + + fn after_map_key(&mut self, _field: &NestedFieldRef) -> Result<()> { + self.parents.pop(); + Ok(()) + } + + fn before_map_value(&mut self, field: &NestedFieldRef) -> Result<()> { + self.parents.push(field.id); + Ok(()) + } + + fn after_map_value(&mut self, _field: &NestedFieldRef) -> Result<()> { + self.parents.pop(); + Ok(()) + } + + fn schema(&mut self, _schema: &Schema, _value: Self::T) -> Result { + Ok(()) + } + + fn field(&mut self, field: &NestedFieldRef, _value: Self::T) -> Result { + if let Some(parent) = self.parents.last().copied() { + self.result.insert(field.id, parent); + } + Ok(()) + } + + fn r#struct(&mut self, _struct: &StructType, _results: Vec) -> Result { + Ok(()) + } + + fn list(&mut self, _list: &ListType, _value: Self::T) -> Result { + Ok(()) + } + + fn map(&mut self, _map: &MapType, _key_value: Self::T, _value: Self::T) -> Result { + Ok(()) + } + + fn primitive(&mut self, _p: &PrimitiveType) -> Result { + Ok(()) + } + } + + let mut index = IndexByParent { + parents: vec![], + result: HashMap::new(), + }; + visit_struct(r#struct, &mut index)?; + Ok(index.result) +} + +#[derive(Default)] +struct IndexByName { + // Maybe radix tree is better here? + name_to_id: HashMap, + short_name_to_id: HashMap, + + field_names: Vec, + short_field_names: Vec, +} + +impl IndexByName { + fn add_field(&mut self, name: &str, field_id: i32) -> Result<()> { + let full_name = self + .field_names + .iter() + .map(String::as_str) + .chain(vec![name]) + .join("."); + if let Some(existing_field_id) = self.name_to_id.get(full_name.as_str()) { + return Err(Error::new(ErrorKind::DataInvalid, format!("Invalid schema: multiple fields for name {full_name}: {field_id} and {existing_field_id}"))); + } else { + self.name_to_id.insert(full_name, field_id); + } + + let full_short_name = self + .short_field_names + .iter() + .map(String::as_str) + .chain(vec![name]) + .join("."); + self.short_name_to_id + .entry(full_short_name) + .or_insert_with(|| field_id); + Ok(()) + } + + /// Returns two indexes: full name to field id, and id to full name. + /// + /// In the first index, short names are returned. + /// In second index, short names are not returned. + pub fn indexes(mut self) -> (HashMap, HashMap) { + self.short_name_to_id.reserve(self.name_to_id.len()); + for (name, id) in &self.name_to_id { + self.short_name_to_id.insert(name.clone(), *id); + } + + let id_to_name = self.name_to_id.into_iter().map(|e| (e.1, e.0)).collect(); + (self.short_name_to_id, id_to_name) + } +} + +impl SchemaVisitor for IndexByName { + type T = (); + + fn before_struct_field(&mut self, field: &NestedFieldRef) -> Result<()> { + self.field_names.push(field.name.to_string()); + self.short_field_names.push(field.name.to_string()); + Ok(()) + } + + fn after_struct_field(&mut self, _field: &NestedFieldRef) -> Result<()> { + self.field_names.pop(); + self.short_field_names.pop(); + Ok(()) + } + + fn before_list_element(&mut self, field: &NestedFieldRef) -> Result<()> { + self.field_names.push(field.name.clone()); + if !field.field_type.is_struct() { + self.short_field_names.push(field.name.to_string()); + } + + Ok(()) + } + + fn after_list_element(&mut self, field: &NestedFieldRef) -> Result<()> { + self.field_names.pop(); + if !field.field_type.is_struct() { + self.short_field_names.pop(); + } + + Ok(()) + } + + fn before_map_key(&mut self, field: &NestedFieldRef) -> Result<()> { + self.before_struct_field(field) + } + + fn after_map_key(&mut self, field: &NestedFieldRef) -> Result<()> { + self.after_struct_field(field) + } + + fn before_map_value(&mut self, field: &NestedFieldRef) -> Result<()> { + self.field_names.push(field.name.to_string()); + if !field.field_type.is_struct() { + self.short_field_names.push(field.name.to_string()); + } + Ok(()) + } + + fn after_map_value(&mut self, field: &NestedFieldRef) -> Result<()> { + self.field_names.pop(); + if !field.field_type.is_struct() { + self.short_field_names.pop(); + } + + Ok(()) + } + + fn schema(&mut self, _schema: &Schema, _value: Self::T) -> Result { + Ok(()) + } + + fn field(&mut self, field: &NestedFieldRef, _value: Self::T) -> Result { + self.add_field(field.name.as_str(), field.id) + } + + fn r#struct(&mut self, _struct: &StructType, _results: Vec) -> Result { + Ok(()) + } + + fn list(&mut self, list: &ListType, _value: Self::T) -> Result { + self.add_field(LIST_FILED_NAME, list.element_field.id) + } + + fn map(&mut self, map: &MapType, _key_value: Self::T, _value: Self::T) -> Result { + self.add_field(MAP_KEY_FIELD_NAME, map.key_field.id)?; + self.add_field(MAP_VALUE_FIELD_NAME, map.value_field.id) + } + + fn primitive(&mut self, _p: &PrimitiveType) -> Result { + Ok(()) + } +} + +pub(super) mod _serde { + /// This is a helper module that defines types to help with serialization/deserialization. + /// For deserialization the input first gets read into either the [SchemaV1] or [SchemaV2] struct + /// and then converted into the [Schema] struct. Serialization works the other way around. + /// [SchemaV1] and [SchemaV2] are internal struct that are only used for serialization and deserialization. + use serde::{Deserialize, Serialize}; + + use crate::{spec::StructType, Error, Result}; + + use super::{Schema, DEFAULT_SCHEMA_ID}; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(untagged)] + /// Enum for Schema serialization/deserializaion + pub(super) enum SchemaEnum { + V2(SchemaV2), + V1(SchemaV1), + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v2 schema for serialization/deserialization + pub(crate) struct SchemaV2 { + pub schema_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier_field_ids: Option>, + #[serde(flatten)] + pub fields: StructType, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v1 schema for serialization/deserialization + pub(crate) struct SchemaV1 { + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier_field_ids: Option>, + #[serde(flatten)] + pub fields: StructType, + } + + /// Helper to serialize/deserializa Schema + impl TryFrom for Schema { + type Error = Error; + fn try_from(value: SchemaEnum) -> Result { + match value { + SchemaEnum::V2(value) => value.try_into(), + SchemaEnum::V1(value) => value.try_into(), + } + } + } + + impl From for SchemaEnum { + fn from(value: Schema) -> Self { + SchemaEnum::V2(value.into()) + } + } + + impl TryFrom for Schema { + type Error = Error; + fn try_from(value: SchemaV2) -> Result { + Schema::builder() + .with_schema_id(value.schema_id) + .with_fields(value.fields.fields().iter().cloned()) + .with_identifier_field_ids(value.identifier_field_ids.unwrap_or_default()) + .build() + } + } + + impl TryFrom for Schema { + type Error = Error; + fn try_from(value: SchemaV1) -> Result { + Schema::builder() + .with_schema_id(value.schema_id.unwrap_or(DEFAULT_SCHEMA_ID)) + .with_fields(value.fields.fields().iter().cloned()) + .with_identifier_field_ids(value.identifier_field_ids.unwrap_or_default()) + .build() + } + } + + impl From for SchemaV2 { + fn from(value: Schema) -> Self { + SchemaV2 { + schema_id: value.schema_id, + identifier_field_ids: if value.identifier_field_ids.is_empty() { + None + } else { + Some(value.identifier_field_ids.into_iter().collect()) + }, + fields: value.r#struct, + } + } + } + + impl From for SchemaV1 { + fn from(value: Schema) -> Self { + SchemaV1 { + schema_id: Some(value.schema_id), + identifier_field_ids: if value.identifier_field_ids.is_empty() { + None + } else { + Some(value.identifier_field_ids.into_iter().collect()) + }, + fields: value.r#struct, + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::spec::datatypes::Type::{List, Map, Primitive, Struct}; + use crate::spec::datatypes::{ + ListType, MapType, NestedField, NestedFieldRef, PrimitiveType, StructType, Type, + }; + use crate::spec::schema::Schema; + use crate::spec::schema::_serde::{SchemaEnum, SchemaV1, SchemaV2}; + use std::collections::HashMap; + + use super::DEFAULT_SCHEMA_ID; + + fn check_schema_serde(json: &str, expected_type: Schema, _expected_enum: SchemaEnum) { + let desered_type: Schema = serde_json::from_str(json).unwrap(); + assert_eq!(desered_type, expected_type); + assert!(matches!(desered_type.clone(), _expected_enum)); + + let sered_json = serde_json::to_string(&expected_type).unwrap(); + let parsed_json_value = serde_json::from_str::(&sered_json).unwrap(); + + assert_eq!(parsed_json_value, desered_type); + } + + #[test] + fn test_serde_with_schema_id() { + let (schema, record) = table_schema_simple(); + + let x: SchemaV2 = serde_json::from_str(record).unwrap(); + check_schema_serde(record, schema, SchemaEnum::V2(x)); + } + + #[test] + fn test_serde_without_schema_id() { + let (mut schema, record) = table_schema_simple(); + // we remove the ""schema-id": 1," string from example + let new_record = record.replace("\"schema-id\":1,", ""); + // By default schema_id field is set to DEFAULT_SCHEMA_ID when no value is set in json + schema.schema_id = DEFAULT_SCHEMA_ID; + + let x: SchemaV1 = serde_json::from_str(new_record.as_str()).unwrap(); + check_schema_serde(&new_record, schema, SchemaEnum::V1(x)); + } + + #[test] + fn test_construct_schema() { + let field1: NestedFieldRef = + NestedField::required(1, "f1", Type::Primitive(PrimitiveType::Boolean)).into(); + let field2: NestedFieldRef = + NestedField::optional(2, "f2", Type::Primitive(PrimitiveType::Int)).into(); + + let schema = Schema::builder() + .with_fields(vec![field1.clone()]) + .with_fields(vec![field2.clone()]) + .with_schema_id(3) + .build() + .unwrap(); + + assert_eq!(3, schema.schema_id()); + assert_eq!(2, schema.highest_field_id()); + assert_eq!(Some(&field1), schema.field_by_id(1)); + assert_eq!(Some(&field2), schema.field_by_id(2)); + assert_eq!(None, schema.field_by_id(3)); + } + + #[test] + fn schema() { + let record = r#" + { + "type": "struct", + "schema-id": 1, + "fields": [ { + "id": 1, + "name": "id", + "required": true, + "type": "uuid" + }, { + "id": 2, + "name": "data", + "required": false, + "type": "int" + } ] + } + "#; + + let result: SchemaV2 = serde_json::from_str(record).unwrap(); + assert_eq!(1, result.schema_id); + assert_eq!( + Box::new(Type::Primitive(PrimitiveType::Uuid)), + result.fields[0].field_type + ); + assert_eq!(1, result.fields[0].id); + assert!(result.fields[0].required); + + assert_eq!( + Box::new(Type::Primitive(PrimitiveType::Int)), + result.fields[1].field_type + ); + assert_eq!(2, result.fields[1].id); + assert!(!result.fields[1].required); + } + + fn table_schema_simple<'a>() -> (Schema, &'a str) { + let schema = Schema::builder() + .with_schema_id(1) + .with_identifier_field_ids(vec![2]) + .with_fields(vec![ + NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String)).into(), + NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean)).into(), + ]) + .build() + .unwrap(); + let record = r#"{ + "type":"struct", + "schema-id":1, + "fields":[ + { + "id":1, + "name":"foo", + "required":false, + "type":"string" + }, + { + "id":2, + "name":"bar", + "required":true, + "type":"int" + }, + { + "id":3, + "name":"baz", + "required":false, + "type":"boolean" + } + ], + "identifier-field-ids":[2] + }"#; + (schema, record) + } + + fn table_schema_nested() -> Schema { + Schema::builder() + .with_schema_id(1) + .with_identifier_field_ids(vec![2]) + .with_fields(vec![ + NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String)).into(), + NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean)).into(), + NestedField::required( + 4, + "qux", + Type::List(ListType { + element_field: NestedField::list_element( + 5, + Type::Primitive(PrimitiveType::String), + true, + ) + .into(), + }), + ) + .into(), + NestedField::required( + 6, + "quux", + Type::Map(MapType { + key_field: NestedField::map_key_element( + 7, + Type::Primitive(PrimitiveType::String), + ) + .into(), + value_field: NestedField::map_value_element( + 8, + Type::Map(MapType { + key_field: NestedField::map_key_element( + 9, + Type::Primitive(PrimitiveType::String), + ) + .into(), + value_field: NestedField::map_value_element( + 10, + Type::Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + true, + ) + .into(), + }), + ) + .into(), + NestedField::required( + 11, + "location", + Type::List(ListType { + element_field: NestedField::list_element( + 12, + Type::Struct(StructType::new(vec![ + NestedField::optional( + 13, + "latitude", + Type::Primitive(PrimitiveType::Float), + ) + .into(), + NestedField::optional( + 14, + "longitude", + Type::Primitive(PrimitiveType::Float), + ) + .into(), + ])), + true, + ) + .into(), + }), + ) + .into(), + NestedField::optional( + 15, + "person", + Type::Struct(StructType::new(vec![ + NestedField::optional(16, "name", Type::Primitive(PrimitiveType::String)) + .into(), + NestedField::required(17, "age", Type::Primitive(PrimitiveType::Int)) + .into(), + ])), + ) + .into(), + ]) + .build() + .unwrap() + } + + #[test] + fn test_schema_display() { + let expected_str = " +table { + 1: foo: optional string\x20 + 2: bar: required int\x20 + 3: baz: optional boolean\x20 +} +"; + + assert_eq!(expected_str, format!("\n{}", table_schema_simple().0)); + } + + #[test] + fn test_schema_build_failed_on_duplicate_names() { + let ret = Schema::builder() + .with_schema_id(1) + .with_identifier_field_ids(vec![1]) + .with_fields(vec![ + NestedField::required(1, "foo", Primitive(PrimitiveType::String)).into(), + NestedField::required(2, "bar", Primitive(PrimitiveType::Int)).into(), + NestedField::optional(3, "baz", Primitive(PrimitiveType::Boolean)).into(), + NestedField::optional(4, "baz", Primitive(PrimitiveType::Boolean)).into(), + ]) + .build(); + + assert!(ret + .unwrap_err() + .message() + .contains("Invalid schema: multiple fields for name baz")); + } + + #[test] + fn test_schema_index_by_name() { + let expected_name_to_id = HashMap::from( + [ + ("foo", 1), + ("bar", 2), + ("baz", 3), + ("qux", 4), + ("qux.element", 5), + ("quux", 6), + ("quux.key", 7), + ("quux.value", 8), + ("quux.value.key", 9), + ("quux.value.value", 10), + ("location", 11), + ("location.element", 12), + ("location.element.latitude", 13), + ("location.element.longitude", 14), + ("location.latitude", 13), + ("location.longitude", 14), + ("person", 15), + ("person.name", 16), + ("person.age", 17), + ] + .map(|e| (e.0.to_string(), e.1)), + ); + + let schema = table_schema_nested(); + assert_eq!(&expected_name_to_id, &schema.name_to_id); + } + + #[test] + fn test_schema_find_column_name() { + let expected_column_name = HashMap::from([ + (1, "foo"), + (2, "bar"), + (3, "baz"), + (4, "qux"), + (5, "qux.element"), + (6, "quux"), + (7, "quux.key"), + (8, "quux.value"), + (9, "quux.value.key"), + (10, "quux.value.value"), + (11, "location"), + (12, "location.element"), + (13, "location.element.latitude"), + (14, "location.element.longitude"), + ]); + + let schema = table_schema_nested(); + for (id, name) in expected_column_name { + assert_eq!( + Some(name), + schema.name_by_field_id(id), + "Column name for field id {} not match.", + id + ); + } + } + + #[test] + fn test_schema_find_column_name_not_found() { + let schema = table_schema_nested(); + + assert!(schema.name_by_field_id(99).is_none()); + } + + #[test] + fn test_schema_find_column_name_by_id_simple() { + let expected_id_to_name = HashMap::from([(1, "foo"), (2, "bar"), (3, "baz")]); + + let schema = table_schema_simple().0; + + for (id, name) in expected_id_to_name { + assert_eq!( + Some(name), + schema.name_by_field_id(id), + "Column name for field id {} not match.", + id + ); + } + } + + #[test] + fn test_schema_find_simple() { + let schema = table_schema_simple().0; + + assert_eq!( + Some(schema.r#struct.fields()[0].clone()), + schema.field_by_id(1).cloned() + ); + assert_eq!( + Some(schema.r#struct.fields()[1].clone()), + schema.field_by_id(2).cloned() + ); + assert_eq!( + Some(schema.r#struct.fields()[2].clone()), + schema.field_by_id(3).cloned() + ); + + assert!(schema.field_by_id(4).is_none()); + assert!(schema.field_by_name("non exist").is_none()); + } + + #[test] + fn test_schema_find_nested() { + let expected_id_to_field: HashMap = HashMap::from([ + ( + 1, + NestedField::optional(1, "foo", Primitive(PrimitiveType::String)), + ), + ( + 2, + NestedField::required(2, "bar", Primitive(PrimitiveType::Int)), + ), + ( + 3, + NestedField::optional(3, "baz", Primitive(PrimitiveType::Boolean)), + ), + ( + 4, + NestedField::required( + 4, + "qux", + Type::List(ListType { + element_field: NestedField::list_element( + 5, + Type::Primitive(PrimitiveType::String), + true, + ) + .into(), + }), + ), + ), + ( + 5, + NestedField::required(5, "element", Primitive(PrimitiveType::String)), + ), + ( + 6, + NestedField::required( + 6, + "quux", + Map(MapType { + key_field: NestedField::map_key_element( + 7, + Primitive(PrimitiveType::String), + ) + .into(), + value_field: NestedField::map_value_element( + 8, + Map(MapType { + key_field: NestedField::map_key_element( + 9, + Primitive(PrimitiveType::String), + ) + .into(), + value_field: NestedField::map_value_element( + 10, + Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + true, + ) + .into(), + }), + ), + ), + ( + 7, + NestedField::required(7, "key", Primitive(PrimitiveType::String)), + ), + ( + 8, + NestedField::required( + 8, + "value", + Map(MapType { + key_field: NestedField::map_key_element( + 9, + Primitive(PrimitiveType::String), + ) + .into(), + value_field: NestedField::map_value_element( + 10, + Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + ), + ), + ( + 9, + NestedField::required(9, "key", Primitive(PrimitiveType::String)), + ), + ( + 10, + NestedField::required(10, "value", Primitive(PrimitiveType::Int)), + ), + ( + 11, + NestedField::required( + 11, + "location", + List(ListType { + element_field: NestedField::list_element( + 12, + Struct(StructType::new(vec![ + NestedField::optional( + 13, + "latitude", + Primitive(PrimitiveType::Float), + ) + .into(), + NestedField::optional( + 14, + "longitude", + Primitive(PrimitiveType::Float), + ) + .into(), + ])), + true, + ) + .into(), + }), + ), + ), + ( + 12, + NestedField::list_element( + 12, + Struct(StructType::new(vec![ + NestedField::optional(13, "latitude", Primitive(PrimitiveType::Float)) + .into(), + NestedField::optional(14, "longitude", Primitive(PrimitiveType::Float)) + .into(), + ])), + true, + ), + ), + ( + 13, + NestedField::optional(13, "latitude", Primitive(PrimitiveType::Float)), + ), + ( + 14, + NestedField::optional(14, "longitude", Primitive(PrimitiveType::Float)), + ), + ( + 15, + NestedField::optional( + 15, + "person", + Type::Struct(StructType::new(vec![ + NestedField::optional(16, "name", Type::Primitive(PrimitiveType::String)) + .into(), + NestedField::required(17, "age", Type::Primitive(PrimitiveType::Int)) + .into(), + ])), + ), + ), + ( + 16, + NestedField::optional(16, "name", Type::Primitive(PrimitiveType::String)), + ), + ( + 17, + NestedField::required(17, "age", Type::Primitive(PrimitiveType::Int)), + ), + ]); + + let schema = table_schema_nested(); + for (id, field) in expected_id_to_field { + assert_eq!( + Some(&field), + schema.field_by_id(id).map(|f| f.as_ref()), + "Field for {} not match.", + id + ); + } + } +} diff --git a/libs/iceberg/src/spec/snapshot.rs b/libs/iceberg/src/spec/snapshot.rs new file mode 100644 index 0000000..3b4558b --- /dev/null +++ b/libs/iceberg/src/spec/snapshot.rs @@ -0,0 +1,404 @@ +// 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. + +/*! + * Snapshots +*/ +use crate::error::Result; +use chrono::{DateTime, TimeZone, Utc}; +use futures::AsyncReadExt; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use typed_builder::TypedBuilder; + +use super::table_metadata::SnapshotLog; +use crate::io::FileIO; +use crate::spec::{ManifestList, SchemaId, SchemaRef, StructType, TableMetadata}; +use crate::{Error, ErrorKind}; +use _serde::SnapshotV2; + +/// Reference to [`Snapshot`]. +pub type SnapshotRef = Arc; +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +/// The operation field is used by some operations, like snapshot expiration, to skip processing certain snapshots. +pub enum Operation { + /// Only data files were added and no files were removed. + Append, + /// Data and delete files were added and removed without changing table data; + /// i.e., compaction, changing the data file format, or relocating data files. + Replace, + /// Data and delete files were added and removed in a logical overwrite operation. + Overwrite, + /// Data files were removed and their contents logically deleted and/or delete files were added to delete rows. + Delete, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +/// Summarises the changes in the snapshot. +pub struct Summary { + /// The type of operation in the snapshot + pub operation: Operation, + /// Other summary data. + #[serde(flatten)] + pub other: HashMap, +} + +impl Default for Operation { + fn default() -> Operation { + Self::Append + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TypedBuilder)] +#[serde(from = "SnapshotV2", into = "SnapshotV2")] +#[builder(field_defaults(setter(prefix = "with_")))] +/// A snapshot represents the state of a table at some time and is used to access the complete set of data files in the table. +pub struct Snapshot { + /// A unique long ID + snapshot_id: i64, + /// The snapshot ID of the snapshot’s parent. + /// Omitted for any snapshot with no parent + #[builder(default = None)] + parent_snapshot_id: Option, + /// A monotonically increasing long that tracks the order of + /// changes to a table. + sequence_number: i64, + /// A timestamp when the snapshot was created, used for garbage + /// collection and table inspection + timestamp_ms: i64, + /// The location of a manifest list for this snapshot that + /// tracks manifest files with additional metadata. + /// Currently we only support manifest list file, and manifest files are not supported. + #[builder(setter(into))] + manifest_list: String, + /// A string map that summarizes the snapshot changes, including operation. + summary: Summary, + /// ID of the table’s current schema when the snapshot was created. + #[builder(setter(strip_option), default = None)] + schema_id: Option, +} + +impl Snapshot { + /// Get the id of the snapshot + #[inline] + pub fn snapshot_id(&self) -> i64 { + self.snapshot_id + } + + /// Get parent snapshot id. + #[inline] + pub fn parent_snapshot_id(&self) -> Option { + self.parent_snapshot_id + } + + /// Get sequence_number of the snapshot. Is 0 for Iceberg V1 tables. + #[inline] + pub fn sequence_number(&self) -> i64 { + self.sequence_number + } + /// Get location of manifest_list file + #[inline] + pub fn manifest_list(&self) -> &str { + &self.manifest_list + } + + /// Get summary of the snapshot + #[inline] + pub fn summary(&self) -> &Summary { + &self.summary + } + /// Get the timestamp of when the snapshot was created + #[inline] + pub fn timestamp(&self) -> DateTime { + Utc.timestamp_millis_opt(self.timestamp_ms).unwrap() + } + + /// Get the schema id of this snapshot. + #[inline] + pub fn schema_id(&self) -> Option { + self.schema_id + } + + /// Get the schema of this snapshot. + pub fn schema(&self, table_metadata: &TableMetadata) -> Result { + Ok(match self.schema_id() { + Some(schema_id) => table_metadata + .schema_by_id(schema_id) + .ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Schema with id {} not found", schema_id), + ) + })? + .clone(), + None => table_metadata.current_schema().clone(), + }) + } + + /// Get parent snapshot. + #[cfg(test)] + pub(crate) fn parent_snapshot(&self, table_metadata: &TableMetadata) -> Option { + match self.parent_snapshot_id { + Some(id) => table_metadata.snapshot_by_id(id).cloned(), + None => None, + } + } + + /// Load manifest list. + pub async fn load_manifest_list( + &self, + file_io: &FileIO, + table_metadata: &TableMetadata, + ) -> Result { + let mut manifest_list_content = Vec::new(); + file_io + .new_input(&self.manifest_list)? + .reader() + .await? + .read_to_end(&mut manifest_list_content) + .await?; + + let schema = self.schema(table_metadata)?; + + let partition_type_provider = |partition_spec_id: i32| -> Result> { + table_metadata + .partition_spec_by_id(partition_spec_id) + .map(|partition_spec| partition_spec.partition_type(&schema)) + .transpose() + }; + + ManifestList::parse_with_version( + &manifest_list_content, + table_metadata.format_version(), + partition_type_provider, + ) + } + + pub(crate) fn log(&self) -> SnapshotLog { + SnapshotLog { + timestamp_ms: self.timestamp_ms, + snapshot_id: self.snapshot_id, + } + } +} + +pub(super) mod _serde { + /// This is a helper module that defines types to help with serialization/deserialization. + /// For deserialization the input first gets read into either the [SnapshotV1] or [SnapshotV2] struct + /// and then converted into the [Snapshot] struct. Serialization works the other way around. + /// [SnapshotV1] and [SnapshotV2] are internal struct that are only used for serialization and deserialization. + use std::collections::HashMap; + + use serde::{Deserialize, Serialize}; + + use crate::spec::SchemaId; + use crate::Error; + + use super::{Operation, Snapshot, Summary}; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v2 snapshot for serialization/deserialization + pub(crate) struct SnapshotV2 { + pub snapshot_id: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_snapshot_id: Option, + pub sequence_number: i64, + pub timestamp_ms: i64, + pub manifest_list: String, + pub summary: Summary, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v1 snapshot for serialization/deserialization + pub(crate) struct SnapshotV1 { + pub snapshot_id: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_snapshot_id: Option, + pub timestamp_ms: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifest_list: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifests: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, + } + + impl From for Snapshot { + fn from(v2: SnapshotV2) -> Self { + Snapshot { + snapshot_id: v2.snapshot_id, + parent_snapshot_id: v2.parent_snapshot_id, + sequence_number: v2.sequence_number, + timestamp_ms: v2.timestamp_ms, + manifest_list: v2.manifest_list, + summary: v2.summary, + schema_id: v2.schema_id, + } + } + } + + impl From for SnapshotV2 { + fn from(v2: Snapshot) -> Self { + SnapshotV2 { + snapshot_id: v2.snapshot_id, + parent_snapshot_id: v2.parent_snapshot_id, + sequence_number: v2.sequence_number, + timestamp_ms: v2.timestamp_ms, + manifest_list: v2.manifest_list, + summary: v2.summary, + schema_id: v2.schema_id, + } + } + } + + impl TryFrom for Snapshot { + type Error = Error; + + fn try_from(v1: SnapshotV1) -> Result { + Ok(Snapshot { + snapshot_id: v1.snapshot_id, + parent_snapshot_id: v1.parent_snapshot_id, + sequence_number: 0, + timestamp_ms: v1.timestamp_ms, + manifest_list: match (v1.manifest_list, v1.manifests) { + (Some(file), None) => file, + (Some(_), Some(_)) => "Invalid v1 snapshot, when manifest list provided, manifest files should be omitted".to_string(), + (None, _) => "Unsupported v1 snapshot, only manifest list is supported".to_string() + }, + summary: v1.summary.unwrap_or(Summary { + operation: Operation::default(), + other: HashMap::new(), + }), + schema_id: v1.schema_id, + }) + } + } + + impl From for SnapshotV1 { + fn from(v2: Snapshot) -> Self { + SnapshotV1 { + snapshot_id: v2.snapshot_id, + parent_snapshot_id: v2.parent_snapshot_id, + timestamp_ms: v2.timestamp_ms, + manifest_list: Some(v2.manifest_list), + summary: Some(v2.summary), + schema_id: v2.schema_id, + manifests: None, + } + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "kebab-case")] +/// Iceberg tables keep track of branches and tags using snapshot references. +pub struct SnapshotReference { + /// A reference’s snapshot ID. The tagged snapshot or latest snapshot of a branch. + pub snapshot_id: i64, + #[serde(flatten)] + /// Snapshot retention policy + pub retention: SnapshotRetention, +} + +impl SnapshotReference { + /// Create new snapshot reference + pub fn new(snapshot_id: i64, retention: SnapshotRetention) -> Self { + SnapshotReference { + snapshot_id, + retention, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase", tag = "type")] +/// The snapshot expiration procedure removes snapshots from table metadata and applies the table’s retention policy. +pub enum SnapshotRetention { + #[serde(rename_all = "kebab-case")] + /// Branches are mutable named references that can be updated by committing a new snapshot as + /// the branch’s referenced snapshot using the Commit Conflict Resolution and Retry procedures. + Branch { + /// A positive number for the minimum number of snapshots to keep in a branch while expiring snapshots. + /// Defaults to table property history.expire.min-snapshots-to-keep. + #[serde(skip_serializing_if = "Option::is_none")] + min_snapshots_to_keep: Option, + /// A positive number for the max age of snapshots to keep when expiring, including the latest snapshot. + /// Defaults to table property history.expire.max-snapshot-age-ms. + #[serde(skip_serializing_if = "Option::is_none")] + max_snapshot_age_ms: Option, + /// For snapshot references except the main branch, a positive number for the max age of the snapshot reference to keep while expiring snapshots. + /// Defaults to table property history.expire.max-ref-age-ms. The main branch never expires. + #[serde(skip_serializing_if = "Option::is_none")] + max_ref_age_ms: Option, + }, + #[serde(rename_all = "kebab-case")] + /// Tags are labels for individual snapshots. + Tag { + /// For snapshot references except the main branch, a positive number for the max age of the snapshot reference to keep while expiring snapshots. + /// Defaults to table property history.expire.max-ref-age-ms. The main branch never expires. + max_ref_age_ms: i64, + }, +} + +#[cfg(test)] +mod tests { + use chrono::{TimeZone, Utc}; + use std::collections::HashMap; + + use crate::spec::snapshot::{Operation, Snapshot, Summary, _serde::SnapshotV1}; + + #[test] + fn schema() { + let record = r#" + { + "snapshot-id": 3051729675574597004, + "timestamp-ms": 1515100955770, + "summary": { + "operation": "append" + }, + "manifest-list": "s3://b/wh/.../s1.avro", + "schema-id": 0 + } + "#; + + let result: Snapshot = serde_json::from_str::(record) + .unwrap() + .try_into() + .unwrap(); + assert_eq!(3051729675574597004, result.snapshot_id()); + assert_eq!( + Utc.timestamp_millis_opt(1515100955770).unwrap(), + result.timestamp() + ); + assert_eq!( + Summary { + operation: Operation::Append, + other: HashMap::new() + }, + *result.summary() + ); + assert_eq!("s3://b/wh/.../s1.avro".to_string(), *result.manifest_list()); + } +} diff --git a/libs/iceberg/src/spec/sort.rs b/libs/iceberg/src/spec/sort.rs new file mode 100644 index 0000000..a4d2f7d --- /dev/null +++ b/libs/iceberg/src/spec/sort.rs @@ -0,0 +1,480 @@ +// 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. + +/*! + * Sorting +*/ +use crate::error::Result; +use crate::spec::Schema; +use crate::{Error, ErrorKind}; +use core::fmt; +use serde::{Deserialize, Serialize}; +use std::fmt::Formatter; +use std::sync::Arc; +use typed_builder::TypedBuilder; + +use super::transform::Transform; + +/// Reference to [`SortOrder`]. +pub type SortOrderRef = Arc; +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Copy, Clone)] +/// Sort direction in a partition, either ascending or descending +pub enum SortDirection { + /// Ascending + #[serde(rename = "asc")] + Ascending, + /// Descending + #[serde(rename = "desc")] + Descending, +} + +impl fmt::Display for SortDirection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match *self { + SortDirection::Ascending => write!(f, "ascending"), + SortDirection::Descending => write!(f, "descending"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Copy, Clone)] +/// Describes the order of null values when sorted. +pub enum NullOrder { + #[serde(rename = "nulls-first")] + /// Nulls are stored first + First, + #[serde(rename = "nulls-last")] + /// Nulls are stored last + Last, +} + +impl fmt::Display for NullOrder { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match *self { + NullOrder::First => write!(f, "first"), + NullOrder::Last => write!(f, "last"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, TypedBuilder)] +#[serde(rename_all = "kebab-case")] +/// Entry for every column that is to be sorted +pub struct SortField { + /// A source column id from the table’s schema + pub source_id: i32, + /// A transform that is used to produce values to be sorted on from the source column. + pub transform: Transform, + /// A sort direction, that can only be either asc or desc + pub direction: SortDirection, + /// A null order that describes the order of null values when sorted. + pub null_order: NullOrder, +} + +impl fmt::Display for SortField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "SortField {{ source_id: {}, transform: {}, direction: {}, null_order: {} }}", + self.source_id, self.transform, self.direction, self.null_order + ) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Builder, Default)] +#[serde(rename_all = "kebab-case")] +#[builder(setter(prefix = "with"))] +#[builder(build_fn(skip))] +/// A sort order is defined by a sort order id and a list of sort fields. +/// The order of the sort fields within the list defines the order in which the sort is applied to the data. +pub struct SortOrder { + /// Identifier for SortOrder, order_id `0` is no sort order. + #[builder(default)] + pub order_id: i64, + /// Details of the sort + #[builder(setter(each(name = "with_sort_field")), default)] + pub fields: Vec, +} + +impl SortOrder { + const UNSORTED_ORDER_ID: i64 = 0; + + /// Create sort order builder + pub fn builder() -> SortOrderBuilder { + SortOrderBuilder::default() + } + + /// Create an unbound unsorted order + pub fn unsorted_order() -> SortOrder { + SortOrder { + order_id: SortOrder::UNSORTED_ORDER_ID, + fields: Vec::new(), + } + } + + /// Returns true if the sort order is unsorted. + /// + /// A [`SortOrder`] is unsorted if it has no sort fields. + pub fn is_unsorted(&self) -> bool { + self.fields.is_empty() + } +} + +impl SortOrderBuilder { + /// Creates a new unbound sort order. + pub fn build_unbound(&self) -> Result { + let fields = self.fields.clone().unwrap_or_default(); + return match (self.order_id, fields.as_slice()) { + (Some(SortOrder::UNSORTED_ORDER_ID) | None, []) => Ok(SortOrder::unsorted_order()), + (_, []) => Err(Error::new( + ErrorKind::Unexpected, + format!("Unsorted order ID must be {}", SortOrder::UNSORTED_ORDER_ID), + )), + (Some(SortOrder::UNSORTED_ORDER_ID), [..]) => Err(Error::new( + ErrorKind::Unexpected, + format!( + "Sort order ID {} is reserved for unsorted order", + SortOrder::UNSORTED_ORDER_ID + ), + )), + (maybe_order_id, [..]) => Ok(SortOrder { + order_id: maybe_order_id.unwrap_or(1), + fields: fields.to_vec(), + }), + }; + } + + /// Creates a new bound sort order. + pub fn build(&self, schema: Schema) -> Result { + let unbound_sort_order = self.build_unbound()?; + SortOrderBuilder::check_compatibility(unbound_sort_order, schema) + } + + /// Returns the given sort order if it is compatible with the given schema + fn check_compatibility(sort_order: SortOrder, schema: Schema) -> Result { + let sort_fields = &sort_order.fields; + for sort_field in sort_fields { + match schema.field_by_id(sort_field.source_id) { + None => { + return Err(Error::new( + ErrorKind::Unexpected, + format!("Cannot find source column for sort field: {sort_field}"), + )) + } + Some(source_field) => { + let source_type = source_field.field_type.as_ref(); + + if !source_type.is_primitive() { + return Err(Error::new( + ErrorKind::Unexpected, + format!("Cannot sort by non-primitive source field: {source_type}"), + )); + } + + let field_transform = sort_field.transform; + if field_transform.result_type(source_type).is_err() { + return Err(Error::new( + ErrorKind::Unexpected, + format!( + "Invalid source type {source_type} for transform {field_transform}" + ), + )); + } + } + } + } + + Ok(sort_order) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spec::{ListType, NestedField, PrimitiveType, Type}; + + #[test] + fn test_sort_field() { + let spec = r#" + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + "#; + + let field: SortField = serde_json::from_str(spec).unwrap(); + assert_eq!(Transform::Bucket(4), field.transform); + assert_eq!(3, field.source_id); + assert_eq!(SortDirection::Descending, field.direction); + assert_eq!(NullOrder::Last, field.null_order); + } + + #[test] + fn test_sort_order() { + let spec = r#" + { + "order-id": 1, + "fields": [ { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } ] + } + "#; + + let order: SortOrder = serde_json::from_str(spec).unwrap(); + assert_eq!(Transform::Identity, order.fields[0].transform); + assert_eq!(2, order.fields[0].source_id); + assert_eq!(SortDirection::Ascending, order.fields[0].direction); + assert_eq!(NullOrder::First, order.fields[0].null_order); + + assert_eq!(Transform::Bucket(4), order.fields[1].transform); + assert_eq!(3, order.fields[1].source_id); + assert_eq!(SortDirection::Descending, order.fields[1].direction); + assert_eq!(NullOrder::Last, order.fields[1].null_order); + } + + #[test] + fn test_build_unbound_should_return_err_if_unsorted_order_does_not_have_an_order_id_of_zero() { + assert_eq!( + SortOrder::builder() + .with_order_id(1) + .build_unbound() + .expect_err("Expected an Err value") + .message(), + "Unsorted order ID must be 0" + ) + } + + #[test] + fn test_build_unbound_should_return_err_if_order_id_equals_zero_is_used_for_anything_other_than_unsorted_order( + ) { + assert_eq!( + SortOrder::builder() + .with_order_id(SortOrder::UNSORTED_ORDER_ID) + .with_sort_field( + SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build() + ) + .build_unbound() + .expect_err("Expected an Err value") + .message(), + "Sort order ID 0 is reserved for unsorted order" + ) + } + + #[test] + fn test_build_unbound_should_return_unsorted_sort_order() { + assert_eq!( + SortOrder::builder() + .with_order_id(SortOrder::UNSORTED_ORDER_ID) + .build_unbound() + .expect("Expected an Ok value"), + SortOrder::unsorted_order() + ) + } + + #[test] + fn test_build_unbound_should_return_sort_order_with_given_order_id_and_sort_fields() { + let sort_field = SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(); + + assert_eq!( + SortOrder::builder() + .with_order_id(2) + .with_sort_field(sort_field.clone()) + .build_unbound() + .expect("Expected an Ok value"), + SortOrder { + order_id: 2, + fields: vec![sort_field] + } + ) + } + + #[test] + fn test_build_unbound_should_return_sort_order_with_given_sort_fields_and_defaults_to_1_if_missing_an_order_id( + ) { + let sort_field = SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(); + + assert_eq!( + SortOrder::builder() + .with_sort_field(sort_field.clone()) + .build_unbound() + .expect("Expected an Ok value"), + SortOrder { + order_id: 1, + fields: vec![sort_field] + } + ) + } + + #[test] + fn test_build_should_return_err_if_sort_order_field_is_not_present_in_schema() { + let schema = Schema::builder() + .with_schema_id(1) + .with_fields(vec![NestedField::required( + 1, + "foo", + Type::Primitive(PrimitiveType::Int), + ) + .into()]) + .build() + .unwrap(); + + let sort_order_builder_result = SortOrder::builder() + .with_sort_field( + SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(), + ) + .build(schema); + + assert_eq!( + sort_order_builder_result + .expect_err("Expected an Err value") + .message(), + "Cannot find source column for sort field: SortField { source_id: 2, transform: identity, direction: ascending, null_order: first }" + ) + } + + #[test] + fn test_build_should_return_err_if_source_field_is_not_a_primitive_type() { + let schema = Schema::builder() + .with_schema_id(1) + .with_fields(vec![NestedField::required( + 1, + "foo", + Type::List(ListType { + element_field: NestedField::list_element( + 2, + Type::Primitive(PrimitiveType::String), + true, + ) + .into(), + }), + ) + .into()]) + .build() + .unwrap(); + + let sort_order_builder_result = SortOrder::builder() + .with_sort_field( + SortField::builder() + .source_id(1) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(), + ) + .build(schema); + + assert_eq!( + sort_order_builder_result + .expect_err("Expected an Err value") + .message(), + "Cannot sort by non-primitive source field: list" + ) + } + + #[test] + fn test_build_should_return_err_if_source_field_type_is_not_supported_by_transform() { + let schema = Schema::builder() + .with_schema_id(1) + .with_fields(vec![NestedField::required( + 1, + "foo", + Type::Primitive(PrimitiveType::Int), + ) + .into()]) + .build() + .unwrap(); + + let sort_order_builder_result = SortOrder::builder() + .with_sort_field( + SortField::builder() + .source_id(1) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Year) + .build(), + ) + .build(schema); + + assert_eq!( + sort_order_builder_result + .expect_err("Expected an Err value") + .message(), + "Invalid source type int for transform year" + ) + } + + #[test] + fn test_build_should_return_valid_sort_order() { + let schema = Schema::builder() + .with_schema_id(1) + .with_fields(vec![ + NestedField::required(1, "foo", Type::Primitive(PrimitiveType::String)).into(), + NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(), + ]) + .build() + .unwrap(); + + let sort_field = SortField::builder() + .source_id(2) + .direction(SortDirection::Ascending) + .null_order(NullOrder::First) + .transform(Transform::Identity) + .build(); + + let sort_order_builder_result = SortOrder::builder() + .with_sort_field(sort_field.clone()) + .build(schema); + + assert_eq!( + sort_order_builder_result.expect("Expected an Ok value"), + SortOrder { + order_id: 1, + fields: vec![sort_field], + } + ) + } +} diff --git a/libs/iceberg/src/spec/table_metadata.rs b/libs/iceberg/src/spec/table_metadata.rs new file mode 100644 index 0000000..a6eb05c --- /dev/null +++ b/libs/iceberg/src/spec/table_metadata.rs @@ -0,0 +1,1572 @@ +// 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. + +//! Defines the [table metadata](https://iceberg.apache.org/spec/#table-metadata). +//! The main struct here is [TableMetadataV2] which defines the data for a table. + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::cmp::Ordering; +use std::fmt::{Display, Formatter}; +use std::{collections::HashMap, sync::Arc}; +use uuid::Uuid; + +use super::{ + snapshot::{Snapshot, SnapshotReference, SnapshotRetention}, + PartitionSpecRef, SchemaId, SchemaRef, SnapshotRef, SortOrderRef, +}; + +use _serde::TableMetadataEnum; + +use chrono::{DateTime, TimeZone, Utc}; + +static MAIN_BRANCH: &str = "main"; +static DEFAULT_SPEC_ID: i32 = 0; +static DEFAULT_SORT_ORDER_ID: i64 = 0; + +pub(crate) static EMPTY_SNAPSHOT_ID: i64 = -1; +pub(crate) static INITIAL_SEQUENCE_NUMBER: i64 = 0; + +/// Reference to [`TableMetadata`]. +pub type TableMetadataRef = Arc; + +#[derive(Debug, PartialEq, Deserialize, Eq, Clone)] +#[serde(try_from = "TableMetadataEnum")] +/// Fields for the version 2 of the table metadata. +/// +/// We assume that this data structure is always valid, so we will panic when invalid error happens. +/// We check the validity of this data structure when constructing. +pub struct TableMetadata { + /// Integer Version for the format. + format_version: FormatVersion, + /// A UUID that identifies the table + table_uuid: Uuid, + /// Location tables base location + location: String, + /// The tables highest sequence number + last_sequence_number: i64, + /// Timestamp in milliseconds from the unix epoch when the table was last updated. + last_updated_ms: i64, + /// An integer; the highest assigned column ID for the table. + last_column_id: i32, + /// A list of schemas, stored as objects with schema-id. + schemas: HashMap, + /// ID of the table’s current schema. + current_schema_id: i32, + /// A list of partition specs, stored as full partition spec objects. + partition_specs: HashMap, + /// ID of the “current” spec that writers should use by default. + default_spec_id: i32, + /// An integer; the highest assigned partition field ID across all partition specs for the table. + last_partition_id: i32, + ///A string to string map of table properties. This is used to control settings that + /// affect reading and writing and is not intended to be used for arbitrary metadata. + /// For example, commit.retry.num-retries is used to control the number of commit retries. + properties: HashMap, + /// long ID of the current table snapshot; must be the same as the current + /// ID of the main branch in refs. + current_snapshot_id: Option, + ///A list of valid snapshots. Valid snapshots are snapshots for which all + /// data files exist in the file system. A data file must not be deleted + /// from the file system until the last snapshot in which it was listed is + /// garbage collected. + snapshots: HashMap, + /// A list (optional) of timestamp and snapshot ID pairs that encodes changes + /// to the current snapshot for the table. Each time the current-snapshot-id + /// is changed, a new entry should be added with the last-updated-ms + /// and the new current-snapshot-id. When snapshots are expired from + /// the list of valid snapshots, all entries before a snapshot that has + /// expired should be removed. + snapshot_log: Vec, + + /// A list (optional) of timestamp and metadata file location pairs + /// that encodes changes to the previous metadata files for the table. + /// Each time a new metadata file is created, a new entry of the + /// previous metadata file location should be added to the list. + /// Tables can be configured to remove oldest metadata log entries and + /// keep a fixed-size log of the most recent entries after a commit. + metadata_log: Vec, + + /// A list of sort orders, stored as full sort order objects. + sort_orders: HashMap, + /// Default sort order id of the table. Note that this could be used by + /// writers, but is not used when reading because reads use the specs + /// stored in manifest files. + default_sort_order_id: i64, + ///A map of snapshot references. The map keys are the unique snapshot reference + /// names in the table, and the map values are snapshot reference objects. + /// There is always a main branch reference pointing to the current-snapshot-id + /// even if the refs map is null. + refs: HashMap, +} + +impl TableMetadata { + /// Returns format version of this metadata. + #[inline] + pub fn format_version(&self) -> FormatVersion { + self.format_version + } + + /// Returns uuid of current table. + #[inline] + pub fn uuid(&self) -> Uuid { + self.table_uuid + } + + /// Returns table location. + #[inline] + pub fn location(&self) -> &str { + self.location.as_str() + } + + /// Returns last sequence number. + #[inline] + pub fn last_sequence_number(&self) -> i64 { + self.last_sequence_number + } + + /// Returns last updated time. + #[inline] + pub fn last_updated_ms(&self) -> DateTime { + Utc.timestamp_millis_opt(self.last_updated_ms).unwrap() + } + + /// Returns schemas + #[inline] + pub fn schemas_iter(&self) -> impl Iterator { + self.schemas.values() + } + + /// Lookup schema by id. + #[inline] + pub fn schema_by_id(&self, schema_id: SchemaId) -> Option<&SchemaRef> { + self.schemas.get(&schema_id) + } + + /// Get current schema + #[inline] + pub fn current_schema(&self) -> &SchemaRef { + self.schema_by_id(self.current_schema_id) + .expect("Current schema id set, but not found in table metadata") + } + + /// Returns all partition specs. + #[inline] + pub fn partition_specs_iter(&self) -> impl Iterator { + self.partition_specs.values() + } + + /// Lookup partition spec by id. + #[inline] + pub fn partition_spec_by_id(&self, spec_id: i32) -> Option<&PartitionSpecRef> { + self.partition_specs.get(&spec_id) + } + + /// Get default partition spec + #[inline] + pub fn default_partition_spec(&self) -> Option<&PartitionSpecRef> { + if self.default_spec_id == DEFAULT_SPEC_ID { + self.partition_spec_by_id(DEFAULT_SPEC_ID) + } else { + Some( + self.partition_spec_by_id(self.default_spec_id) + .expect("Default partition spec id set, but not found in table metadata"), + ) + } + } + + /// Returns all snapshots + #[inline] + pub fn snapshots(&self) -> impl Iterator { + self.snapshots.values() + } + + /// Lookup snapshot by id. + #[inline] + pub fn snapshot_by_id(&self, snapshot_id: i64) -> Option<&SnapshotRef> { + self.snapshots.get(&snapshot_id) + } + + /// Returns snapshot history. + #[inline] + pub fn history(&self) -> &[SnapshotLog] { + &self.snapshot_log + } + + /// Get current snapshot + #[inline] + pub fn current_snapshot(&self) -> Option<&SnapshotRef> { + self.current_snapshot_id.map(|s| { + self.snapshot_by_id(s) + .expect("Current snapshot id has been set, but doesn't exist in metadata") + }) + } + + /// Return all sort orders. + #[inline] + pub fn sort_orders_iter(&self) -> impl Iterator { + self.sort_orders.values() + } + + /// Lookup sort order by id. + #[inline] + pub fn sort_order_by_id(&self, sort_order_id: i64) -> Option<&SortOrderRef> { + self.sort_orders.get(&sort_order_id) + } + + /// Returns default sort order id. + #[inline] + pub fn default_sort_order(&self) -> Option<&SortOrderRef> { + if self.default_sort_order_id == DEFAULT_SORT_ORDER_ID { + self.sort_orders.get(&DEFAULT_SORT_ORDER_ID) + } else { + Some( + self.sort_orders + .get(&self.default_sort_order_id) + .expect("Default order id has been set, but not found in table metadata!"), + ) + } + } + + /// Returns properties of table. + #[inline] + pub fn properties(&self) -> &HashMap { + &self.properties + } + + /// Append snapshot to table + pub fn append_snapshot(&mut self, snapshot: Snapshot) { + self.last_updated_ms = snapshot.timestamp().timestamp_millis(); + self.last_sequence_number = snapshot.sequence_number(); + + self.refs + .entry(MAIN_BRANCH.to_string()) + .and_modify(|s| { + s.snapshot_id = snapshot.snapshot_id(); + }) + .or_insert_with(|| { + SnapshotReference::new( + snapshot.snapshot_id(), + SnapshotRetention::Branch { + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + }, + ) + }); + + self.snapshot_log.push(snapshot.log()); + self.snapshots + .insert(snapshot.snapshot_id(), Arc::new(snapshot)); + } +} + +pub(super) mod _serde { + /// This is a helper module that defines types to help with serialization/deserialization. + /// For deserialization the input first gets read into either the [TableMetadataV1] or [TableMetadataV2] struct + /// and then converted into the [TableMetadata] struct. Serialization works the other way around. + /// [TableMetadataV1] and [TableMetadataV2] are internal struct that are only used for serialization and deserialization. + use std::{collections::HashMap, sync::Arc}; + + use itertools::Itertools; + use serde::{Deserialize, Serialize}; + use uuid::Uuid; + + use crate::spec::{Snapshot, EMPTY_SNAPSHOT_ID}; + use crate::{ + spec::{ + schema::_serde::{SchemaV1, SchemaV2}, + snapshot::_serde::{SnapshotV1, SnapshotV2}, + PartitionField, PartitionSpec, Schema, SnapshotReference, SnapshotRetention, SortOrder, + }, + Error, ErrorKind, + }; + + use super::{ + FormatVersion, MetadataLog, SnapshotLog, TableMetadata, DEFAULT_SORT_ORDER_ID, + DEFAULT_SPEC_ID, MAIN_BRANCH, + }; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(untagged)] + pub(super) enum TableMetadataEnum { + V2(TableMetadataV2), + V1(TableMetadataV1), + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v2 table metadata for serialization/deserialization + pub(super) struct TableMetadataV2 { + pub format_version: VersionNumber<2>, + pub table_uuid: Uuid, + pub location: String, + pub last_sequence_number: i64, + pub last_updated_ms: i64, + pub last_column_id: i32, + pub schemas: Vec, + pub current_schema_id: i32, + pub partition_specs: Vec, + pub default_spec_id: i32, + pub last_partition_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshots: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_log: Option>, + pub sort_orders: Vec, + pub default_sort_order_id: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub refs: Option>, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + /// Defines the structure of a v1 table metadata for serialization/deserialization + pub(super) struct TableMetadataV1 { + pub format_version: VersionNumber<1>, + #[serde(skip_serializing_if = "Option::is_none")] + pub table_uuid: Option, + pub location: String, + pub last_updated_ms: i64, + pub last_column_id: i32, + pub schema: SchemaV1, + #[serde(skip_serializing_if = "Option::is_none")] + pub schemas: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_schema_id: Option, + pub partition_spec: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_specs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_spec_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_partition_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshots: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_log: Option>, + pub sort_orders: Option>, + pub default_sort_order_id: Option, + } + + /// Helper to serialize and deserialize the format version. + #[derive(Debug, PartialEq, Eq)] + pub(super) struct VersionNumber; + + impl Serialize for TableMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // we must do a clone here + let table_metadata_enum: TableMetadataEnum = + self.clone().try_into().map_err(serde::ser::Error::custom)?; + + table_metadata_enum.serialize(serializer) + } + } + + impl Serialize for VersionNumber { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(V) + } + } + + impl<'de, const V: u8> Deserialize<'de> for VersionNumber { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + if value == V { + Ok(VersionNumber::) + } else { + Err(serde::de::Error::custom("Invalid Version")) + } + } + } + + impl TryFrom for TableMetadata { + type Error = Error; + fn try_from(value: TableMetadataEnum) -> Result { + match value { + TableMetadataEnum::V2(value) => value.try_into(), + TableMetadataEnum::V1(value) => value.try_into(), + } + } + } + + impl TryFrom for TableMetadataEnum { + type Error = Error; + fn try_from(value: TableMetadata) -> Result { + Ok(match value.format_version { + FormatVersion::V2 => TableMetadataEnum::V2(value.into()), + FormatVersion::V1 => TableMetadataEnum::V1(value.try_into()?), + }) + } + } + + impl TryFrom for TableMetadata { + type Error = Error; + fn try_from(value: TableMetadataV2) -> Result { + let current_snapshot_id = if let &Some(-1) = &value.current_snapshot_id { + None + } else { + value.current_snapshot_id + }; + let schemas = HashMap::from_iter( + value + .schemas + .into_iter() + .map(|schema| Ok((schema.schema_id, Arc::new(schema.try_into()?)))) + .collect::, Error>>()?, + ); + Ok(TableMetadata { + format_version: FormatVersion::V2, + table_uuid: value.table_uuid, + location: value.location, + last_sequence_number: value.last_sequence_number, + last_updated_ms: value.last_updated_ms, + last_column_id: value.last_column_id, + current_schema_id: if schemas.keys().contains(&value.current_schema_id) { + Ok(value.current_schema_id) + } else { + Err(self::Error::new( + ErrorKind::DataInvalid, + format!( + "No schema exists with the current schema id {}.", + value.current_schema_id + ), + )) + }?, + schemas, + partition_specs: HashMap::from_iter( + value + .partition_specs + .into_iter() + .map(|x| (x.spec_id, Arc::new(x))), + ), + default_spec_id: value.default_spec_id, + last_partition_id: value.last_partition_id, + properties: value.properties.unwrap_or_default(), + current_snapshot_id, + snapshots: value + .snapshots + .map(|snapshots| { + HashMap::from_iter( + snapshots + .into_iter() + .map(|x| (x.snapshot_id, Arc::new(x.into()))), + ) + }) + .unwrap_or_default(), + snapshot_log: value.snapshot_log.unwrap_or_default(), + metadata_log: value.metadata_log.unwrap_or_default(), + sort_orders: HashMap::from_iter( + value + .sort_orders + .into_iter() + .map(|x| (x.order_id, Arc::new(x))), + ), + default_sort_order_id: value.default_sort_order_id, + refs: value.refs.unwrap_or_else(|| { + if let Some(snapshot_id) = current_snapshot_id { + HashMap::from_iter(vec![( + MAIN_BRANCH.to_string(), + SnapshotReference { + snapshot_id, + retention: SnapshotRetention::Branch { + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + }, + }, + )]) + } else { + HashMap::new() + } + }), + }) + } + } + + impl TryFrom for TableMetadata { + type Error = Error; + fn try_from(value: TableMetadataV1) -> Result { + let schemas = value + .schemas + .map(|schemas| { + Ok::<_, Error>(HashMap::from_iter( + schemas + .into_iter() + .enumerate() + .map(|(i, schema)| { + Ok(( + schema.schema_id.unwrap_or(i as i32), + Arc::new(schema.try_into()?), + )) + }) + .collect::, Error>>()? + .into_iter(), + )) + }) + .or_else(|| { + Some(value.schema.try_into().map(|schema: Schema| { + HashMap::from_iter(vec![(schema.schema_id(), Arc::new(schema))]) + })) + }) + .transpose()? + .unwrap_or_default(); + let partition_specs = HashMap::from_iter( + value + .partition_specs + .unwrap_or_else(|| { + vec![PartitionSpec { + spec_id: DEFAULT_SPEC_ID, + fields: value.partition_spec, + }] + }) + .into_iter() + .map(|x| (x.spec_id, Arc::new(x))), + ); + Ok(TableMetadata { + format_version: FormatVersion::V1, + table_uuid: value.table_uuid.unwrap_or_default(), + location: value.location, + last_sequence_number: 0, + last_updated_ms: value.last_updated_ms, + last_column_id: value.last_column_id, + current_schema_id: value + .current_schema_id + .unwrap_or_else(|| schemas.keys().copied().max().unwrap_or_default()), + default_spec_id: value + .default_spec_id + .unwrap_or_else(|| partition_specs.keys().copied().max().unwrap_or_default()), + last_partition_id: value + .last_partition_id + .unwrap_or_else(|| partition_specs.keys().copied().max().unwrap_or_default()), + partition_specs, + schemas, + + properties: value.properties.unwrap_or_default(), + current_snapshot_id: if let &Some(id) = &value.current_snapshot_id { + if id == EMPTY_SNAPSHOT_ID { + None + } else { + Some(id) + } + } else { + value.current_snapshot_id + }, + snapshots: value + .snapshots + .map(|snapshots| { + Ok::<_, Error>(HashMap::from_iter( + snapshots + .into_iter() + .map(|x| Ok((x.snapshot_id, Arc::new(x.try_into()?)))) + .collect::, Error>>()?, + )) + }) + .transpose()? + .unwrap_or_default(), + snapshot_log: value.snapshot_log.unwrap_or_default(), + metadata_log: value.metadata_log.unwrap_or_default(), + sort_orders: match value.sort_orders { + Some(sort_orders) => HashMap::from_iter( + sort_orders.into_iter().map(|x| (x.order_id, Arc::new(x))), + ), + None => HashMap::new(), + }, + default_sort_order_id: value.default_sort_order_id.unwrap_or(DEFAULT_SORT_ORDER_ID), + refs: HashMap::from_iter(vec![( + MAIN_BRANCH.to_string(), + SnapshotReference { + snapshot_id: value.current_snapshot_id.unwrap_or_default(), + retention: SnapshotRetention::Branch { + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + }, + }, + )]), + }) + } + } + + impl From for TableMetadataV2 { + fn from(v: TableMetadata) -> Self { + TableMetadataV2 { + format_version: VersionNumber::<2>, + table_uuid: v.table_uuid, + location: v.location, + last_sequence_number: v.last_sequence_number, + last_updated_ms: v.last_updated_ms, + last_column_id: v.last_column_id, + schemas: v + .schemas + .into_values() + .map(|x| { + Arc::try_unwrap(x) + .unwrap_or_else(|schema| schema.as_ref().clone()) + .into() + }) + .collect(), + current_schema_id: v.current_schema_id, + partition_specs: v + .partition_specs + .into_values() + .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone())) + .collect(), + default_spec_id: v.default_spec_id, + last_partition_id: v.last_partition_id, + properties: if v.properties.is_empty() { + None + } else { + Some(v.properties) + }, + current_snapshot_id: v.current_snapshot_id.or(Some(-1)), + snapshots: if v.snapshots.is_empty() { + None + } else { + Some( + v.snapshots + .into_values() + .map(|x| { + Arc::try_unwrap(x) + .unwrap_or_else(|snapshot| snapshot.as_ref().clone()) + .into() + }) + .collect(), + ) + }, + snapshot_log: if v.snapshot_log.is_empty() { + None + } else { + Some(v.snapshot_log) + }, + metadata_log: if v.metadata_log.is_empty() { + None + } else { + Some(v.metadata_log) + }, + sort_orders: v + .sort_orders + .into_values() + .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone())) + .collect(), + default_sort_order_id: v.default_sort_order_id, + refs: Some(v.refs), + } + } + } + + impl TryFrom for TableMetadataV1 { + type Error = Error; + fn try_from(v: TableMetadata) -> Result { + Ok(TableMetadataV1 { + format_version: VersionNumber::<1>, + table_uuid: Some(v.table_uuid), + location: v.location, + last_updated_ms: v.last_updated_ms, + last_column_id: v.last_column_id, + schema: v + .schemas + .get(&v.current_schema_id) + .ok_or(Error::new( + ErrorKind::Unexpected, + "current_schema_id not found in schemas", + ))? + .as_ref() + .clone() + .into(), + schemas: Some( + v.schemas + .into_values() + .map(|x| { + Arc::try_unwrap(x) + .unwrap_or_else(|schema| schema.as_ref().clone()) + .into() + }) + .collect(), + ), + current_schema_id: Some(v.current_schema_id), + partition_spec: v + .partition_specs + .get(&v.default_spec_id) + .map(|x| x.fields.clone()) + .unwrap_or_default(), + partition_specs: Some( + v.partition_specs + .into_values() + .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone())) + .collect(), + ), + default_spec_id: Some(v.default_spec_id), + last_partition_id: Some(v.last_partition_id), + properties: if v.properties.is_empty() { + None + } else { + Some(v.properties) + }, + current_snapshot_id: v.current_snapshot_id.or(Some(-1)), + snapshots: if v.snapshots.is_empty() { + None + } else { + Some( + v.snapshots + .into_values() + .map(|x| Snapshot::clone(&x).into()) + .collect(), + ) + }, + snapshot_log: if v.snapshot_log.is_empty() { + None + } else { + Some(v.snapshot_log) + }, + metadata_log: if v.metadata_log.is_empty() { + None + } else { + Some(v.metadata_log) + }, + sort_orders: Some( + v.sort_orders + .into_values() + .map(|s| Arc::try_unwrap(s).unwrap_or_else(|s| s.as_ref().clone())) + .collect(), + ), + default_sort_order_id: Some(v.default_sort_order_id), + }) + } + } +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy)] +#[repr(u8)] +/// Iceberg format version +pub enum FormatVersion { + /// Iceberg spec version 1 + V1 = 1u8, + /// Iceberg spec version 2 + V2 = 2u8, +} + +impl PartialOrd for FormatVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FormatVersion { + fn cmp(&self, other: &Self) -> Ordering { + (*self as u8).cmp(&(*other as u8)) + } +} + +impl Display for FormatVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FormatVersion::V1 => write!(f, "v1"), + FormatVersion::V2 => write!(f, "v2"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "kebab-case")] +/// Encodes changes to the previous metadata files for the table +pub struct MetadataLog { + /// The file for the log. + pub metadata_file: String, + /// Time new metadata was created + pub timestamp_ms: i64, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "kebab-case")] +/// A log of when each snapshot was made. +pub struct SnapshotLog { + /// Id of the snapshot. + pub snapshot_id: i64, + /// Last updated timestamp + pub timestamp_ms: i64, +} + +impl SnapshotLog { + /// Returns the last updated timestamp as a DateTime with millisecond precision + pub fn timestamp(self) -> DateTime { + Utc.timestamp_millis_opt(self.timestamp_ms).unwrap() + } +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, fs, sync::Arc}; + + use anyhow::Result; + use uuid::Uuid; + + use pretty_assertions::assert_eq; + + use crate::spec::{ + table_metadata::TableMetadata, NestedField, NullOrder, Operation, PartitionField, + PartitionSpec, PrimitiveType, Schema, Snapshot, SnapshotReference, SnapshotRetention, + SortDirection, SortField, SortOrder, Summary, Transform, Type, + }; + + use super::{FormatVersion, MetadataLog, SnapshotLog}; + + fn check_table_metadata_serde(json: &str, expected_type: TableMetadata) { + let desered_type: TableMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(desered_type, expected_type); + + let sered_json = serde_json::to_string(&expected_type).unwrap(); + let parsed_json_value = serde_json::from_str::(&sered_json).unwrap(); + + assert_eq!(parsed_json_value, desered_type); + } + + fn get_test_table_metadata(file_name: &str) -> TableMetadata { + let path = format!("testdata/table_metadata/{}", file_name); + let metadata: String = fs::read_to_string(path).unwrap(); + + serde_json::from_str(&metadata).unwrap() + } + + #[test] + fn test_table_data_v2() { + let data = r#" + { + "format-version" : 2, + "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94", + "location": "s3://b/wh/data.db/table", + "last-sequence-number" : 1, + "last-updated-ms": 1515100955770, + "last-column-id": 1, + "schemas": [ + { + "schema-id" : 1, + "type" : "struct", + "fields" :[ + { + "id": 1, + "name": "struct_name", + "required": true, + "type": "fixed[1]" + } + ] + } + ], + "current-schema-id" : 1, + "partition-specs": [ + { + "spec-id": 1, + "fields": [ + { + "source-id": 4, + "field-id": 1000, + "name": "ts_day", + "transform": "day" + } + ] + } + ], + "default-spec-id": 1, + "last-partition-id": 1000, + "properties": { + "commit.retry.num-retries": "1" + }, + "metadata-log": [ + { + "metadata-file": "s3://bucket/.../v1.json", + "timestamp-ms": 1515100 + } + ], + "sort-orders": [], + "default-sort-order-id": 0 + } + "#; + + let schema = Schema::builder() + .with_schema_id(1) + .with_fields(vec![Arc::new(NestedField::required( + 1, + "struct_name", + Type::Primitive(PrimitiveType::Fixed(1)), + ))]) + .build() + .unwrap(); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(1) + .with_partition_field(PartitionField { + name: "ts_day".to_string(), + transform: Transform::Day, + source_id: 4, + field_id: 1000, + }) + .build() + .unwrap(); + + let expected = TableMetadata { + format_version: FormatVersion::V2, + table_uuid: Uuid::parse_str("fb072c92-a02b-11e9-ae9c-1bb7bc9eca94").unwrap(), + location: "s3://b/wh/data.db/table".to_string(), + last_updated_ms: 1515100955770, + last_column_id: 1, + schemas: HashMap::from_iter(vec![(1, Arc::new(schema))]), + current_schema_id: 1, + partition_specs: HashMap::from_iter(vec![(1, partition_spec.into())]), + default_spec_id: 1, + last_partition_id: 1000, + default_sort_order_id: 0, + sort_orders: HashMap::from_iter(vec![]), + snapshots: HashMap::default(), + current_snapshot_id: None, + last_sequence_number: 1, + properties: HashMap::from_iter(vec![( + "commit.retry.num-retries".to_string(), + "1".to_string(), + )]), + snapshot_log: Vec::new(), + metadata_log: vec![MetadataLog { + metadata_file: "s3://bucket/.../v1.json".to_string(), + timestamp_ms: 1515100, + }], + refs: HashMap::new(), + }; + + check_table_metadata_serde(data, expected); + } + + #[test] + fn test_table_data_v1() { + let data = r#" + { + "format-version" : 1, + "table-uuid" : "df838b92-0b32-465d-a44e-d39936e538b7", + "location" : "/home/iceberg/warehouse/nyc/taxis", + "last-updated-ms" : 1662532818843, + "last-column-id" : 5, + "schema" : { + "type" : "struct", + "schema-id" : 0, + "fields" : [ { + "id" : 1, + "name" : "vendor_id", + "required" : false, + "type" : "long" + }, { + "id" : 2, + "name" : "trip_id", + "required" : false, + "type" : "long" + }, { + "id" : 3, + "name" : "trip_distance", + "required" : false, + "type" : "float" + }, { + "id" : 4, + "name" : "fare_amount", + "required" : false, + "type" : "double" + }, { + "id" : 5, + "name" : "store_and_fwd_flag", + "required" : false, + "type" : "string" + } ] + }, + "partition-spec" : [ { + "name" : "vendor_id", + "transform" : "identity", + "source-id" : 1, + "field-id" : 1000 + } ], + "last-partition-id" : 1000, + "default-sort-order-id" : 0, + "sort-orders" : [ { + "order-id" : 0, + "fields" : [ ] + } ], + "properties" : { + "owner" : "root" + }, + "current-snapshot-id" : 638933773299822130, + "refs" : { + "main" : { + "snapshot-id" : 638933773299822130, + "type" : "branch" + } + }, + "snapshots" : [ { + "snapshot-id" : 638933773299822130, + "timestamp-ms" : 1662532818843, + "sequence-number" : 0, + "summary" : { + "operation" : "append", + "spark.app.id" : "local-1662532784305", + "added-data-files" : "4", + "added-records" : "4", + "added-files-size" : "6001" + }, + "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro", + "schema-id" : 0 + } ], + "snapshot-log" : [ { + "timestamp-ms" : 1662532818843, + "snapshot-id" : 638933773299822130 + } ], + "metadata-log" : [ { + "timestamp-ms" : 1662532805245, + "metadata-file" : "/home/iceberg/warehouse/nyc/taxis/metadata/00000-8a62c37d-4573-4021-952a-c0baef7d21d0.metadata.json" + } ] + } + "#; + + let schema = Schema::builder() + .with_fields(vec![ + Arc::new(NestedField::optional( + 1, + "vendor_id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 2, + "trip_id", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new(NestedField::optional( + 3, + "trip_distance", + Type::Primitive(PrimitiveType::Float), + )), + Arc::new(NestedField::optional( + 4, + "fare_amount", + Type::Primitive(PrimitiveType::Double), + )), + Arc::new(NestedField::optional( + 5, + "store_and_fwd_flag", + Type::Primitive(PrimitiveType::String), + )), + ]) + .build() + .unwrap(); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(0) + .with_partition_field(PartitionField { + name: "vendor_id".to_string(), + transform: Transform::Identity, + source_id: 1, + field_id: 1000, + }) + .build() + .unwrap(); + + let sort_order = SortOrder::builder() + .with_order_id(0) + .build_unbound() + .unwrap(); + + let snapshot = Snapshot::builder() + .with_snapshot_id(638933773299822130) + .with_timestamp_ms(1662532818843) + .with_sequence_number(0) + .with_schema_id(0) + .with_manifest_list("/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro") + .with_summary(Summary { operation: Operation::Append, other: HashMap::from_iter(vec![("spark.app.id".to_string(), "local-1662532784305".to_string()), ("added-data-files".to_string(), "4".to_string()), ("added-records".to_string(), "4".to_string()), ("added-files-size".to_string(), "6001".to_string())]) }) + .build(); + + let expected = TableMetadata { + format_version: FormatVersion::V1, + table_uuid: Uuid::parse_str("df838b92-0b32-465d-a44e-d39936e538b7").unwrap(), + location: "/home/iceberg/warehouse/nyc/taxis".to_string(), + last_updated_ms: 1662532818843, + last_column_id: 5, + schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]), + current_schema_id: 0, + partition_specs: HashMap::from_iter(vec![(0, partition_spec.into())]), + default_spec_id: 0, + last_partition_id: 1000, + default_sort_order_id: 0, + sort_orders: HashMap::from_iter(vec![(0, sort_order.into())]), + snapshots: HashMap::from_iter(vec![(638933773299822130, Arc::new(snapshot))]), + current_snapshot_id: Some(638933773299822130), + last_sequence_number: 0, + properties: HashMap::from_iter(vec![("owner".to_string(), "root".to_string())]), + snapshot_log: vec![SnapshotLog { + snapshot_id: 638933773299822130, + timestamp_ms: 1662532818843, + }], + metadata_log: vec![MetadataLog { metadata_file: "/home/iceberg/warehouse/nyc/taxis/metadata/00000-8a62c37d-4573-4021-952a-c0baef7d21d0.metadata.json".to_string(), timestamp_ms: 1662532805245 }], + refs: HashMap::from_iter(vec![("main".to_string(), SnapshotReference { snapshot_id: 638933773299822130, retention: SnapshotRetention::Branch { min_snapshots_to_keep: None, max_snapshot_age_ms: None, max_ref_age_ms: None } })]), + }; + + check_table_metadata_serde(data, expected); + } + + #[test] + fn test_invalid_table_uuid() -> Result<()> { + let data = r#" + { + "format-version" : 2, + "table-uuid": "xxxx" + } + "#; + assert!(serde_json::from_str::(data).is_err()); + Ok(()) + } + + #[test] + fn test_deserialize_table_data_v2_invalid_format_version() -> Result<()> { + let data = r#" + { + "format-version" : 1 + } + "#; + assert!(serde_json::from_str::(data).is_err()); + Ok(()) + } + + #[test] + fn test_table_metadata_v2_file_valid() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2Valid.json").unwrap(); + + let schema1 = Schema::builder() + .with_schema_id(0) + .with_fields(vec![Arc::new(NestedField::required( + 1, + "x", + Type::Primitive(PrimitiveType::Long), + ))]) + .build() + .unwrap(); + + let schema2 = Schema::builder() + .with_schema_id(1) + .with_fields(vec![ + Arc::new(NestedField::required( + 1, + "x", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new( + NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long)) + .with_doc("comment"), + ), + Arc::new(NestedField::required( + 3, + "z", + Type::Primitive(PrimitiveType::Long), + )), + ]) + .with_identifier_field_ids(vec![1, 2]) + .build() + .unwrap(); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(0) + .with_partition_field(PartitionField { + name: "x".to_string(), + transform: Transform::Identity, + source_id: 1, + field_id: 1000, + }) + .build() + .unwrap(); + + let sort_order = SortOrder::builder() + .with_order_id(3) + .with_sort_field(SortField { + source_id: 2, + transform: Transform::Identity, + direction: SortDirection::Ascending, + null_order: NullOrder::First, + }) + .with_sort_field(SortField { + source_id: 3, + transform: Transform::Bucket(4), + direction: SortDirection::Descending, + null_order: NullOrder::Last, + }) + .build_unbound() + .unwrap(); + + let snapshot1 = Snapshot::builder() + .with_snapshot_id(3051729675574597004) + .with_timestamp_ms(1515100955770) + .with_sequence_number(0) + .with_manifest_list("s3://a/b/1.avro") + .with_summary(Summary { + operation: Operation::Append, + other: HashMap::new(), + }) + .build(); + + let snapshot2 = Snapshot::builder() + .with_snapshot_id(3055729675574597004) + .with_parent_snapshot_id(Some(3051729675574597004)) + .with_timestamp_ms(1555100955770) + .with_sequence_number(1) + .with_schema_id(1) + .with_manifest_list("s3://a/b/2.avro") + .with_summary(Summary { + operation: Operation::Append, + other: HashMap::new(), + }) + .build(); + + let expected = TableMetadata { + format_version: FormatVersion::V2, + table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(), + location: "s3://bucket/test/location".to_string(), + last_updated_ms: 1602638573590, + last_column_id: 3, + schemas: HashMap::from_iter(vec![(0, Arc::new(schema1)), (1, Arc::new(schema2))]), + current_schema_id: 1, + partition_specs: HashMap::from_iter(vec![(0, partition_spec.into())]), + default_spec_id: 0, + last_partition_id: 1000, + default_sort_order_id: 3, + sort_orders: HashMap::from_iter(vec![(3, sort_order.into())]), + snapshots: HashMap::from_iter(vec![ + (3051729675574597004, Arc::new(snapshot1)), + (3055729675574597004, Arc::new(snapshot2)), + ]), + current_snapshot_id: Some(3055729675574597004), + last_sequence_number: 34, + properties: HashMap::new(), + snapshot_log: vec![ + SnapshotLog { + snapshot_id: 3051729675574597004, + timestamp_ms: 1515100955770, + }, + SnapshotLog { + snapshot_id: 3055729675574597004, + timestamp_ms: 1555100955770, + }, + ], + metadata_log: Vec::new(), + refs: HashMap::from_iter(vec![( + "main".to_string(), + SnapshotReference { + snapshot_id: 3055729675574597004, + retention: SnapshotRetention::Branch { + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + }, + }, + )]), + }; + + check_table_metadata_serde(&metadata, expected); + } + + #[test] + fn test_table_metadata_v2_file_valid_minimal() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2ValidMinimal.json").unwrap(); + + let schema = Schema::builder() + .with_schema_id(0) + .with_fields(vec![ + Arc::new(NestedField::required( + 1, + "x", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new( + NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long)) + .with_doc("comment"), + ), + Arc::new(NestedField::required( + 3, + "z", + Type::Primitive(PrimitiveType::Long), + )), + ]) + .build() + .unwrap(); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(0) + .with_partition_field(PartitionField { + name: "x".to_string(), + transform: Transform::Identity, + source_id: 1, + field_id: 1000, + }) + .build() + .unwrap(); + + let sort_order = SortOrder::builder() + .with_order_id(3) + .with_sort_field(SortField { + source_id: 2, + transform: Transform::Identity, + direction: SortDirection::Ascending, + null_order: NullOrder::First, + }) + .with_sort_field(SortField { + source_id: 3, + transform: Transform::Bucket(4), + direction: SortDirection::Descending, + null_order: NullOrder::Last, + }) + .build_unbound() + .unwrap(); + + let expected = TableMetadata { + format_version: FormatVersion::V2, + table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(), + location: "s3://bucket/test/location".to_string(), + last_updated_ms: 1602638573590, + last_column_id: 3, + schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]), + current_schema_id: 0, + partition_specs: HashMap::from_iter(vec![(0, partition_spec.into())]), + default_spec_id: 0, + last_partition_id: 1000, + default_sort_order_id: 3, + sort_orders: HashMap::from_iter(vec![(3, sort_order.into())]), + snapshots: HashMap::default(), + current_snapshot_id: None, + last_sequence_number: 34, + properties: HashMap::new(), + snapshot_log: vec![], + metadata_log: Vec::new(), + refs: HashMap::new(), + }; + + check_table_metadata_serde(&metadata, expected); + } + + #[test] + fn test_table_metadata_v1_file_valid() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV1Valid.json").unwrap(); + + let schema = Schema::builder() + .with_schema_id(0) + .with_fields(vec![ + Arc::new(NestedField::required( + 1, + "x", + Type::Primitive(PrimitiveType::Long), + )), + Arc::new( + NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long)) + .with_doc("comment"), + ), + Arc::new(NestedField::required( + 3, + "z", + Type::Primitive(PrimitiveType::Long), + )), + ]) + .build() + .unwrap(); + + let partition_spec = PartitionSpec::builder() + .with_spec_id(0) + .with_partition_field(PartitionField { + name: "x".to_string(), + transform: Transform::Identity, + source_id: 1, + field_id: 1000, + }) + .build() + .unwrap(); + + let expected = TableMetadata { + format_version: FormatVersion::V1, + table_uuid: Uuid::parse_str("d20125c8-7284-442c-9aea-15fee620737c").unwrap(), + location: "s3://bucket/test/location".to_string(), + last_updated_ms: 1602638573874, + last_column_id: 3, + schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]), + current_schema_id: 0, + partition_specs: HashMap::from_iter(vec![(0, partition_spec.into())]), + default_spec_id: 0, + last_partition_id: 0, + default_sort_order_id: 0, + sort_orders: HashMap::new(), + snapshots: HashMap::new(), + current_snapshot_id: None, + last_sequence_number: 0, + properties: HashMap::new(), + snapshot_log: vec![], + metadata_log: Vec::new(), + refs: HashMap::from_iter(vec![( + "main".to_string(), + SnapshotReference { + snapshot_id: -1, + retention: SnapshotRetention::Branch { + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + }, + }, + )]), + }; + + check_table_metadata_serde(&metadata, expected); + } + + #[test] + fn test_table_metadata_v2_schema_not_found() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json") + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "DataInvalid => No schema exists with the current schema id 2." + ) + } + + #[test] + fn test_table_metadata_v2_missing_sort_order() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingSortOrder.json") + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "data did not match any variant of untagged enum TableMetadataEnum" + ) + } + + #[test] + fn test_table_metadata_v2_missing_partition_specs() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json") + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "data did not match any variant of untagged enum TableMetadataEnum" + ) + } + + #[test] + fn test_table_metadata_v2_missing_last_partition_id() { + let metadata = fs::read_to_string( + "testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json", + ) + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "data did not match any variant of untagged enum TableMetadataEnum" + ) + } + + #[test] + fn test_table_metadata_v2_missing_schemas() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingSchemas.json") + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "data did not match any variant of untagged enum TableMetadataEnum" + ) + } + + #[test] + fn test_table_metadata_v2_unsupported_version() { + let metadata = + fs::read_to_string("testdata/table_metadata/TableMetadataUnsupportedVersion.json") + .unwrap(); + + let desered: Result = serde_json::from_str(&metadata); + + assert_eq!( + desered.unwrap_err().to_string(), + "data did not match any variant of untagged enum TableMetadataEnum" + ) + } + + #[test] + fn test_order_of_format_version() { + assert!(FormatVersion::V1 < FormatVersion::V2); + assert_eq!(FormatVersion::V1, FormatVersion::V1); + assert_eq!(FormatVersion::V2, FormatVersion::V2); + } + + #[test] + fn test_default_partition_spec() { + let default_spec_id = 1234; + let mut table_meta_data = get_test_table_metadata("TableMetadataV2Valid.json"); + table_meta_data.default_spec_id = default_spec_id; + table_meta_data + .partition_specs + .insert(default_spec_id, Arc::new(PartitionSpec::default())); + + assert_eq!( + table_meta_data.default_partition_spec(), + table_meta_data.partition_spec_by_id(default_spec_id) + ); + } + #[test] + fn test_default_sort_order() { + let default_sort_order_id = 1234; + let mut table_meta_data = get_test_table_metadata("TableMetadataV2Valid.json"); + table_meta_data.default_sort_order_id = default_sort_order_id; + table_meta_data + .sort_orders + .insert(default_sort_order_id, Arc::new(SortOrder::default())); + + assert_eq!( + table_meta_data.default_sort_order(), + table_meta_data.sort_orders.get(&default_sort_order_id) + ) + } +} diff --git a/libs/iceberg/src/spec/transform.rs b/libs/iceberg/src/spec/transform.rs new file mode 100644 index 0000000..839d582 --- /dev/null +++ b/libs/iceberg/src/spec/transform.rs @@ -0,0 +1,861 @@ +// 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. + +//! Transforms in iceberg. + +use crate::error::{Error, Result}; +use crate::spec::datatypes::{PrimitiveType, Type}; +use crate::ErrorKind; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +/// Transform is used to transform predicates to partition predicates, +/// in addition to transforming data values. +/// +/// Deriving partition predicates from column predicates on the table data +/// is used to separate the logical queries from physical storage: the +/// partitioning can change and the correct partition filters are always +/// derived from column predicates. +/// +/// This simplifies queries because users don’t have to supply both logical +/// predicates and partition predicates. +/// +/// All transforms must return `null` for a `null` input value. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Transform { + /// Source value, unmodified + /// + /// - Source type could be any type. + /// - Return type is the same with source type. + Identity, + /// Hash of value, mod `N`. + /// + /// Bucket partition transforms use a 32-bit hash of the source value. + /// The 32-bit hash implementation is the 32-bit Murmur3 hash, x86 + /// variant, seeded with 0. + /// + /// Transforms are parameterized by a number of buckets, N. The hash mod + /// N must produce a positive value by first discarding the sign bit of + /// the hash value. In pseudo-code, the function is: + /// + /// ```text + /// def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N + /// ``` + /// + /// - Source type could be `int`, `long`, `decimal`, `date`, `time`, + /// `timestamp`, `timestamptz`, `string`, `uuid`, `fixed`, `binary`. + /// - Return type is `int`. + Bucket(u32), + /// Value truncated to width `W` + /// + /// For `int`: + /// + /// - `v - (v % W)` remainders must be positive + /// - example: W=10: 1 → 0, -1 → -10 + /// - note: The remainder, v % W, must be positive. + /// + /// For `long`: + /// + /// - `v - (v % W)` remainders must be positive + /// - example: W=10: 1 → 0, -1 → -10 + /// - note: The remainder, v % W, must be positive. + /// + /// For `decimal`: + /// + /// - `scaled_W = decimal(W, scale(v)) v - (v % scaled_W)` + /// - example: W=50, s=2: 10.65 → 10.50 + /// + /// For `string`: + /// + /// - Substring of length L: `v.substring(0, L)` + /// - example: L=3: iceberg → ice + /// - note: Strings are truncated to a valid UTF-8 string with no more + /// than L code points. + /// + /// - Source type could be `int`, `long`, `decimal`, `string` + /// - Return type is the same with source type. + Truncate(u32), + /// Extract a date or timestamp year, as years from 1970 + /// + /// - Source type could be `date`, `timestamp`, `timestamptz` + /// - Return type is `int` + Year, + /// Extract a date or timestamp month, as months from 1970-01-01 + /// + /// - Source type could be `date`, `timestamp`, `timestamptz` + /// - Return type is `int` + Month, + /// Extract a date or timestamp day, as days from 1970-01-01 + /// + /// - Source type could be `date`, `timestamp`, `timestamptz` + /// - Return type is `int` + Day, + /// Extract a timestamp hour, as hours from 1970-01-01 00:00:00 + /// + /// - Source type could be `timestamp`, `timestamptz` + /// - Return type is `int` + Hour, + /// Always produces `null` + /// + /// The void transform may be used to replace the transform in an + /// existing partition field so that the field is effectively dropped in + /// v1 tables. + /// + /// - Source type could be any type.. + /// - Return type is Source type. + Void, + /// Used to represent some customized transform that can't be recognized or supported now. + Unknown, +} + +impl Transform { + /// Get the return type of transform given the input type. + /// Returns `None` if it can't be transformed. + pub fn result_type(&self, input_type: &Type) -> Result { + match self { + Transform::Identity => { + if matches!(input_type, Type::Primitive(_)) { + Ok(input_type.clone()) + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of identity transform",), + )) + } + } + Transform::Void => Ok(input_type.clone()), + Transform::Unknown => Ok(Type::Primitive(PrimitiveType::String)), + Transform::Bucket(_) => { + if let Type::Primitive(p) = input_type { + match p { + PrimitiveType::Int + | PrimitiveType::Long + | PrimitiveType::Decimal { .. } + | PrimitiveType::Date + | PrimitiveType::Time + | PrimitiveType::Timestamp + | PrimitiveType::Timestamptz + | PrimitiveType::String + | PrimitiveType::Uuid + | PrimitiveType::Fixed(_) + | PrimitiveType::Binary => Ok(Type::Primitive(PrimitiveType::Int)), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of bucket transform",), + )), + } + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of bucket transform",), + )) + } + } + Transform::Truncate(_) => { + if let Type::Primitive(p) = input_type { + match p { + PrimitiveType::Int + | PrimitiveType::Long + | PrimitiveType::String + | PrimitiveType::Binary + | PrimitiveType::Decimal { .. } => Ok(input_type.clone()), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of truncate transform",), + )), + } + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of truncate transform",), + )) + } + } + Transform::Year | Transform::Month | Transform::Day => { + if let Type::Primitive(p) = input_type { + match p { + PrimitiveType::Timestamp + | PrimitiveType::Timestamptz + | PrimitiveType::Date => Ok(Type::Primitive(PrimitiveType::Int)), + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of {self} transform",), + )), + } + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of {self} transform",), + )) + } + } + Transform::Hour => { + if let Type::Primitive(p) = input_type { + match p { + PrimitiveType::Timestamp | PrimitiveType::Timestamptz => { + Ok(Type::Primitive(PrimitiveType::Int)) + } + _ => Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of {self} transform",), + )), + } + } else { + Err(Error::new( + ErrorKind::DataInvalid, + format!("{input_type} is not a valid input type of {self} transform",), + )) + } + } + } + } + + /// Whether the transform preserves the order of values. + pub fn preserves_order(&self) -> bool { + !matches!( + self, + Transform::Void | Transform::Bucket(_) | Transform::Unknown + ) + } + + /// Return the unique transform name to check if similar transforms for the same source field + /// are added multiple times in partition spec builder. + pub fn dedup_name(&self) -> String { + match self { + Transform::Year | Transform::Month | Transform::Day | Transform::Hour => { + "time".to_string() + } + _ => format!("{self}"), + } + } + + /// Whether ordering by this transform's result satisfies the ordering of another transform's + /// result. + /// + /// For example, sorting by day(ts) will produce an ordering that is also by month(ts) or + // year(ts). However, sorting by day(ts) will not satisfy the order of hour(ts) or identity(ts). + pub fn satisfies_order_of(&self, other: &Self) -> bool { + match self { + Transform::Identity => other.preserves_order(), + Transform::Hour => matches!( + other, + Transform::Hour | Transform::Day | Transform::Month | Transform::Year + ), + Transform::Day => matches!(other, Transform::Day | Transform::Month | Transform::Year), + Transform::Month => matches!(other, Transform::Month | Transform::Year), + _ => self == other, + } + } +} + +impl Display for Transform { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Transform::Identity => write!(f, "identity"), + Transform::Year => write!(f, "year"), + Transform::Month => write!(f, "month"), + Transform::Day => write!(f, "day"), + Transform::Hour => write!(f, "hour"), + Transform::Void => write!(f, "void"), + Transform::Bucket(length) => write!(f, "bucket[{length}]"), + Transform::Truncate(width) => write!(f, "truncate[{width}]"), + Transform::Unknown => write!(f, "unknown"), + } + } +} + +impl FromStr for Transform { + type Err = Error; + + fn from_str(s: &str) -> Result { + let t = match s { + "identity" => Transform::Identity, + "year" => Transform::Year, + "month" => Transform::Month, + "day" => Transform::Day, + "hour" => Transform::Hour, + "void" => Transform::Void, + "unknown" => Transform::Unknown, + v if v.starts_with("bucket") => { + let length = v + .strip_prefix("bucket") + .expect("transform must starts with `bucket`") + .trim_start_matches('[') + .trim_end_matches(']') + .parse() + .map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + format!("transform bucket type {v:?} is invalid"), + ) + .with_source(err) + })?; + + Transform::Bucket(length) + } + v if v.starts_with("truncate") => { + let width = v + .strip_prefix("truncate") + .expect("transform must starts with `truncate`") + .trim_start_matches('[') + .trim_end_matches(']') + .parse() + .map_err(|err| { + Error::new( + ErrorKind::DataInvalid, + format!("transform truncate type {v:?} is invalid"), + ) + .with_source(err) + })?; + + Transform::Truncate(width) + } + v => { + return Err(Error::new( + ErrorKind::DataInvalid, + format!("transform {v:?} is invalid"), + )) + } + }; + + Ok(t) + } +} + +impl Serialize for Transform { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(format!("{self}").as_str()) + } +} + +impl<'de> Deserialize<'de> for Transform { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(::custom) + } +} + +#[cfg(test)] +mod tests { + use crate::spec::datatypes::PrimitiveType::{ + Binary, Date, Decimal, Fixed, Int, Long, String as StringType, Time, Timestamp, + Timestamptz, Uuid, + }; + use crate::spec::datatypes::Type::{Primitive, Struct}; + use crate::spec::datatypes::{NestedField, StructType, Type}; + use crate::spec::transform::Transform; + + struct TestParameter { + display: String, + json: String, + dedup_name: String, + preserves_order: bool, + satisfies_order_of: Vec<(Transform, bool)>, + trans_types: Vec<(Type, Option)>, + } + + fn check_transform(trans: Transform, param: TestParameter) { + assert_eq!(param.display, format!("{trans}")); + assert_eq!(param.json, serde_json::to_string(&trans).unwrap()); + assert_eq!(trans, serde_json::from_str(param.json.as_str()).unwrap()); + assert_eq!(param.dedup_name, trans.dedup_name()); + assert_eq!(param.preserves_order, trans.preserves_order()); + + for (other_trans, satisfies_order_of) in param.satisfies_order_of { + assert_eq!( + satisfies_order_of, + trans.satisfies_order_of(&other_trans), + "Failed to check satisfies order {}, {}, {}", + trans, + other_trans, + satisfies_order_of + ); + } + + for (input_type, result_type) in param.trans_types { + assert_eq!(result_type, trans.result_type(&input_type).ok()); + } + } + + #[test] + fn test_bucket_transform() { + let trans = Transform::Bucket(8); + + let test_param = TestParameter { + display: "bucket[8]".to_string(), + json: r#""bucket[8]""#.to_string(), + dedup_name: "bucket[8]".to_string(), + preserves_order: false, + satisfies_order_of: vec![ + (Transform::Bucket(8), true), + (Transform::Bucket(4), false), + (Transform::Void, false), + (Transform::Day, false), + ], + trans_types: vec![ + (Primitive(Binary), Some(Primitive(Int))), + (Primitive(Date), Some(Primitive(Int))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + Some(Primitive(Int)), + ), + (Primitive(Fixed(8)), Some(Primitive(Int))), + (Primitive(Int), Some(Primitive(Int))), + (Primitive(Long), Some(Primitive(Int))), + (Primitive(StringType), Some(Primitive(Int))), + (Primitive(Uuid), Some(Primitive(Int))), + (Primitive(Time), Some(Primitive(Int))), + (Primitive(Timestamp), Some(Primitive(Int))), + (Primitive(Timestamptz), Some(Primitive(Int))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_truncate_transform() { + let trans = Transform::Truncate(4); + + let test_param = TestParameter { + display: "truncate[4]".to_string(), + json: r#""truncate[4]""#.to_string(), + dedup_name: "truncate[4]".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Truncate(4), true), + (Transform::Truncate(2), false), + (Transform::Bucket(4), false), + (Transform::Void, false), + (Transform::Day, false), + ], + trans_types: vec![ + (Primitive(Binary), Some(Primitive(Binary))), + (Primitive(Date), None), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + Some(Primitive(Decimal { + precision: 8, + scale: 5, + })), + ), + (Primitive(Fixed(8)), None), + (Primitive(Int), Some(Primitive(Int))), + (Primitive(Long), Some(Primitive(Long))), + (Primitive(StringType), Some(Primitive(StringType))), + (Primitive(Uuid), None), + (Primitive(Time), None), + (Primitive(Timestamp), None), + (Primitive(Timestamptz), None), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_identity_transform() { + let trans = Transform::Identity; + + let test_param = TestParameter { + display: "identity".to_string(), + json: r#""identity""#.to_string(), + dedup_name: "identity".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Truncate(4), true), + (Transform::Truncate(2), true), + (Transform::Bucket(4), false), + (Transform::Void, false), + (Transform::Day, true), + ], + trans_types: vec![ + (Primitive(Binary), Some(Primitive(Binary))), + (Primitive(Date), Some(Primitive(Date))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + Some(Primitive(Decimal { + precision: 8, + scale: 5, + })), + ), + (Primitive(Fixed(8)), Some(Primitive(Fixed(8)))), + (Primitive(Int), Some(Primitive(Int))), + (Primitive(Long), Some(Primitive(Long))), + (Primitive(StringType), Some(Primitive(StringType))), + (Primitive(Uuid), Some(Primitive(Uuid))), + (Primitive(Time), Some(Primitive(Time))), + (Primitive(Timestamp), Some(Primitive(Timestamp))), + (Primitive(Timestamptz), Some(Primitive(Timestamptz))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_year_transform() { + let trans = Transform::Year; + + let test_param = TestParameter { + display: "year".to_string(), + json: r#""year""#.to_string(), + dedup_name: "time".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Year, true), + (Transform::Month, false), + (Transform::Day, false), + (Transform::Hour, false), + (Transform::Void, false), + (Transform::Identity, false), + ], + trans_types: vec![ + (Primitive(Binary), None), + (Primitive(Date), Some(Primitive(Int))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + None, + ), + (Primitive(Fixed(8)), None), + (Primitive(Int), None), + (Primitive(Long), None), + (Primitive(StringType), None), + (Primitive(Uuid), None), + (Primitive(Time), None), + (Primitive(Timestamp), Some(Primitive(Int))), + (Primitive(Timestamptz), Some(Primitive(Int))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_month_transform() { + let trans = Transform::Month; + + let test_param = TestParameter { + display: "month".to_string(), + json: r#""month""#.to_string(), + dedup_name: "time".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Year, true), + (Transform::Month, true), + (Transform::Day, false), + (Transform::Hour, false), + (Transform::Void, false), + (Transform::Identity, false), + ], + trans_types: vec![ + (Primitive(Binary), None), + (Primitive(Date), Some(Primitive(Int))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + None, + ), + (Primitive(Fixed(8)), None), + (Primitive(Int), None), + (Primitive(Long), None), + (Primitive(StringType), None), + (Primitive(Uuid), None), + (Primitive(Time), None), + (Primitive(Timestamp), Some(Primitive(Int))), + (Primitive(Timestamptz), Some(Primitive(Int))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_day_transform() { + let trans = Transform::Day; + + let test_param = TestParameter { + display: "day".to_string(), + json: r#""day""#.to_string(), + dedup_name: "time".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Year, true), + (Transform::Month, true), + (Transform::Day, true), + (Transform::Hour, false), + (Transform::Void, false), + (Transform::Identity, false), + ], + trans_types: vec![ + (Primitive(Binary), None), + (Primitive(Date), Some(Primitive(Int))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + None, + ), + (Primitive(Fixed(8)), None), + (Primitive(Int), None), + (Primitive(Long), None), + (Primitive(StringType), None), + (Primitive(Uuid), None), + (Primitive(Time), None), + (Primitive(Timestamp), Some(Primitive(Int))), + (Primitive(Timestamptz), Some(Primitive(Int))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_hour_transform() { + let trans = Transform::Hour; + + let test_param = TestParameter { + display: "hour".to_string(), + json: r#""hour""#.to_string(), + dedup_name: "time".to_string(), + preserves_order: true, + satisfies_order_of: vec![ + (Transform::Year, true), + (Transform::Month, true), + (Transform::Day, true), + (Transform::Hour, true), + (Transform::Void, false), + (Transform::Identity, false), + ], + trans_types: vec![ + (Primitive(Binary), None), + (Primitive(Date), None), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + None, + ), + (Primitive(Fixed(8)), None), + (Primitive(Int), None), + (Primitive(Long), None), + (Primitive(StringType), None), + (Primitive(Uuid), None), + (Primitive(Time), None), + (Primitive(Timestamp), Some(Primitive(Int))), + (Primitive(Timestamptz), Some(Primitive(Int))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + None, + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_void_transform() { + let trans = Transform::Void; + + let test_param = TestParameter { + display: "void".to_string(), + json: r#""void""#.to_string(), + dedup_name: "void".to_string(), + preserves_order: false, + satisfies_order_of: vec![ + (Transform::Year, false), + (Transform::Month, false), + (Transform::Day, false), + (Transform::Hour, false), + (Transform::Void, true), + (Transform::Identity, false), + ], + trans_types: vec![ + (Primitive(Binary), Some(Primitive(Binary))), + (Primitive(Date), Some(Primitive(Date))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + Some(Primitive(Decimal { + precision: 8, + scale: 5, + })), + ), + (Primitive(Fixed(8)), Some(Primitive(Fixed(8)))), + (Primitive(Int), Some(Primitive(Int))), + (Primitive(Long), Some(Primitive(Long))), + (Primitive(StringType), Some(Primitive(StringType))), + (Primitive(Uuid), Some(Primitive(Uuid))), + (Primitive(Time), Some(Primitive(Time))), + (Primitive(Timestamp), Some(Primitive(Timestamp))), + (Primitive(Timestamptz), Some(Primitive(Timestamptz))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + Some(Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()]))), + ), + ], + }; + + check_transform(trans, test_param); + } + + #[test] + fn test_known_transform() { + let trans = Transform::Unknown; + + let test_param = TestParameter { + display: "unknown".to_string(), + json: r#""unknown""#.to_string(), + dedup_name: "unknown".to_string(), + preserves_order: false, + satisfies_order_of: vec![ + (Transform::Year, false), + (Transform::Month, false), + (Transform::Day, false), + (Transform::Hour, false), + (Transform::Void, false), + (Transform::Identity, false), + (Transform::Unknown, true), + ], + trans_types: vec![ + (Primitive(Binary), Some(Primitive(StringType))), + (Primitive(Date), Some(Primitive(StringType))), + ( + Primitive(Decimal { + precision: 8, + scale: 5, + }), + Some(Primitive(StringType)), + ), + (Primitive(Fixed(8)), Some(Primitive(StringType))), + (Primitive(Int), Some(Primitive(StringType))), + (Primitive(Long), Some(Primitive(StringType))), + (Primitive(StringType), Some(Primitive(StringType))), + (Primitive(Uuid), Some(Primitive(StringType))), + (Primitive(Time), Some(Primitive(StringType))), + (Primitive(Timestamp), Some(Primitive(StringType))), + (Primitive(Timestamptz), Some(Primitive(StringType))), + ( + Struct(StructType::new(vec![NestedField::optional( + 1, + "a", + Primitive(Timestamp), + ) + .into()])), + Some(Primitive(StringType)), + ), + ], + }; + + check_transform(trans, test_param); + } +} diff --git a/libs/iceberg/src/spec/values.rs b/libs/iceberg/src/spec/values.rs new file mode 100644 index 0000000..a8a748d --- /dev/null +++ b/libs/iceberg/src/spec/values.rs @@ -0,0 +1,2237 @@ +// 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. + +/*! + * Value in iceberg + */ + +use std::str::FromStr; +use std::{any::Any, collections::BTreeMap}; + +use crate::error::Result; +use bitvec::vec::BitVec; +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; +use ordered_float::OrderedFloat; +use rust_decimal::Decimal; +use serde_bytes::ByteBuf; +use serde_json::{Map as JsonMap, Number, Value as JsonValue}; +use uuid::Uuid; + +use crate::{Error, ErrorKind}; + +use super::datatypes::{PrimitiveType, Type}; + +pub use _serde::RawLiteral; + +/// Values present in iceberg type +#[derive(Clone, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub enum PrimitiveLiteral { + /// 0x00 for false, non-zero byte for true + Boolean(bool), + /// Stored as 4-byte little-endian + Int(i32), + /// Stored as 8-byte little-endian + Long(i64), + /// Stored as 4-byte little-endian + Float(OrderedFloat), + /// Stored as 8-byte little-endian + Double(OrderedFloat), + /// Stores days from the 1970-01-01 in an 4-byte little-endian int + Date(i32), + /// Stores microseconds from midnight in an 8-byte little-endian long + Time(i64), + /// Timestamp without timezone + Timestamp(i64), + /// Timestamp with timezone + TimestampTZ(i64), + /// UTF-8 bytes (without length) + String(String), + /// 16-byte big-endian value + UUID(Uuid), + /// Binary value + Fixed(Vec), + /// Binary value (without length) + Binary(Vec), + /// Stores unscaled value as big int. According to iceberg spec, the precision must less than 38(`MAX_DECIMAL_PRECISION`) , so i128 is suit here. + Decimal(i128), +} + +/// Values present in iceberg type +#[derive(Clone, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub enum Literal { + /// A primitive value + Primitive(PrimitiveLiteral), + /// A struct is a tuple of typed values. Each field in the tuple is named and has an integer id that is unique in the table schema. + /// Each field can be either optional or required, meaning that values can (or cannot) be null. Fields may be any type. + /// Fields may have an optional comment or doc string. Fields can have default values. + Struct(Struct), + /// A list is a collection of values with some element type. + /// The element field has an integer id that is unique in the table schema. + /// Elements can be either optional or required. Element types may be any type. + List(Vec>), + /// A map is a collection of key-value pairs with a key type and a value type. + /// Both the key field and value field each have an integer id that is unique in the table schema. + /// Map keys are required and map values can be either optional or required. Both map keys and map values may be any type, including nested types. + Map(BTreeMap>), +} + +impl Literal { + /// Creates a boolean value. + /// + /// Example: + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::bool(true); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Boolean(true)), t); + /// ``` + pub fn bool>(t: T) -> Self { + Self::Primitive(PrimitiveLiteral::Boolean(t.into())) + } + + /// Creates a boolean value from string. + /// See [Parse bool from str](https://doc.rust-lang.org/stable/std/primitive.bool.html#impl-FromStr-for-bool) for reference. + /// + /// Example: + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::bool_from_str("false").unwrap(); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Boolean(false)), t); + /// ``` + pub fn bool_from_str>(s: S) -> Result { + let v = s.as_ref().parse::().map_err(|e| { + Error::new(ErrorKind::DataInvalid, "Can't parse string to bool.").with_source(e) + })?; + Ok(Self::Primitive(PrimitiveLiteral::Boolean(v))) + } + + /// Creates an 32bit integer. + /// + /// Example: + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::int(23i8); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Int(23)), t); + /// ``` + pub fn int>(t: T) -> Self { + Self::Primitive(PrimitiveLiteral::Int(t.into())) + } + + /// Creates an 64bit integer. + /// + /// Example: + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::long(24i8); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Long(24)), t); + /// ``` + pub fn long>(t: T) -> Self { + Self::Primitive(PrimitiveLiteral::Long(t.into())) + } + + /// Creates an 32bit floating point number. + /// + /// Example: + /// ```rust + /// use ordered_float::OrderedFloat; + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::float( 32.1f32 ); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Float(OrderedFloat(32.1))), t); + /// ``` + pub fn float>(t: T) -> Self { + Self::Primitive(PrimitiveLiteral::Float(OrderedFloat(t.into()))) + } + + /// Creates an 32bit floating point number. + /// + /// Example: + /// ```rust + /// use ordered_float::OrderedFloat; + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t = Literal::double( 32.1f64 ); + /// + /// assert_eq!(Literal::Primitive(PrimitiveLiteral::Double(OrderedFloat(32.1))), t); + /// ``` + pub fn double>(t: T) -> Self { + Self::Primitive(PrimitiveLiteral::Double(OrderedFloat(t.into()))) + } + + /// Returns unix epoch. + pub fn unix_epoch() -> DateTime { + Utc.timestamp_nanos(0) + } + + /// Creates date literal from number of days from unix epoch directly. + pub fn date(days: i32) -> Self { + Self::Primitive(PrimitiveLiteral::Date(days)) + } + + /// Creates date literal from `NaiveDate`, assuming it's utc timezone. + fn date_from_naive_date(date: NaiveDate) -> Self { + let days = (date - Self::unix_epoch().date_naive()).num_days(); + Self::date(days as i32) + } + + /// Creates a date in `%Y-%m-%d` format, assume in utc timezone. + /// + /// See [`NaiveDate::from_str`]. + /// + /// Example + /// ```rust + /// use iceberg::spec::Literal; + /// let t = Literal::date_from_str("1970-01-03").unwrap(); + /// + /// assert_eq!(Literal::date(2), t); + /// ``` + pub fn date_from_str>(s: S) -> Result { + let t = s.as_ref().parse::().map_err(|e| { + Error::new( + ErrorKind::DataInvalid, + format!("Can't parse date from string: {}", s.as_ref()), + ) + .with_source(e) + })?; + + Ok(Self::date_from_naive_date(t)) + } + + /// Create a date from calendar date (year, month and day). + /// + /// See [`NaiveDate::from_ymd_opt`]. + /// + /// Example: + /// + ///```rust + /// use iceberg::spec::Literal; + /// let t = Literal::date_from_ymd(1970, 1, 5).unwrap(); + /// + /// assert_eq!(Literal::date(4), t); + /// ``` + pub fn date_from_ymd(year: i32, month: u32, day: u32) -> Result { + let t = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Can't create date from year: {year}, month: {month}, day: {day}"), + ) + })?; + + Ok(Self::date_from_naive_date(t)) + } + + /// Creates time in microseconds directly + pub fn time(value: i64) -> Self { + Self::Primitive(PrimitiveLiteral::Time(value)) + } + + /// Creates time literal from [`chrono::NaiveTime`]. + fn time_from_naive_time(t: NaiveTime) -> Self { + let duration = t - Self::unix_epoch().time(); + // It's safe to unwrap here since less than 24 hours will never overflow. + let micro_secs = duration.num_microseconds().unwrap(); + + Literal::time(micro_secs) + } + + /// Creates time in microseconds in `%H:%M:%S:.f` format. + /// + /// See [`NaiveTime::from_str`] for details. + /// + /// Example: + /// ```rust + /// use iceberg::spec::Literal; + /// let t = Literal::time_from_str("01:02:01.888999777").unwrap(); + /// + /// let micro_secs = { + /// 1 * 3600 * 1_000_000 + // 1 hour + /// 2 * 60 * 1_000_000 + // 2 minutes + /// 1 * 1_000_000 + // 1 second + /// 888999 // microseconds + /// }; + /// assert_eq!(Literal::time(micro_secs), t); + /// ``` + pub fn time_from_str>(s: S) -> Result { + let t = s.as_ref().parse::().map_err(|e| { + Error::new( + ErrorKind::DataInvalid, + format!("Can't parse time from string: {}", s.as_ref()), + ) + .with_source(e) + })?; + + Ok(Self::time_from_naive_time(t)) + } + + /// Creates time literal from hour, minute, second, and microseconds. + /// + /// See [`NaiveTime::from_hms_micro_opt`]. + /// + /// Example: + /// ```rust + /// + /// use iceberg::spec::Literal; + /// let t = Literal::time_from_hms_micro(22, 15, 33, 111).unwrap(); + /// + /// assert_eq!(Literal::time_from_str("22:15:33.000111").unwrap(), t); + /// ``` + pub fn time_from_hms_micro(hour: u32, min: u32, sec: u32, micro: u32) -> Result { + let t = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) + .ok_or_else(|| Error::new( + ErrorKind::DataInvalid, + format!("Can't create time from hour: {hour}, min: {min}, second: {sec}, microsecond: {micro}"), + ))?; + Ok(Self::time_from_naive_time(t)) + } + + /// Creates a timestamp from unix epoch in microseconds. + pub fn timestamp(value: i64) -> Self { + Self::Primitive(PrimitiveLiteral::Timestamp(value)) + } + + /// Creates a timestamp with timezone from unix epoch in microseconds. + pub fn timestamptz(value: i64) -> Self { + Self::Primitive(PrimitiveLiteral::TimestampTZ(value)) + } + + /// Creates a timestamp from [`DateTime`]. + pub fn timestamp_from_datetime(dt: DateTime) -> Self { + Self::timestamp(dt.with_timezone(&Utc).timestamp_micros()) + } + + /// Creates a timestamp with timezone from [`DateTime`]. + pub fn timestamptz_from_datetime(dt: DateTime) -> Self { + Self::timestamptz(dt.with_timezone(&Utc).timestamp_micros()) + } + + /// Parse a timestamp in RFC3339 format. + /// + /// See [`DateTime::from_str`]. + /// + /// Example: + /// + /// ```rust + /// use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; + /// use iceberg::spec::Literal; + /// let t = Literal::timestamp_from_str("2012-12-12 12:12:12.8899-04:00").unwrap(); + /// + /// let t2 = { + /// let date = NaiveDate::from_ymd_opt(2012, 12, 12).unwrap(); + /// let time = NaiveTime::from_hms_micro_opt(12, 12, 12, 889900).unwrap(); + /// let dt = NaiveDateTime::new(date, time); + /// Literal::timestamp_from_datetime(DateTime::::from_local(dt, FixedOffset::west_opt(4 * 3600).unwrap())) + /// }; + /// + /// assert_eq!(t, t2); + /// ``` + pub fn timestamp_from_str>(s: S) -> Result { + let dt = DateTime::::from_str(s.as_ref()).map_err(|e| { + Error::new(ErrorKind::DataInvalid, "Can't parse datetime.").with_source(e) + })?; + + Ok(Self::timestamp_from_datetime(dt)) + } + + /// Similar to [`Literal::timestamp_from_str`], but return timestamp with timezone literal. + pub fn timestamptz_from_str>(s: S) -> Result { + let dt = DateTime::::from_str(s.as_ref()).map_err(|e| { + Error::new(ErrorKind::DataInvalid, "Can't parse datetime.").with_source(e) + })?; + + Ok(Self::timestamptz_from_datetime(dt)) + } + + /// Creates a string literal. + pub fn string(s: S) -> Self { + Self::Primitive(PrimitiveLiteral::String(s.to_string())) + } + + /// Creates uuid literal. + pub fn uuid(uuid: Uuid) -> Self { + Self::Primitive(PrimitiveLiteral::UUID(uuid)) + } + + /// Creates uuid from str. See [`Uuid::parse_str`]. + /// + /// Example: + /// + /// ```rust + /// use uuid::Uuid; + /// use iceberg::spec::Literal; + /// let t1 = Literal::uuid_from_str("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8").unwrap(); + /// let t2 = Literal::uuid(Uuid::from_u128_le(0xd8d7d6d5d4d3d2d1c2c1b2b1a4a3a2a1)); + /// + /// assert_eq!(t1, t2); + /// ``` + pub fn uuid_from_str>(s: S) -> Result { + let uuid = Uuid::parse_str(s.as_ref()).map_err(|e| { + Error::new( + ErrorKind::DataInvalid, + format!("Can't parse uuid from string: {}", s.as_ref()), + ) + .with_source(e) + })?; + Ok(Self::uuid(uuid)) + } + + /// Creates a fixed literal from bytes. + /// + /// Example: + /// + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t1 = Literal::fixed(vec![1u8, 2u8]); + /// let t2 = Literal::Primitive(PrimitiveLiteral::Fixed(vec![1u8, 2u8])); + /// + /// assert_eq!(t1, t2); + /// ``` + pub fn fixed>(input: I) -> Self { + Literal::Primitive(PrimitiveLiteral::Fixed(input.into_iter().collect())) + } + + /// Creates a binary literal from bytes. + /// + /// Example: + /// + /// ```rust + /// use iceberg::spec::{Literal, PrimitiveLiteral}; + /// let t1 = Literal::binary(vec![1u8, 2u8]); + /// let t2 = Literal::Primitive(PrimitiveLiteral::Binary(vec![1u8, 2u8])); + /// + /// assert_eq!(t1, t2); + /// ``` + pub fn binary>(input: I) -> Self { + Literal::Primitive(PrimitiveLiteral::Binary(input.into_iter().collect())) + } + + /// Creates a decimal literal. + pub fn decimal(decimal: i128) -> Self { + Self::Primitive(PrimitiveLiteral::Decimal(decimal)) + } + + /// Creates decimal literal from string. See [`Decimal::from_str_exact`]. + /// + /// Example: + /// + /// ```rust + /// use rust_decimal::Decimal; + /// use iceberg::spec::Literal; + /// let t1 = Literal::decimal(12345); + /// let t2 = Literal::decimal_from_str("123.45").unwrap(); + /// + /// assert_eq!(t1, t2); + /// ``` + pub fn decimal_from_str>(s: S) -> Result { + let decimal = Decimal::from_str_exact(s.as_ref()).map_err(|e| { + Error::new(ErrorKind::DataInvalid, "Can't parse decimal.").with_source(e) + })?; + Ok(Self::decimal(decimal.mantissa())) + } +} + +impl From for ByteBuf { + fn from(value: Literal) -> Self { + match value { + Literal::Primitive(prim) => match prim { + PrimitiveLiteral::Boolean(val) => { + if val { + ByteBuf::from([1u8]) + } else { + ByteBuf::from([0u8]) + } + } + PrimitiveLiteral::Int(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Long(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Float(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Double(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Date(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Time(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::Timestamp(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::TimestampTZ(val) => ByteBuf::from(val.to_le_bytes()), + PrimitiveLiteral::String(val) => ByteBuf::from(val.as_bytes()), + PrimitiveLiteral::UUID(val) => ByteBuf::from(val.as_u128().to_be_bytes()), + PrimitiveLiteral::Fixed(val) => ByteBuf::from(val), + PrimitiveLiteral::Binary(val) => ByteBuf::from(val), + PrimitiveLiteral::Decimal(_) => todo!(), + }, + _ => unimplemented!(), + } + } +} + +impl From for Vec { + fn from(value: Literal) -> Self { + match value { + Literal::Primitive(prim) => match prim { + PrimitiveLiteral::Boolean(val) => { + if val { + Vec::from([1u8]) + } else { + Vec::from([0u8]) + } + } + PrimitiveLiteral::Int(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Long(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Float(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Double(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Date(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Time(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::Timestamp(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::TimestampTZ(val) => Vec::from(val.to_le_bytes()), + PrimitiveLiteral::String(val) => Vec::from(val.as_bytes()), + PrimitiveLiteral::UUID(val) => Vec::from(val.as_u128().to_be_bytes()), + PrimitiveLiteral::Fixed(val) => val, + PrimitiveLiteral::Binary(val) => val, + PrimitiveLiteral::Decimal(_) => todo!(), + }, + _ => unimplemented!(), + } + } +} + +/// The partition struct stores the tuple of partition values for each file. +/// Its type is derived from the partition fields of the partition spec used to write the manifest file. +/// In v2, the partition struct’s field ids must match the ids from the partition spec. +#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub struct Struct { + /// Vector to store the field values + fields: Vec, + /// Null bitmap + null_bitmap: BitVec, +} + +impl Struct { + /// Create a empty struct. + pub fn empty() -> Self { + Self { + fields: Vec::new(), + null_bitmap: BitVec::new(), + } + } + + /// Create a iterator to read the field in order of field_value. + pub fn iter(&self) -> impl Iterator> { + self.null_bitmap.iter().zip(self.fields.iter()).map( + |(null, value)| { + if *null { + None + } else { + Some(value) + } + }, + ) + } +} + +/// An iterator that moves out of a struct. +pub struct StructValueIntoIter { + null_bitmap: bitvec::boxed::IntoIter, + fields: std::vec::IntoIter, +} + +impl Iterator for StructValueIntoIter { + type Item = Option; + + fn next(&mut self) -> Option { + match (self.null_bitmap.next(), self.fields.next()) { + (Some(null), Some(value)) => Some(if null { None } else { Some(value) }), + _ => None, + } + } +} + +impl IntoIterator for Struct { + type Item = Option; + + type IntoIter = StructValueIntoIter; + + fn into_iter(self) -> Self::IntoIter { + StructValueIntoIter { + null_bitmap: self.null_bitmap.into_iter(), + fields: self.fields.into_iter(), + } + } +} + +impl FromIterator> for Struct { + fn from_iter>>(iter: I) -> Self { + let mut fields = Vec::new(); + let mut null_bitmap = BitVec::new(); + + for value in iter.into_iter() { + match value { + Some(value) => { + fields.push(value); + null_bitmap.push(false) + } + None => { + fields.push(Literal::Primitive(PrimitiveLiteral::Boolean(false))); + null_bitmap.push(true) + } + } + } + Struct { + fields, + null_bitmap, + } + } +} + +impl Literal { + /// Create iceberg value from bytes + pub fn try_from_bytes(bytes: &[u8], data_type: &Type) -> Result { + match data_type { + Type::Primitive(primitive) => match primitive { + PrimitiveType::Boolean => { + if bytes.len() == 1 && bytes[0] == 0u8 { + Ok(Literal::Primitive(PrimitiveLiteral::Boolean(false))) + } else { + Ok(Literal::Primitive(PrimitiveLiteral::Boolean(true))) + } + } + PrimitiveType::Int => Ok(Literal::Primitive(PrimitiveLiteral::Int( + i32::from_le_bytes(bytes.try_into()?), + ))), + PrimitiveType::Long => Ok(Literal::Primitive(PrimitiveLiteral::Long( + i64::from_le_bytes(bytes.try_into()?), + ))), + PrimitiveType::Float => Ok(Literal::Primitive(PrimitiveLiteral::Float( + OrderedFloat(f32::from_le_bytes(bytes.try_into()?)), + ))), + PrimitiveType::Double => Ok(Literal::Primitive(PrimitiveLiteral::Double( + OrderedFloat(f64::from_le_bytes(bytes.try_into()?)), + ))), + PrimitiveType::Date => Ok(Literal::Primitive(PrimitiveLiteral::Date( + i32::from_le_bytes(bytes.try_into()?), + ))), + PrimitiveType::Time => Ok(Literal::Primitive(PrimitiveLiteral::Time( + i64::from_le_bytes(bytes.try_into()?), + ))), + PrimitiveType::Timestamp => Ok(Literal::Primitive(PrimitiveLiteral::Timestamp( + i64::from_le_bytes(bytes.try_into()?), + ))), + PrimitiveType::Timestamptz => Ok(Literal::Primitive( + PrimitiveLiteral::TimestampTZ(i64::from_le_bytes(bytes.try_into()?)), + )), + PrimitiveType::String => Ok(Literal::Primitive(PrimitiveLiteral::String( + std::str::from_utf8(bytes)?.to_string(), + ))), + PrimitiveType::Uuid => Ok(Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::from_u128(u128::from_be_bytes(bytes.try_into()?)), + ))), + PrimitiveType::Fixed(_) => Ok(Literal::Primitive(PrimitiveLiteral::Fixed( + Vec::from(bytes), + ))), + PrimitiveType::Binary => Ok(Literal::Primitive(PrimitiveLiteral::Binary( + Vec::from(bytes), + ))), + PrimitiveType::Decimal { + precision: _, + scale: _, + } => todo!(), + }, + _ => Err(Error::new( + crate::ErrorKind::DataInvalid, + "Converting bytes to non-primitive types is not supported.", + )), + } + } + + /// Create iceberg value from a json value + pub fn try_from_json(value: JsonValue, data_type: &Type) -> Result> { + match data_type { + Type::Primitive(primitive) => match (primitive, value) { + (PrimitiveType::Boolean, JsonValue::Bool(bool)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::Boolean(bool)))) + } + (PrimitiveType::Int, JsonValue::Number(number)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::Int( + number + .as_i64() + .ok_or(Error::new( + crate::ErrorKind::DataInvalid, + "Failed to convert json number to int", + ))? + .try_into()?, + )))) + } + (PrimitiveType::Long, JsonValue::Number(number)) => Ok(Some(Literal::Primitive( + PrimitiveLiteral::Long(number.as_i64().ok_or(Error::new( + crate::ErrorKind::DataInvalid, + "Failed to convert json number to long", + ))?), + ))), + (PrimitiveType::Float, JsonValue::Number(number)) => Ok(Some(Literal::Primitive( + PrimitiveLiteral::Float(OrderedFloat(number.as_f64().ok_or(Error::new( + crate::ErrorKind::DataInvalid, + "Failed to convert json number to float", + ))? as f32)), + ))), + (PrimitiveType::Double, JsonValue::Number(number)) => Ok(Some(Literal::Primitive( + PrimitiveLiteral::Double(OrderedFloat(number.as_f64().ok_or(Error::new( + crate::ErrorKind::DataInvalid, + "Failed to convert json number to double", + ))?)), + ))), + (PrimitiveType::Date, JsonValue::String(s)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::Date( + date::date_to_days(&NaiveDate::parse_from_str(&s, "%Y-%m-%d")?), + )))) + } + (PrimitiveType::Time, JsonValue::String(s)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::Time( + time::time_to_microseconds(&NaiveTime::parse_from_str(&s, "%H:%M:%S%.f")?), + )))) + } + (PrimitiveType::Timestamp, JsonValue::String(s)) => Ok(Some(Literal::Primitive( + PrimitiveLiteral::Timestamp(timestamp::datetime_to_microseconds( + &NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f")?, + )), + ))), + (PrimitiveType::Timestamptz, JsonValue::String(s)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::TimestampTZ( + timestamptz::datetimetz_to_microseconds(&Utc.from_utc_datetime( + &NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f+00:00")?, + )), + )))) + } + (PrimitiveType::String, JsonValue::String(s)) => { + Ok(Some(Literal::Primitive(PrimitiveLiteral::String(s)))) + } + (PrimitiveType::Uuid, JsonValue::String(s)) => Ok(Some(Literal::Primitive( + PrimitiveLiteral::UUID(Uuid::parse_str(&s)?), + ))), + (PrimitiveType::Fixed(_), JsonValue::String(_)) => todo!(), + (PrimitiveType::Binary, JsonValue::String(_)) => todo!(), + ( + PrimitiveType::Decimal { + precision: _, + scale, + }, + JsonValue::String(s), + ) => { + let mut decimal = Decimal::from_str_exact(&s)?; + decimal.rescale(*scale); + Ok(Some(Literal::Primitive(PrimitiveLiteral::Decimal( + decimal.mantissa(), + )))) + } + (_, JsonValue::Null) => Ok(None), + (i, j) => Err(Error::new( + crate::ErrorKind::DataInvalid, + format!( + "The json value {} doesn't fit to the iceberg type {}.", + j, i + ), + )), + }, + Type::Struct(schema) => { + if let JsonValue::Object(mut object) = value { + Ok(Some(Literal::Struct(Struct::from_iter( + schema.fields().iter().map(|field| { + object.remove(&field.id.to_string()).and_then(|value| { + Literal::try_from_json(value, &field.field_type) + .and_then(|value| { + value.ok_or(Error::new( + ErrorKind::DataInvalid, + "Key of map cannot be null", + )) + }) + .ok() + }) + }), + )))) + } else { + Err(Error::new( + crate::ErrorKind::DataInvalid, + "The json value for a struct type must be an object.", + )) + } + } + Type::List(list) => { + if let JsonValue::Array(array) = value { + Ok(Some(Literal::List( + array + .into_iter() + .map(|value| { + Literal::try_from_json(value, &list.element_field.field_type) + }) + .collect::>>()?, + ))) + } else { + Err(Error::new( + crate::ErrorKind::DataInvalid, + "The json value for a list type must be an array.", + )) + } + } + Type::Map(map) => { + if let JsonValue::Object(mut object) = value { + if let (Some(JsonValue::Array(keys)), Some(JsonValue::Array(values))) = + (object.remove("keys"), object.remove("values")) + { + Ok(Some(Literal::Map(BTreeMap::from_iter( + keys.into_iter() + .zip(values.into_iter()) + .map(|(key, value)| { + Ok(( + Literal::try_from_json(key, &map.key_field.field_type) + .and_then(|value| { + value.ok_or(Error::new( + ErrorKind::DataInvalid, + "Key of map cannot be null", + )) + })?, + Literal::try_from_json(value, &map.value_field.field_type)?, + )) + }) + .collect::>>()?, + )))) + } else { + Err(Error::new( + crate::ErrorKind::DataInvalid, + "The json value for a list type must be an array.", + )) + } + } else { + Err(Error::new( + crate::ErrorKind::DataInvalid, + "The json value for a list type must be an array.", + )) + } + } + } + } + + /// Converting iceberg value to json value. + /// + /// See [this spec](https://iceberg.apache.org/spec/#json-single-value-serialization) for reference. + pub fn try_into_json(self, r#type: &Type) -> Result { + match (self, r#type) { + (Literal::Primitive(prim), _) => match prim { + PrimitiveLiteral::Boolean(val) => Ok(JsonValue::Bool(val)), + PrimitiveLiteral::Int(val) => Ok(JsonValue::Number((val).into())), + PrimitiveLiteral::Long(val) => Ok(JsonValue::Number((val).into())), + PrimitiveLiteral::Float(val) => match Number::from_f64(val.0 as f64) { + Some(number) => Ok(JsonValue::Number(number)), + None => Ok(JsonValue::Null), + }, + PrimitiveLiteral::Double(val) => match Number::from_f64(val.0) { + Some(number) => Ok(JsonValue::Number(number)), + None => Ok(JsonValue::Null), + }, + PrimitiveLiteral::Date(val) => { + Ok(JsonValue::String(date::days_to_date(val).to_string())) + } + PrimitiveLiteral::Time(val) => Ok(JsonValue::String( + time::microseconds_to_time(val).to_string(), + )), + PrimitiveLiteral::Timestamp(val) => Ok(JsonValue::String( + timestamp::microseconds_to_datetime(val) + .format("%Y-%m-%dT%H:%M:%S%.f") + .to_string(), + )), + PrimitiveLiteral::TimestampTZ(val) => Ok(JsonValue::String( + timestamptz::microseconds_to_datetimetz(val) + .format("%Y-%m-%dT%H:%M:%S%.f+00:00") + .to_string(), + )), + PrimitiveLiteral::String(val) => Ok(JsonValue::String(val.clone())), + PrimitiveLiteral::UUID(val) => Ok(JsonValue::String(val.to_string())), + PrimitiveLiteral::Fixed(val) => Ok(JsonValue::String(val.iter().fold( + String::new(), + |mut acc, x| { + acc.push_str(&format!("{:x}", x)); + acc + }, + ))), + PrimitiveLiteral::Binary(val) => Ok(JsonValue::String(val.iter().fold( + String::new(), + |mut acc, x| { + acc.push_str(&format!("{:x}", x)); + acc + }, + ))), + PrimitiveLiteral::Decimal(val) => match r#type { + Type::Primitive(PrimitiveType::Decimal { + precision: _precision, + scale, + }) => { + let decimal = Decimal::try_from_i128_with_scale(val, *scale)?; + Ok(JsonValue::String(decimal.to_string())) + } + _ => Err(Error::new( + ErrorKind::DataInvalid, + "The iceberg type for decimal literal must be decimal.", + ))?, + }, + }, + (Literal::Struct(s), Type::Struct(struct_type)) => { + let mut id_and_value = Vec::with_capacity(struct_type.fields().len()); + for (value, field) in s.into_iter().zip(struct_type.fields()) { + let json = match value { + Some(val) => val.try_into_json(&field.field_type)?, + None => JsonValue::Null, + }; + id_and_value.push((field.id.to_string(), json)); + } + Ok(JsonValue::Object(JsonMap::from_iter(id_and_value))) + } + (Literal::List(list), Type::List(list_type)) => Ok(JsonValue::Array( + list.into_iter() + .map(|opt| match opt { + Some(literal) => literal.try_into_json(&list_type.element_field.field_type), + None => Ok(JsonValue::Null), + }) + .collect::>>()?, + )), + (Literal::Map(map), Type::Map(map_type)) => { + let mut object = JsonMap::with_capacity(2); + let mut json_keys = Vec::with_capacity(map.len()); + let mut json_values = Vec::with_capacity(map.len()); + for (key, value) in map.into_iter() { + json_keys.push(key.try_into_json(&map_type.key_field.field_type)?); + json_values.push(match value { + Some(literal) => literal.try_into_json(&map_type.value_field.field_type)?, + None => JsonValue::Null, + }); + } + object.insert("keys".to_string(), JsonValue::Array(json_keys)); + object.insert("values".to_string(), JsonValue::Array(json_values)); + Ok(JsonValue::Object(object)) + } + (value, r#type) => Err(Error::new( + ErrorKind::DataInvalid, + format!( + "The iceberg value {:?} doesn't fit to the iceberg type {}.", + value, r#type + ), + )), + } + } + + /// Convert Value to the any type + pub fn into_any(self) -> Box { + match self { + Literal::Primitive(prim) => match prim { + PrimitiveLiteral::Boolean(any) => Box::new(any), + PrimitiveLiteral::Int(any) => Box::new(any), + PrimitiveLiteral::Long(any) => Box::new(any), + PrimitiveLiteral::Float(any) => Box::new(any), + PrimitiveLiteral::Double(any) => Box::new(any), + PrimitiveLiteral::Date(any) => Box::new(any), + PrimitiveLiteral::Time(any) => Box::new(any), + PrimitiveLiteral::Timestamp(any) => Box::new(any), + PrimitiveLiteral::TimestampTZ(any) => Box::new(any), + PrimitiveLiteral::Fixed(any) => Box::new(any), + PrimitiveLiteral::Binary(any) => Box::new(any), + PrimitiveLiteral::String(any) => Box::new(any), + PrimitiveLiteral::UUID(any) => Box::new(any), + PrimitiveLiteral::Decimal(any) => Box::new(any), + }, + _ => unimplemented!(), + } + } +} + +mod date { + use chrono::{NaiveDate, NaiveDateTime}; + + pub(crate) fn date_to_days(date: &NaiveDate) -> i32 { + date.signed_duration_since( + // This is always the same and shouldn't fail + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + ) + .num_days() as i32 + } + + pub(crate) fn days_to_date(days: i32) -> NaiveDate { + // This shouldn't fail until the year 262000 + NaiveDateTime::from_timestamp_opt(days as i64 * 86_400, 0) + .unwrap() + .date() + } +} + +mod time { + use chrono::NaiveTime; + + pub(crate) fn time_to_microseconds(time: &NaiveTime) -> i64 { + time.signed_duration_since( + // This is always the same and shouldn't fail + NaiveTime::from_num_seconds_from_midnight_opt(0, 0).unwrap(), + ) + .num_microseconds() + .unwrap() + } + + pub(crate) fn microseconds_to_time(micros: i64) -> NaiveTime { + let (secs, rem) = (micros / 1_000_000, micros % 1_000_000); + + NaiveTime::from_num_seconds_from_midnight_opt(secs as u32, rem as u32 * 1_000).unwrap() + } +} + +mod timestamp { + use chrono::NaiveDateTime; + + pub(crate) fn datetime_to_microseconds(time: &NaiveDateTime) -> i64 { + time.timestamp_micros() + } + + pub(crate) fn microseconds_to_datetime(micros: i64) -> NaiveDateTime { + let (secs, rem) = (micros / 1_000_000, micros % 1_000_000); + + // This shouldn't fail until the year 262000 + NaiveDateTime::from_timestamp_opt(secs, rem as u32 * 1_000).unwrap() + } +} + +mod timestamptz { + use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; + + pub(crate) fn datetimetz_to_microseconds(time: &DateTime) -> i64 { + time.timestamp_micros() + } + + pub(crate) fn microseconds_to_datetimetz(micros: i64) -> DateTime { + let (secs, rem) = (micros / 1_000_000, micros % 1_000_000); + + Utc.from_utc_datetime( + // This shouldn't fail until the year 262000 + &NaiveDateTime::from_timestamp_opt(secs, rem as u32 * 1_000).unwrap(), + ) + } +} + +mod _serde { + use std::collections::BTreeMap; + + use crate::{ + spec::{PrimitiveType, Type, MAP_KEY_FIELD_NAME, MAP_VALUE_FIELD_NAME}, + Error, ErrorKind, + }; + + use super::{Literal, PrimitiveLiteral}; + use serde::{ + de::Visitor, + ser::{SerializeMap, SerializeSeq, SerializeStruct}, + Deserialize, Serialize, + }; + use serde_bytes::ByteBuf; + use serde_derive::Deserialize as DeserializeDerive; + use serde_derive::Serialize as SerializeDerive; + + #[derive(SerializeDerive, DeserializeDerive, Debug)] + #[serde(transparent)] + /// Raw literal representation used for serde. The serialize way is used for Avro serializer. + pub struct RawLiteral(RawLiteralEnum); + + impl RawLiteral { + /// Covert literal to raw literal. + pub fn try_from(literal: Literal, ty: &Type) -> Result { + Ok(Self(RawLiteralEnum::try_from(literal, ty)?)) + } + + /// Convert raw literal to literal. + pub fn try_into(self, ty: &Type) -> Result, Error> { + self.0.try_into(ty) + } + } + + #[derive(SerializeDerive, Clone, Debug)] + #[serde(untagged)] + enum RawLiteralEnum { + Null, + Boolean(bool), + Int(i32), + Long(i64), + Float(f32), + Double(f64), + String(String), + Bytes(ByteBuf), + List(List), + StringMap(StringMap), + Record(Record), + } + + #[derive(Clone, Debug)] + struct Record { + required: Vec<(String, RawLiteralEnum)>, + optional: Vec<(String, Option)>, + } + + impl Serialize for Record { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let len = self.required.len() + self.optional.len(); + let mut record = serializer.serialize_struct("", len)?; + for (k, v) in &self.required { + record.serialize_field(Box::leak(k.clone().into_boxed_str()), &v)?; + } + for (k, v) in &self.optional { + record.serialize_field(Box::leak(k.clone().into_boxed_str()), &v)?; + } + record.end() + } + } + + #[derive(Clone, Debug)] + struct List { + list: Vec>, + required: bool, + } + + impl Serialize for List { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.list.len()))?; + for value in &self.list { + if self.required { + seq.serialize_element(value.as_ref().ok_or_else(|| { + serde::ser::Error::custom( + "List element is required, element cannot be null", + ) + })?)?; + } else { + seq.serialize_element(&value)?; + } + } + seq.end() + } + } + + #[derive(Clone, Debug)] + struct StringMap { + raw: Vec<(String, Option)>, + required: bool, + } + + impl Serialize for StringMap { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.raw.len()))?; + for (k, v) in &self.raw { + if self.required { + map.serialize_entry( + k, + v.as_ref().ok_or_else(|| { + serde::ser::Error::custom( + "Map element is required, element cannot be null", + ) + })?, + )?; + } else { + map.serialize_entry(k, v)?; + } + } + map.end() + } + } + + impl<'de> Deserialize<'de> for RawLiteralEnum { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct RawLiteralVisitor; + impl<'de> Visitor<'de> for RawLiteralVisitor { + type Value = RawLiteralEnum; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("expect") + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Boolean(v)) + } + + fn visit_i32(self, v: i32) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Int(v)) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Long(v)) + } + + /// Used in json + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Long(v as i64)) + } + + fn visit_f32(self, v: f32) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Float(v)) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Double(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::String(v.to_string())) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Bytes(ByteBuf::from(v))) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::String(v.to_string())) + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(RawLiteralEnum::Null) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut required = Vec::new(); + while let Some(key) = map.next_key::()? { + let value = map.next_value::()?; + required.push((key, value)); + } + Ok(RawLiteralEnum::Record(Record { + required, + optional: Vec::new(), + })) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut list = Vec::new(); + while let Some(value) = seq.next_element::()? { + list.push(Some(value)); + } + Ok(RawLiteralEnum::List(List { + list, + // `required` only used in serialize, just set default in deserialize. + required: false, + })) + } + } + deserializer.deserialize_any(RawLiteralVisitor) + } + } + + impl RawLiteralEnum { + pub fn try_from(literal: Literal, ty: &Type) -> Result { + let raw = match literal { + Literal::Primitive(prim) => match prim { + super::PrimitiveLiteral::Boolean(v) => RawLiteralEnum::Boolean(v), + super::PrimitiveLiteral::Int(v) => RawLiteralEnum::Int(v), + super::PrimitiveLiteral::Long(v) => RawLiteralEnum::Long(v), + super::PrimitiveLiteral::Float(v) => RawLiteralEnum::Float(v.0), + super::PrimitiveLiteral::Double(v) => RawLiteralEnum::Double(v.0), + super::PrimitiveLiteral::Date(v) => RawLiteralEnum::Int(v), + super::PrimitiveLiteral::Time(v) => RawLiteralEnum::Long(v), + super::PrimitiveLiteral::Timestamp(v) => RawLiteralEnum::Long(v), + super::PrimitiveLiteral::TimestampTZ(v) => RawLiteralEnum::Long(v), + super::PrimitiveLiteral::String(v) => RawLiteralEnum::String(v), + super::PrimitiveLiteral::UUID(v) => { + RawLiteralEnum::Bytes(ByteBuf::from(v.as_u128().to_be_bytes())) + } + super::PrimitiveLiteral::Fixed(v) => RawLiteralEnum::Bytes(ByteBuf::from(v)), + super::PrimitiveLiteral::Binary(v) => RawLiteralEnum::Bytes(ByteBuf::from(v)), + super::PrimitiveLiteral::Decimal(v) => { + RawLiteralEnum::Bytes(ByteBuf::from(v.to_be_bytes())) + } + }, + Literal::Struct(r#struct) => { + let mut required = Vec::new(); + let mut optional = Vec::new(); + if let Type::Struct(struct_ty) = ty { + for (value, field) in r#struct.into_iter().zip(struct_ty.fields()) { + if field.required { + if let Some(value) = value { + required.push(( + field.name.clone(), + RawLiteralEnum::try_from(value, &field.field_type)?, + )); + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + "Can't convert null to required field", + )); + } + } else if let Some(value) = value { + optional.push(( + field.name.clone(), + Some(RawLiteralEnum::try_from(value, &field.field_type)?), + )); + } else { + optional.push((field.name.clone(), None)); + } + } + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + format!("Type {} should be a struct", ty), + )); + } + RawLiteralEnum::Record(Record { required, optional }) + } + Literal::List(list) => { + if let Type::List(list_ty) = ty { + let list = list + .into_iter() + .map(|v| { + v.map(|v| { + RawLiteralEnum::try_from(v, &list_ty.element_field.field_type) + }) + .transpose() + }) + .collect::>()?; + RawLiteralEnum::List(List { + list, + required: list_ty.element_field.required, + }) + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + format!("Type {} should be a list", ty), + )); + } + } + Literal::Map(map) => { + if let Type::Map(map_ty) = ty { + if let Type::Primitive(PrimitiveType::String) = *map_ty.key_field.field_type + { + let mut raw = Vec::with_capacity(map.len()); + for (k, v) in map { + if let Literal::Primitive(PrimitiveLiteral::String(k)) = k { + raw.push(( + k, + v.map(|v| { + RawLiteralEnum::try_from( + v, + &map_ty.value_field.field_type, + ) + }) + .transpose()?, + )); + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + "literal type is inconsistent with type", + )); + } + } + RawLiteralEnum::StringMap(StringMap { + raw, + required: map_ty.value_field.required, + }) + } else { + let list = map.into_iter().map(|(k,v)| { + let raw_k = + RawLiteralEnum::try_from(k, &map_ty.key_field.field_type)?; + let raw_v = v + .map(|v| { + RawLiteralEnum::try_from(v, &map_ty.value_field.field_type) + }) + .transpose()?; + if map_ty.value_field.required { + Ok(Some(RawLiteralEnum::Record(Record { + required: vec![ + (MAP_KEY_FIELD_NAME.to_string(), raw_k), + (MAP_VALUE_FIELD_NAME.to_string(), raw_v.ok_or_else(||Error::new(ErrorKind::DataInvalid, "Map value is required, value cannot be null"))?), + ], + optional: vec![], + }))) + } else { + Ok(Some(RawLiteralEnum::Record(Record { + required: vec![ + (MAP_KEY_FIELD_NAME.to_string(), raw_k), + ], + optional: vec![ + (MAP_VALUE_FIELD_NAME.to_string(), raw_v) + ], + }))) + } + }).collect::>()?; + RawLiteralEnum::List(List { + list, + required: true, + }) + } + } else { + return Err(Error::new( + ErrorKind::DataInvalid, + format!("Type {} should be a map", ty), + )); + } + } + }; + Ok(raw) + } + + pub fn try_into(self, ty: &Type) -> Result, Error> { + let invalid_err = |v: &str| { + Error::new( + ErrorKind::DataInvalid, + format!( + "Unable to convert raw literal ({}) fail convert to type {} for: type mismatch", + v, ty + ), + ) + }; + let invalid_err_with_reason = |v: &str, reason: &str| { + Error::new( + ErrorKind::DataInvalid, + format!( + "Unable to convert raw literal ({}) fail convert to type {} for: {}", + v, ty, reason + ), + ) + }; + match self { + RawLiteralEnum::Null => Ok(None), + RawLiteralEnum::Boolean(v) => Ok(Some(Literal::bool(v))), + RawLiteralEnum::Int(v) => match ty { + Type::Primitive(PrimitiveType::Int) => Ok(Some(Literal::int(v))), + Type::Primitive(PrimitiveType::Date) => Ok(Some(Literal::date(v))), + _ => Err(invalid_err("int")), + }, + RawLiteralEnum::Long(v) => match ty { + Type::Primitive(PrimitiveType::Long) => Ok(Some(Literal::long(v))), + Type::Primitive(PrimitiveType::Time) => Ok(Some(Literal::time(v))), + Type::Primitive(PrimitiveType::Timestamp) => Ok(Some(Literal::timestamp(v))), + Type::Primitive(PrimitiveType::Timestamptz) => { + Ok(Some(Literal::timestamptz(v))) + } + _ => Err(invalid_err("long")), + }, + RawLiteralEnum::Float(v) => match ty { + Type::Primitive(PrimitiveType::Float) => Ok(Some(Literal::float(v))), + _ => Err(invalid_err("float")), + }, + RawLiteralEnum::Double(v) => match ty { + Type::Primitive(PrimitiveType::Double) => Ok(Some(Literal::double(v))), + _ => Err(invalid_err("double")), + }, + RawLiteralEnum::String(v) => match ty { + Type::Primitive(PrimitiveType::String) => Ok(Some(Literal::string(v))), + _ => Err(invalid_err("string")), + }, + // # TODO:https://github.com/apache/iceberg-rust/issues/86 + // rust avro don't support deserialize any bytes representation now. + RawLiteralEnum::Bytes(_) => Err(invalid_err_with_reason( + "bytes", + "todo: rust avro doesn't support deserialize any bytes representation now", + )), + RawLiteralEnum::List(v) => { + match ty { + Type::List(ty) => Ok(Some(Literal::List( + v.list + .into_iter() + .map(|v| { + if let Some(v) = v { + v.try_into(&ty.element_field.field_type) + } else { + Ok(None) + } + }) + .collect::>()?, + ))), + Type::Map(map_ty) => { + let key_ty = map_ty.key_field.field_type.as_ref(); + let value_ty = map_ty.value_field.field_type.as_ref(); + let mut map = BTreeMap::new(); + for k_v in v.list { + let k_v = k_v.ok_or_else(|| invalid_err_with_reason("list","In deserialize, None will be represented as Some(RawLiteral::Null), all element in list must be valid"))?; + if let RawLiteralEnum::Record(Record { + required, + optional: _, + }) = k_v + { + if required.len() != 2 { + return Err(invalid_err_with_reason("list","Record must contains two element(key and value) of array")); + } + let mut key = None; + let mut value = None; + required.into_iter().for_each(|(k, v)| { + if k == MAP_KEY_FIELD_NAME { + key = Some(v); + } else if k == MAP_VALUE_FIELD_NAME { + value = Some(v); + } + }); + match (key, value) { + (Some(k), Some(v)) => { + let key = k.try_into(key_ty)?.ok_or_else(|| { + invalid_err_with_reason( + "list", + "Key element in Map must be valid", + ) + })?; + let value = v.try_into(value_ty)?; + if map_ty.value_field.required && value.is_none() { + return Err(invalid_err_with_reason( + "list", + "Value element is required in this Map", + )); + } + map.insert(key, value); + } + _ => return Err(invalid_err_with_reason( + "list", + "The elements of record in list are not key and value", + )), + } + } else { + return Err(invalid_err_with_reason( + "list", + "Map should represented as record array.", + )); + } + } + Ok(Some(Literal::Map(map))) + } + _ => Err(invalid_err("list")), + } + } + RawLiteralEnum::Record(Record { + required, + optional: _, + }) => match ty { + Type::Struct(struct_ty) => { + let iters: Vec> = required + .into_iter() + .map(|(field_name, value)| { + let field = struct_ty + .field_by_name(field_name.as_str()) + .ok_or_else(|| { + invalid_err_with_reason( + "record", + &format!("field {} is not exist", &field_name), + ) + })?; + let value = value.try_into(&field.field_type)?; + Ok(value) + }) + .collect::>()?; + Ok(Some(Literal::Struct(super::Struct::from_iter(iters)))) + } + Type::Map(map_ty) => { + if *map_ty.key_field.field_type != Type::Primitive(PrimitiveType::String) { + return Err(invalid_err_with_reason( + "record", + "Map key must be string", + )); + } + let mut map = BTreeMap::new(); + for (k, v) in required { + let value = v.try_into(&map_ty.value_field.field_type)?; + if map_ty.value_field.required && value.is_none() { + return Err(invalid_err_with_reason( + "record", + "Value element is required in this Map", + )); + } + map.insert(Literal::string(k), value); + } + Ok(Some(Literal::Map(map))) + } + _ => Err(invalid_err("record")), + }, + RawLiteralEnum::StringMap(_) => Err(invalid_err("string map")), + } + } + } +} + +#[cfg(test)] +mod tests { + + use apache_avro::{to_value, types::Value}; + + use crate::{ + avro::schema_to_avro_schema, + spec::{ + datatypes::{ListType, MapType, NestedField, StructType}, + Schema, + }, + }; + + use super::*; + + fn check_json_serde(json: &str, expected_literal: Literal, expected_type: &Type) { + let raw_json_value = serde_json::from_str::(json).unwrap(); + let desered_literal = + Literal::try_from_json(raw_json_value.clone(), expected_type).unwrap(); + assert_eq!(desered_literal, Some(expected_literal.clone())); + + let expected_json_value: JsonValue = expected_literal.try_into_json(expected_type).unwrap(); + let sered_json = serde_json::to_string(&expected_json_value).unwrap(); + let parsed_json_value = serde_json::from_str::(&sered_json).unwrap(); + + assert_eq!(parsed_json_value, raw_json_value); + } + + fn check_avro_bytes_serde(input: Vec, expected_literal: Literal, expected_type: &Type) { + let raw_schema = r#""bytes""#; + let schema = apache_avro::Schema::parse_str(raw_schema).unwrap(); + + let bytes = ByteBuf::from(input); + let literal = Literal::try_from_bytes(&bytes, expected_type).unwrap(); + assert_eq!(literal, expected_literal); + + let mut writer = apache_avro::Writer::new(&schema, Vec::new()); + writer.append_ser(ByteBuf::from(literal)).unwrap(); + let encoded = writer.into_inner().unwrap(); + let reader = apache_avro::Reader::with_schema(&schema, &*encoded).unwrap(); + + for record in reader { + let result = apache_avro::from_value::(&record.unwrap()).unwrap(); + let desered_literal = Literal::try_from_bytes(&result, expected_type).unwrap(); + assert_eq!(desered_literal, expected_literal); + } + } + + fn check_convert_with_avro(expected_literal: Literal, expected_type: &Type) { + let fields = vec![NestedField::required(1, "col", expected_type.clone()).into()]; + let schema = Schema::builder() + .with_fields(fields.clone()) + .build() + .unwrap(); + let avro_schema = schema_to_avro_schema("test", &schema).unwrap(); + let struct_type = Type::Struct(StructType::new(fields)); + let struct_literal = + Literal::Struct(Struct::from_iter(vec![Some(expected_literal.clone())])); + + let mut writer = apache_avro::Writer::new(&avro_schema, Vec::new()); + let raw_literal = RawLiteral::try_from(struct_literal.clone(), &struct_type).unwrap(); + writer.append_ser(raw_literal).unwrap(); + let encoded = writer.into_inner().unwrap(); + + let reader = apache_avro::Reader::new(&*encoded).unwrap(); + for record in reader { + let result = apache_avro::from_value::(&record.unwrap()).unwrap(); + let desered_literal = result.try_into(&struct_type).unwrap().unwrap(); + assert_eq!(desered_literal, struct_literal); + } + } + + fn check_serialize_avro(literal: Literal, ty: &Type, expect_value: Value) { + let expect_value = Value::Record(vec![("col".to_string(), expect_value)]); + + let fields = vec![NestedField::required(1, "col", ty.clone()).into()]; + let schema = Schema::builder() + .with_fields(fields.clone()) + .build() + .unwrap(); + let avro_schema = schema_to_avro_schema("test", &schema).unwrap(); + let struct_type = Type::Struct(StructType::new(fields)); + let struct_literal = Literal::Struct(Struct::from_iter(vec![Some(literal.clone())])); + let mut writer = apache_avro::Writer::new(&avro_schema, Vec::new()); + let raw_literal = RawLiteral::try_from(struct_literal.clone(), &struct_type).unwrap(); + let value = to_value(raw_literal) + .unwrap() + .resolve(&avro_schema) + .unwrap(); + writer.append_value_ref(&value).unwrap(); + let encoded = writer.into_inner().unwrap(); + + let reader = apache_avro::Reader::new(&*encoded).unwrap(); + for record in reader { + assert_eq!(record.unwrap(), expect_value); + } + } + + #[test] + fn json_boolean() { + let record = r#"true"#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Boolean(true)), + &Type::Primitive(PrimitiveType::Boolean), + ); + } + + #[test] + fn json_int() { + let record = r#"32"#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Int(32)), + &Type::Primitive(PrimitiveType::Int), + ); + } + + #[test] + fn json_long() { + let record = r#"32"#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Long(32)), + &Type::Primitive(PrimitiveType::Long), + ); + } + + #[test] + fn json_float() { + let record = r#"1.0"#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Float(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Float), + ); + } + + #[test] + fn json_double() { + let record = r#"1.0"#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Double(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Double), + ); + } + + #[test] + fn json_date() { + let record = r#""2017-11-16""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Date(17486)), + &Type::Primitive(PrimitiveType::Date), + ); + } + + #[test] + fn json_time() { + let record = r#""22:31:08.123456""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Time(81068123456)), + &Type::Primitive(PrimitiveType::Time), + ); + } + + #[test] + fn json_timestamp() { + let record = r#""2017-11-16T22:31:08.123456""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Timestamp(1510871468123456)), + &Type::Primitive(PrimitiveType::Timestamp), + ); + } + + #[test] + fn json_timestamptz() { + let record = r#""2017-11-16T22:31:08.123456+00:00""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::TimestampTZ(1510871468123456)), + &Type::Primitive(PrimitiveType::Timestamptz), + ); + } + + #[test] + fn json_string() { + let record = r#""iceberg""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::String("iceberg".to_string())), + &Type::Primitive(PrimitiveType::String), + ); + } + + #[test] + fn json_uuid() { + let record = r#""f79c3e09-677c-4bbd-a479-3f349cb785e7""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::UUID( + Uuid::parse_str("f79c3e09-677c-4bbd-a479-3f349cb785e7").unwrap(), + )), + &Type::Primitive(PrimitiveType::Uuid), + ); + } + + #[test] + fn json_decimal() { + let record = r#""14.20""#; + + check_json_serde( + record, + Literal::Primitive(PrimitiveLiteral::Decimal(1420)), + &Type::decimal(28, 2).unwrap(), + ); + } + + #[test] + fn json_struct() { + let record = r#"{"1": 1, "2": "bar", "3": null}"#; + + check_json_serde( + record, + Literal::Struct(Struct::from_iter(vec![ + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + Some(Literal::Primitive(PrimitiveLiteral::String( + "bar".to_string(), + ))), + None, + ])), + &Type::Struct(StructType::new(vec![ + NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::optional(2, "name", Type::Primitive(PrimitiveType::String)).into(), + NestedField::optional(3, "address", Type::Primitive(PrimitiveType::String)).into(), + ])), + ); + } + + #[test] + fn json_list() { + let record = r#"[1, 2, 3, null]"#; + + check_json_serde( + record, + Literal::List(vec![ + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + Some(Literal::Primitive(PrimitiveLiteral::Int(3))), + None, + ]), + &Type::List(ListType { + element_field: NestedField::list_element( + 0, + Type::Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + ); + } + + #[test] + fn json_map() { + let record = r#"{ "keys": ["a", "b", "c"], "values": [1, 2, null] }"#; + + check_json_serde( + record, + Literal::Map(BTreeMap::from([ + ( + Literal::Primitive(PrimitiveLiteral::String("a".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("b".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("c".to_string())), + None, + ), + ])), + &Type::Map(MapType { + key_field: NestedField::map_key_element(0, Type::Primitive(PrimitiveType::String)) + .into(), + value_field: NestedField::map_value_element( + 1, + Type::Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + ); + } + + #[test] + fn avro_bytes_boolean() { + let bytes = vec![1u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::Boolean(true)), + &Type::Primitive(PrimitiveType::Boolean), + ); + } + + #[test] + fn avro_bytes_int() { + let bytes = vec![32u8, 0u8, 0u8, 0u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::Int(32)), + &Type::Primitive(PrimitiveType::Int), + ); + } + + #[test] + fn avro_bytes_long() { + let bytes = vec![32u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::Long(32)), + &Type::Primitive(PrimitiveType::Long), + ); + } + + #[test] + fn avro_bytes_float() { + let bytes = vec![0u8, 0u8, 128u8, 63u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::Float(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Float), + ); + } + + #[test] + fn avro_bytes_double() { + let bytes = vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 240u8, 63u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::Double(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Double), + ); + } + + #[test] + fn avro_bytes_string() { + let bytes = vec![105u8, 99u8, 101u8, 98u8, 101u8, 114u8, 103u8]; + + check_avro_bytes_serde( + bytes, + Literal::Primitive(PrimitiveLiteral::String("iceberg".to_string())), + &Type::Primitive(PrimitiveType::String), + ); + } + + #[test] + fn avro_convert_test_int() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Int(32)), + &Type::Primitive(PrimitiveType::Int), + ); + } + + #[test] + fn avro_convert_test_long() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Long(32)), + &Type::Primitive(PrimitiveType::Long), + ); + } + + #[test] + fn avro_convert_test_float() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Float(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Float), + ); + } + + #[test] + fn avro_convert_test_double() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Double(OrderedFloat(1.0))), + &Type::Primitive(PrimitiveType::Double), + ); + } + + #[test] + fn avro_convert_test_string() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::String("iceberg".to_string())), + &Type::Primitive(PrimitiveType::String), + ); + } + + #[test] + fn avro_convert_test_date() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Date(17486)), + &Type::Primitive(PrimitiveType::Date), + ); + } + + #[test] + fn avro_convert_test_time() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Time(81068123456)), + &Type::Primitive(PrimitiveType::Time), + ); + } + + #[test] + fn avro_convert_test_timestamp() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::Timestamp(1510871468123456)), + &Type::Primitive(PrimitiveType::Timestamp), + ); + } + + #[test] + fn avro_convert_test_timestamptz() { + check_convert_with_avro( + Literal::Primitive(PrimitiveLiteral::TimestampTZ(1510871468123456)), + &Type::Primitive(PrimitiveType::Timestamptz), + ); + } + + #[test] + fn avro_convert_test_list() { + check_convert_with_avro( + Literal::List(vec![ + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + Some(Literal::Primitive(PrimitiveLiteral::Int(3))), + None, + ]), + &Type::List(ListType { + element_field: NestedField::list_element( + 0, + Type::Primitive(PrimitiveType::Int), + false, + ) + .into(), + }), + ); + + check_convert_with_avro( + Literal::List(vec![ + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + Some(Literal::Primitive(PrimitiveLiteral::Int(3))), + ]), + &Type::List(ListType { + element_field: NestedField::list_element( + 0, + Type::Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + ); + } + + #[test] + fn avro_convert_test_map() { + check_convert_with_avro( + Literal::Map(BTreeMap::from([ + ( + Literal::Primitive(PrimitiveLiteral::Int(1)), + Some(Literal::Primitive(PrimitiveLiteral::Long(1))), + ), + ( + Literal::Primitive(PrimitiveLiteral::Int(2)), + Some(Literal::Primitive(PrimitiveLiteral::Long(2))), + ), + (Literal::Primitive(PrimitiveLiteral::Int(3)), None), + ])), + &Type::Map(MapType { + key_field: NestedField::map_key_element(0, Type::Primitive(PrimitiveType::Int)) + .into(), + value_field: NestedField::map_value_element( + 1, + Type::Primitive(PrimitiveType::Long), + false, + ) + .into(), + }), + ); + + check_convert_with_avro( + Literal::Map(BTreeMap::from([ + ( + Literal::Primitive(PrimitiveLiteral::Int(1)), + Some(Literal::Primitive(PrimitiveLiteral::Long(1))), + ), + ( + Literal::Primitive(PrimitiveLiteral::Int(2)), + Some(Literal::Primitive(PrimitiveLiteral::Long(2))), + ), + ( + Literal::Primitive(PrimitiveLiteral::Int(3)), + Some(Literal::Primitive(PrimitiveLiteral::Long(3))), + ), + ])), + &Type::Map(MapType { + key_field: NestedField::map_key_element(0, Type::Primitive(PrimitiveType::Int)) + .into(), + value_field: NestedField::map_value_element( + 1, + Type::Primitive(PrimitiveType::Long), + true, + ) + .into(), + }), + ); + } + + #[test] + fn avro_convert_test_string_map() { + check_convert_with_avro( + Literal::Map(BTreeMap::from([ + ( + Literal::Primitive(PrimitiveLiteral::String("a".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("b".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("c".to_string())), + None, + ), + ])), + &Type::Map(MapType { + key_field: NestedField::map_key_element(0, Type::Primitive(PrimitiveType::String)) + .into(), + value_field: NestedField::map_value_element( + 1, + Type::Primitive(PrimitiveType::Int), + false, + ) + .into(), + }), + ); + + check_convert_with_avro( + Literal::Map(BTreeMap::from([ + ( + Literal::Primitive(PrimitiveLiteral::String("a".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("b".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(2))), + ), + ( + Literal::Primitive(PrimitiveLiteral::String("c".to_string())), + Some(Literal::Primitive(PrimitiveLiteral::Int(3))), + ), + ])), + &Type::Map(MapType { + key_field: NestedField::map_key_element(0, Type::Primitive(PrimitiveType::String)) + .into(), + value_field: NestedField::map_value_element( + 1, + Type::Primitive(PrimitiveType::Int), + true, + ) + .into(), + }), + ); + } + + #[test] + fn avro_convert_test_record() { + check_convert_with_avro( + Literal::Struct(Struct::from_iter(vec![ + Some(Literal::Primitive(PrimitiveLiteral::Int(1))), + Some(Literal::Primitive(PrimitiveLiteral::String( + "bar".to_string(), + ))), + None, + ])), + &Type::Struct(StructType::new(vec![ + NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(), + NestedField::optional(2, "name", Type::Primitive(PrimitiveType::String)).into(), + NestedField::optional(3, "address", Type::Primitive(PrimitiveType::String)).into(), + ])), + ); + } + + // # TODO:https://github.com/apache/iceberg-rust/issues/86 + // rust avro don't support deserialize any bytes representation now: + // - binary + // - decimal + #[test] + fn avro_convert_test_binary_ser() { + let literal = Literal::Primitive(PrimitiveLiteral::Binary(vec![1, 2, 3, 4, 5])); + let ty = Type::Primitive(PrimitiveType::Binary); + let expect_value = Value::Bytes(vec![1, 2, 3, 4, 5]); + check_serialize_avro(literal, &ty, expect_value); + } + + #[test] + fn avro_convert_test_decimal_ser() { + let literal = Literal::decimal(12345); + let ty = Type::Primitive(PrimitiveType::Decimal { + precision: 9, + scale: 8, + }); + let expect_value = Value::Decimal(apache_avro::Decimal::from(12345_i128.to_be_bytes())); + check_serialize_avro(literal, &ty, expect_value); + } + + // # TODO:https://github.com/apache/iceberg-rust/issues/86 + // rust avro can't support to convert any byte-like type to fixed in avro now. + // - uuid ser/de + // - fixed ser/de +} diff --git a/libs/iceberg/src/table.rs b/libs/iceberg/src/table.rs new file mode 100644 index 0000000..e3260a8 --- /dev/null +++ b/libs/iceberg/src/table.rs @@ -0,0 +1,65 @@ +// 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. + +//! Table API for Apache Iceberg +use crate::io::FileIO; +use crate::scan::TableScanBuilder; +use crate::spec::{TableMetadata, TableMetadataRef}; +use crate::TableIdent; +use typed_builder::TypedBuilder; + +/// Table represents a table in the catalog. +#[derive(TypedBuilder, Debug)] +pub struct Table { + file_io: FileIO, + #[builder(default, setter(strip_option, into))] + metadata_location: Option, + #[builder(setter(into))] + metadata: TableMetadataRef, + identifier: TableIdent, +} + +impl Table { + /// Returns table identifier. + pub fn identifier(&self) -> &TableIdent { + &self.identifier + } + /// Returns current metadata. + pub fn metadata(&self) -> &TableMetadata { + &self.metadata + } + + /// Returns current metadata ref. + pub fn metadata_ref(&self) -> TableMetadataRef { + self.metadata.clone() + } + + /// Returns current metadata location. + pub fn metadata_location(&self) -> Option<&str> { + self.metadata_location.as_deref() + } + + /// Returns file io used in this table. + pub fn file_io(&self) -> &FileIO { + &self.file_io + } + + /// Creates a table scan. + pub fn scan(&self) -> TableScanBuilder<'_> { + TableScanBuilder::new(self) + } +} diff --git a/libs/iceberg/src/transaction.rs b/libs/iceberg/src/transaction.rs new file mode 100644 index 0000000..165fb89 --- /dev/null +++ b/libs/iceberg/src/transaction.rs @@ -0,0 +1,370 @@ +// 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. + +//! This module contains transaction api. + +use crate::error::Result; +use crate::spec::{FormatVersion, NullOrder, SortDirection, SortField, SortOrder, Transform}; +use crate::table::Table; +use crate::TableUpdate::UpgradeFormatVersion; +use crate::{Catalog, Error, ErrorKind, TableCommit, TableRequirement, TableUpdate}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::mem::discriminant; + +/// Table transaction. +pub struct Transaction<'a> { + table: &'a Table, + updates: Vec, + requirements: Vec, +} + +impl<'a> Transaction<'a> { + /// Creates a new transaction. + pub fn new(table: &'a Table) -> Self { + Self { + table, + updates: vec![], + requirements: vec![], + } + } + + fn append_updates(&mut self, updates: Vec) -> Result<()> { + for update in &updates { + for up in &self.updates { + if discriminant(up) == discriminant(update) { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Cannot apply update with same type at same time: {:?}", + update + ), + )); + } + } + } + self.updates.extend(updates); + Ok(()) + } + + fn append_requirements(&mut self, requirements: Vec) -> Result<()> { + self.requirements.extend(requirements); + Ok(()) + } + + /// Sets table to a new version. + pub fn upgrade_table_version(mut self, format_version: FormatVersion) -> Result { + let current_version = self.table.metadata().format_version(); + match current_version.cmp(&format_version) { + Ordering::Greater => { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Cannot downgrade table version from {} to {}", + current_version, format_version + ), + )); + } + Ordering::Less => { + self.append_updates(vec![UpgradeFormatVersion { format_version }])?; + } + Ordering::Equal => { + // Do nothing. + } + } + Ok(self) + } + + /// Update table's property. + pub fn set_properties(mut self, props: HashMap) -> Result { + self.append_updates(vec![TableUpdate::SetProperties { updates: props }])?; + Ok(self) + } + + /// Creates replace sort order action. + pub fn replace_sort_order(self) -> ReplaceSortOrderAction<'a> { + ReplaceSortOrderAction { + tx: self, + sort_fields: vec![], + } + } + + /// Remove properties in table. + pub fn remove_properties(mut self, keys: Vec) -> Result { + self.append_updates(vec![TableUpdate::RemoveProperties { removals: keys }])?; + Ok(self) + } + + /// Commit transaction. + pub async fn commit(self, catalog: &impl Catalog) -> Result
{ + let table_commit = TableCommit::builder() + .ident(self.table.identifier().clone()) + .updates(self.updates) + .requirements(self.requirements) + .build(); + + catalog.update_table(table_commit).await + } +} + +/// Transaction action for replacing sort order. +pub struct ReplaceSortOrderAction<'a> { + tx: Transaction<'a>, + sort_fields: Vec, +} + +impl<'a> ReplaceSortOrderAction<'a> { + /// Adds a field for sorting in ascending order. + pub fn asc(self, name: &str, null_order: NullOrder) -> Result { + self.add_sort_field(name, SortDirection::Ascending, null_order) + } + + /// Adds a field for sorting in descending order. + pub fn desc(self, name: &str, null_order: NullOrder) -> Result { + self.add_sort_field(name, SortDirection::Descending, null_order) + } + + /// Finished building the action and apply it to the transaction. + pub fn apply(mut self) -> Result> { + let unbound_sort_order = SortOrder::builder() + .with_fields(self.sort_fields) + .build_unbound()?; + + let updates = vec![ + TableUpdate::AddSortOrder { + sort_order: unbound_sort_order, + }, + TableUpdate::SetDefaultSortOrder { sort_order_id: -1 }, + ]; + + let requirements = vec![ + TableRequirement::CurrentSchemaIdMatch { + current_schema_id: self.tx.table.metadata().current_schema().schema_id() as i64, + }, + TableRequirement::DefaultSortOrderIdMatch { + default_sort_order_id: self + .tx + .table + .metadata() + .default_sort_order() + .ok_or(Error::new( + ErrorKind::Unexpected, + "default sort order impossible to be none", + ))? + .order_id, + }, + ]; + + self.tx.append_requirements(requirements)?; + self.tx.append_updates(updates)?; + Ok(self.tx) + } + + fn add_sort_field( + mut self, + name: &str, + sort_direction: SortDirection, + null_order: NullOrder, + ) -> Result { + let field_id = self + .tx + .table + .metadata() + .current_schema() + .field_id_by_name(name) + .ok_or_else(|| { + Error::new( + ErrorKind::DataInvalid, + format!("Cannot find field {} in table schema", name), + ) + })?; + + let sort_field = SortField::builder() + .source_id(field_id) + .transform(Transform::Identity) + .direction(sort_direction) + .null_order(null_order) + .build(); + + self.sort_fields.push(sort_field); + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use crate::io::FileIO; + use crate::spec::{FormatVersion, TableMetadata}; + use crate::table::Table; + use crate::transaction::Transaction; + use crate::{TableIdent, TableRequirement, TableUpdate}; + use std::collections::HashMap; + use std::fs::File; + use std::io::BufReader; + + fn make_v1_table() -> Table { + let file = File::open(format!( + "{}/testdata/table_metadata/{}", + env!("CARGO_MANIFEST_DIR"), + "TableMetadataV1Valid.json" + )) + .unwrap(); + let reader = BufReader::new(file); + let resp = serde_json::from_reader::<_, TableMetadata>(reader).unwrap(); + + Table::builder() + .metadata(resp) + .metadata_location("s3://bucket/test/location/metadata/v1.json".to_string()) + .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap()) + .file_io(FileIO::from_path("/tmp").unwrap().build().unwrap()) + .build() + } + + fn make_v2_table() -> Table { + let file = File::open(format!( + "{}/testdata/table_metadata/{}", + env!("CARGO_MANIFEST_DIR"), + "TableMetadataV2Valid.json" + )) + .unwrap(); + let reader = BufReader::new(file); + let resp = serde_json::from_reader::<_, TableMetadata>(reader).unwrap(); + + Table::builder() + .metadata(resp) + .metadata_location("s3://bucket/test/location/metadata/v1.json".to_string()) + .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap()) + .file_io(FileIO::from_path("/tmp").unwrap().build().unwrap()) + .build() + } + + #[test] + fn test_upgrade_table_version_v1_to_v2() { + let table = make_v1_table(); + let tx = Transaction::new(&table); + let tx = tx.upgrade_table_version(FormatVersion::V2).unwrap(); + + assert_eq!( + vec![TableUpdate::UpgradeFormatVersion { + format_version: FormatVersion::V2 + }], + tx.updates + ); + } + + #[test] + fn test_upgrade_table_version_v2_to_v2() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx.upgrade_table_version(FormatVersion::V2).unwrap(); + + assert!( + tx.updates.is_empty(), + "Upgrade table to same version should not generate any updates" + ); + assert!( + tx.requirements.is_empty(), + "Upgrade table to same version should not generate any requirements" + ); + } + + #[test] + fn test_downgrade_table_version() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx.upgrade_table_version(FormatVersion::V1); + + assert!(tx.is_err(), "Downgrade table version should fail!"); + } + + #[test] + fn test_set_table_property() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx + .set_properties(HashMap::from([("a".to_string(), "b".to_string())])) + .unwrap(); + + assert_eq!( + vec![TableUpdate::SetProperties { + updates: HashMap::from([("a".to_string(), "b".to_string())]) + }], + tx.updates + ); + } + + #[test] + fn test_remove_property() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx + .remove_properties(vec!["a".to_string(), "b".to_string()]) + .unwrap(); + + assert_eq!( + vec![TableUpdate::RemoveProperties { + removals: vec!["a".to_string(), "b".to_string()] + }], + tx.updates + ); + } + + #[test] + fn test_replace_sort_order() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx.replace_sort_order().apply().unwrap(); + + assert_eq!( + vec![ + TableUpdate::AddSortOrder { + sort_order: Default::default() + }, + TableUpdate::SetDefaultSortOrder { sort_order_id: -1 } + ], + tx.updates + ); + + assert_eq!( + vec![ + TableRequirement::CurrentSchemaIdMatch { + current_schema_id: 1 + }, + TableRequirement::DefaultSortOrderIdMatch { + default_sort_order_id: 3 + } + ], + tx.requirements + ); + } + + #[test] + fn test_do_same_update_in_same_transaction() { + let table = make_v2_table(); + let tx = Transaction::new(&table); + let tx = tx + .remove_properties(vec!["a".to_string(), "b".to_string()]) + .unwrap(); + + let tx = tx.remove_properties(vec!["c".to_string(), "d".to_string()]); + + assert!( + tx.is_err(), + "Should not allow to do same kinds update in same transaction" + ); + } +} diff --git a/libs/iceberg/src/transform/bucket.rs b/libs/iceberg/src/transform/bucket.rs new file mode 100644 index 0000000..beff0be --- /dev/null +++ b/libs/iceberg/src/transform/bucket.rs @@ -0,0 +1,245 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::ArrayRef; +use arrow_schema::{DataType, TimeUnit}; + +use super::TransformFunction; + +#[derive(Debug)] +pub struct Bucket { + mod_n: u32, +} + +impl Bucket { + pub fn new(mod_n: u32) -> Self { + Self { mod_n } + } +} + +impl Bucket { + /// When switch the hash function, we only need to change this function. + fn hash_bytes(mut v: &[u8]) -> i32 { + murmur3::murmur3_32(&mut v, 0).unwrap() as i32 + } + + fn hash_int(v: i32) -> i32 { + Self::hash_long(v as i64) + } + + fn hash_long(v: i64) -> i32 { + Self::hash_bytes(v.to_le_bytes().as_slice()) + } + + /// v is days from unix epoch + fn hash_date(v: i32) -> i32 { + Self::hash_int(v) + } + + /// v is microseconds from midnight + fn hash_time(v: i64) -> i32 { + Self::hash_long(v) + } + + /// v is microseconds from unix epoch + fn hash_timestamp(v: i64) -> i32 { + Self::hash_long(v) + } + + fn hash_str(s: &str) -> i32 { + Self::hash_bytes(s.as_bytes()) + } + + /// Decimal values are hashed using the minimum number of bytes required to hold the unscaled value as a two’s complement big-endian + /// ref: https://iceberg.apache.org/spec/#appendix-b-32-bit-hash-requirements + fn hash_decimal(v: i128) -> i32 { + let bytes = v.to_be_bytes(); + if let Some(start) = bytes.iter().position(|&x| x != 0) { + Self::hash_bytes(&bytes[start..]) + } else { + Self::hash_bytes(&[0]) + } + } + + /// def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N + /// ref: https://iceberg.apache.org/spec/#partitioning + fn bucket_n(&self, v: i32) -> i32 { + (v & i32::MAX) % (self.mod_n as i32) + } +} + +impl TransformFunction for Bucket { + fn transform(&self, input: ArrayRef) -> crate::Result { + let res: arrow_array::Int32Array = match input.data_type() { + DataType::Int32 => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_int(v))), + DataType::Int64 => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_long(v))), + DataType::Decimal128(_, _) => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_decimal(v))), + DataType::Date32 => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_date(v))), + DataType::Time64(TimeUnit::Microsecond) => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_time(v))), + DataType::Timestamp(TimeUnit::Microsecond, _) => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| self.bucket_n(Self::hash_timestamp(v))), + DataType::Utf8 => arrow_array::Int32Array::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| self.bucket_n(Self::hash_str(v.unwrap()))), + ), + DataType::LargeUtf8 => arrow_array::Int32Array::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| self.bucket_n(Self::hash_str(v.unwrap()))), + ), + DataType::Binary => arrow_array::Int32Array::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))), + ), + DataType::LargeBinary => arrow_array::Int32Array::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))), + ), + DataType::FixedSizeBinary(_) => arrow_array::Int32Array::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))), + ), + _ => unreachable!("Unsupported data type: {:?}", input.data_type()), + }; + Ok(Arc::new(res)) + } +} + +#[cfg(test)] +mod test { + use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime}; + + use super::Bucket; + #[test] + fn test_hash() { + // test int + assert_eq!(Bucket::hash_int(34), 2017239379); + // test long + assert_eq!(Bucket::hash_long(34), 2017239379); + // test decimal + assert_eq!(Bucket::hash_decimal(1420), -500754589); + // test date + let date = NaiveDate::from_ymd_opt(2017, 11, 16).unwrap(); + assert_eq!( + Bucket::hash_date( + date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()) + .num_days() as i32 + ), + -653330422 + ); + // test time + let time = NaiveTime::from_hms_opt(22, 31, 8).unwrap(); + assert_eq!( + Bucket::hash_time( + time.signed_duration_since(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) + .num_microseconds() + .unwrap() + ), + -662762989 + ); + // test timestamp + let timestamp = + NaiveDateTime::parse_from_str("2017-11-16 22:31:08", "%Y-%m-%d %H:%M:%S").unwrap(); + assert_eq!( + Bucket::hash_timestamp( + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str("1970-01-01 00:00:00", "%Y-%m-%d %H:%M:%S") + .unwrap() + ) + .num_microseconds() + .unwrap() + ), + -2047944441 + ); + // test timestamp with tz + let timestamp = DateTime::parse_from_rfc3339("2017-11-16T14:31:08-08:00").unwrap(); + assert_eq!( + Bucket::hash_timestamp( + timestamp + .signed_duration_since( + DateTime::parse_from_rfc3339("1970-01-01T00:00:00-00:00").unwrap() + ) + .num_microseconds() + .unwrap() + ), + -2047944441 + ); + // test str + assert_eq!(Bucket::hash_str("iceberg"), 1210000089); + // test uuid + assert_eq!( + Bucket::hash_bytes( + [ + 0xF7, 0x9C, 0x3E, 0x09, 0x67, 0x7C, 0x4B, 0xBD, 0xA4, 0x79, 0x3F, 0x34, 0x9C, + 0xB7, 0x85, 0xE7 + ] + .as_ref() + ), + 1488055340 + ); + // test fixed and binary + assert_eq!( + Bucket::hash_bytes([0x00, 0x01, 0x02, 0x03].as_ref()), + -188683207 + ); + } +} diff --git a/libs/iceberg/src/transform/identity.rs b/libs/iceberg/src/transform/identity.rs new file mode 100644 index 0000000..d22c28f --- /dev/null +++ b/libs/iceberg/src/transform/identity.rs @@ -0,0 +1,31 @@ +// 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. + +use crate::Result; +use arrow_array::ArrayRef; + +use super::TransformFunction; + +/// Return identity array. +#[derive(Debug)] +pub struct Identity {} + +impl TransformFunction for Identity { + fn transform(&self, input: ArrayRef) -> Result { + Ok(input) + } +} diff --git a/libs/iceberg/src/transform/mod.rs b/libs/iceberg/src/transform/mod.rs new file mode 100644 index 0000000..dead9db --- /dev/null +++ b/libs/iceberg/src/transform/mod.rs @@ -0,0 +1,55 @@ +// 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. + +//! Transform function used to compute partition values. +use crate::{spec::Transform, Result}; +use arrow_array::ArrayRef; + +mod bucket; +mod identity; +mod temporal; +mod truncate; +mod void; + +/// TransformFunction is a trait that defines the interface for all transform functions. +pub trait TransformFunction: Send { + /// transform will take an input array and transform it into a new array. + /// The implementation of this function will need to check and downcast the input to specific + /// type. + fn transform(&self, input: ArrayRef) -> Result; +} + +/// BoxedTransformFunction is a boxed trait object of TransformFunction. +pub type BoxedTransformFunction = Box; + +/// create_transform_function creates a boxed trait object of TransformFunction from a Transform. +pub fn create_transform_function(transform: &Transform) -> Result { + match transform { + Transform::Identity => Ok(Box::new(identity::Identity {})), + Transform::Void => Ok(Box::new(void::Void {})), + Transform::Year => Ok(Box::new(temporal::Year {})), + Transform::Month => Ok(Box::new(temporal::Month {})), + Transform::Day => Ok(Box::new(temporal::Day {})), + Transform::Hour => Ok(Box::new(temporal::Hour {})), + Transform::Bucket(mod_n) => Ok(Box::new(bucket::Bucket::new(*mod_n))), + Transform::Truncate(width) => Ok(Box::new(truncate::Truncate::new(*width))), + Transform::Unknown => Err(crate::error::Error::new( + crate::ErrorKind::FeatureUnsupported, + "Transform Unknown is not implemented", + )), + } +} diff --git a/libs/iceberg/src/transform/temporal.rs b/libs/iceberg/src/transform/temporal.rs new file mode 100644 index 0000000..7b8deb1 --- /dev/null +++ b/libs/iceberg/src/transform/temporal.rs @@ -0,0 +1,412 @@ +// 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. + +use super::TransformFunction; +use crate::{Error, ErrorKind, Result}; +use arrow_arith::{ + arity::binary, + temporal::{month_dyn, year_dyn}, +}; +use arrow_array::{ + types::Date32Type, Array, ArrayRef, Date32Array, Int32Array, TimestampMicrosecondArray, +}; +use arrow_schema::{DataType, TimeUnit}; +use chrono::Datelike; +use std::sync::Arc; + +/// The number of days since unix epoch. +const DAY_SINCE_UNIX_EPOCH: i32 = 719163; +/// Hour in one second. +const HOUR_PER_SECOND: f64 = 1.0_f64 / 3600.0_f64; +/// Day in one second. +const DAY_PER_SECOND: f64 = 1.0_f64 / 24.0_f64 / 3600.0_f64; +/// Year of unix epoch. +const UNIX_EPOCH_YEAR: i32 = 1970; + +/// Extract a date or timestamp year, as years from 1970 +#[derive(Debug)] +pub struct Year; + +impl TransformFunction for Year { + fn transform(&self, input: ArrayRef) -> Result { + let array = + year_dyn(&input).map_err(|err| Error::new(ErrorKind::Unexpected, format!("{err}")))?; + Ok(Arc::::new( + array + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| v - UNIX_EPOCH_YEAR), + )) + } +} + +/// Extract a date or timestamp month, as months from 1970-01-01 +#[derive(Debug)] +pub struct Month; + +impl TransformFunction for Month { + fn transform(&self, input: ArrayRef) -> Result { + let year_array = + year_dyn(&input).map_err(|err| Error::new(ErrorKind::Unexpected, format!("{err}")))?; + let year_array: Int32Array = year_array + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| 12 * (v - UNIX_EPOCH_YEAR)); + let month_array = + month_dyn(&input).map_err(|err| Error::new(ErrorKind::Unexpected, format!("{err}")))?; + Ok(Arc::::new( + binary( + month_array.as_any().downcast_ref::().unwrap(), + year_array.as_any().downcast_ref::().unwrap(), + // Compute month from 1970-01-01, so minus 1 here. + |a, b| a + b - 1, + ) + .unwrap(), + )) + } +} + +/// Extract a date or timestamp day, as days from 1970-01-01 +#[derive(Debug)] +pub struct Day; + +impl TransformFunction for Day { + fn transform(&self, input: ArrayRef) -> Result { + let res: Int32Array = match input.data_type() { + DataType::Timestamp(TimeUnit::Microsecond, _) => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| -> i32 { (v as f64 / 1000.0 / 1000.0 * DAY_PER_SECOND) as i32 }), + DataType::Date32 => { + input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| -> i32 { + Date32Type::to_naive_date(v).num_days_from_ce() - DAY_SINCE_UNIX_EPOCH + }) + } + _ => { + return Err(Error::new( + ErrorKind::Unexpected, + format!( + "Should not call internally for unsupported data type {:?}", + input.data_type() + ), + )) + } + }; + Ok(Arc::new(res)) + } +} + +/// Extract a timestamp hour, as hours from 1970-01-01 00:00:00 +#[derive(Debug)] +pub struct Hour; + +impl TransformFunction for Hour { + fn transform(&self, input: ArrayRef) -> Result { + let res: Int32Array = match input.data_type() { + DataType::Timestamp(TimeUnit::Microsecond, _) => input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| -> i32 { (v as f64 * HOUR_PER_SECOND / 1000.0 / 1000.0) as i32 }), + _ => { + return Err(Error::new( + ErrorKind::Unexpected, + format!( + "Should not call internally for unsupported data type {:?}", + input.data_type() + ), + )) + } + }; + Ok(Arc::new(res)) + } +} + +#[cfg(test)] +mod test { + use arrow_array::{ArrayRef, Date32Array, Int32Array, TimestampMicrosecondArray}; + use chrono::{NaiveDate, NaiveDateTime}; + use std::sync::Arc; + + use crate::transform::TransformFunction; + + #[test] + fn test_transform_years() { + let year = super::Year; + + // Test Date32 + let ori_date = vec![ + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2030, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2060, 1, 1).unwrap(), + ]; + let date_array: ArrayRef = Arc::new(Date32Array::from( + ori_date + .into_iter() + .map(|date| { + date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()) + .num_days() as i32 + }) + .collect::>(), + )); + let res = year.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), 0); + assert_eq!(res.value(1), 30); + assert_eq!(res.value(2), 60); + assert_eq!(res.value(3), 90); + + // Test TimestampMicrosecond + let ori_timestamp = vec![ + NaiveDateTime::parse_from_str("1970-01-01 12:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2000-01-01 19:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2030-01-01 10:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2060-01-01 11:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + ]; + let date_array: ArrayRef = Arc::new(TimestampMicrosecondArray::from( + ori_timestamp + .into_iter() + .map(|timestamp| { + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str( + "1970-01-01 00:00:00.0", + "%Y-%m-%d %H:%M:%S.%f", + ) + .unwrap(), + ) + .num_microseconds() + .unwrap() + }) + .collect::>(), + )); + let res = year.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), 0); + assert_eq!(res.value(1), 30); + assert_eq!(res.value(2), 60); + assert_eq!(res.value(3), 90); + } + + #[test] + fn test_transform_months() { + let month = super::Month; + + // Test Date32 + let ori_date = vec![ + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 4, 1).unwrap(), + NaiveDate::from_ymd_opt(2030, 7, 1).unwrap(), + NaiveDate::from_ymd_opt(2060, 10, 1).unwrap(), + ]; + let date_array: ArrayRef = Arc::new(Date32Array::from( + ori_date + .into_iter() + .map(|date| { + date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()) + .num_days() as i32 + }) + .collect::>(), + )); + let res = month.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), 0); + assert_eq!(res.value(1), 30 * 12 + 3); + assert_eq!(res.value(2), 60 * 12 + 6); + assert_eq!(res.value(3), 90 * 12 + 9); + + // Test TimestampMicrosecond + let ori_timestamp = vec![ + NaiveDateTime::parse_from_str("1970-01-01 12:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2000-04-01 19:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2030-07-01 10:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2060-10-01 11:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + ]; + let date_array: ArrayRef = Arc::new(TimestampMicrosecondArray::from( + ori_timestamp + .into_iter() + .map(|timestamp| { + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str( + "1970-01-01 00:00:00.0", + "%Y-%m-%d %H:%M:%S.%f", + ) + .unwrap(), + ) + .num_microseconds() + .unwrap() + }) + .collect::>(), + )); + let res = month.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), 0); + assert_eq!(res.value(1), 30 * 12 + 3); + assert_eq!(res.value(2), 60 * 12 + 6); + assert_eq!(res.value(3), 90 * 12 + 9); + } + + #[test] + fn test_transform_days() { + let day = super::Day; + let ori_date = vec![ + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 4, 1).unwrap(), + NaiveDate::from_ymd_opt(2030, 7, 1).unwrap(), + NaiveDate::from_ymd_opt(2060, 10, 1).unwrap(), + ]; + let expect_day = ori_date + .clone() + .into_iter() + .map(|data| { + data.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()) + .num_days() as i32 + }) + .collect::>(); + + // Test Date32 + let date_array: ArrayRef = Arc::new(Date32Array::from( + ori_date + .into_iter() + .map(|date| { + date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()) + .num_days() as i32 + }) + .collect::>(), + )); + let res = day.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), expect_day[0]); + assert_eq!(res.value(1), expect_day[1]); + assert_eq!(res.value(2), expect_day[2]); + assert_eq!(res.value(3), expect_day[3]); + + // Test TimestampMicrosecond + let ori_timestamp = vec![ + NaiveDateTime::parse_from_str("1970-01-01 12:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2000-04-01 19:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2030-07-01 10:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2060-10-01 11:30:42.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + ]; + let date_array: ArrayRef = Arc::new(TimestampMicrosecondArray::from( + ori_timestamp + .into_iter() + .map(|timestamp| { + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str( + "1970-01-01 00:00:00.0", + "%Y-%m-%d %H:%M:%S.%f", + ) + .unwrap(), + ) + .num_microseconds() + .unwrap() + }) + .collect::>(), + )); + let res = day.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), expect_day[0]); + assert_eq!(res.value(1), expect_day[1]); + assert_eq!(res.value(2), expect_day[2]); + assert_eq!(res.value(3), expect_day[3]); + } + + #[test] + fn test_transform_hours() { + let hour = super::Hour; + let ori_timestamp = vec![ + NaiveDateTime::parse_from_str("1970-01-01 19:01:23.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2000-03-01 12:01:23.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2030-10-02 10:01:23.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + NaiveDateTime::parse_from_str("2060-09-01 05:03:23.123", "%Y-%m-%d %H:%M:%S.%f") + .unwrap(), + ]; + let expect_hour = ori_timestamp + .clone() + .into_iter() + .map(|timestamp| { + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str( + "1970-01-01 00:00:0.0", + "%Y-%m-%d %H:%M:%S.%f", + ) + .unwrap(), + ) + .num_hours() as i32 + }) + .collect::>(); + + // Test TimestampMicrosecond + let date_array: ArrayRef = Arc::new(TimestampMicrosecondArray::from( + ori_timestamp + .into_iter() + .map(|timestamp| { + timestamp + .signed_duration_since( + NaiveDateTime::parse_from_str( + "1970-01-01 00:00:0.0", + "%Y-%m-%d %H:%M:%S.%f", + ) + .unwrap(), + ) + .num_microseconds() + .unwrap() + }) + .collect::>(), + )); + let res = hour.transform(date_array).unwrap(); + let res = res.as_any().downcast_ref::().unwrap(); + assert_eq!(res.len(), 4); + assert_eq!(res.value(0), expect_hour[0]); + assert_eq!(res.value(1), expect_hour[1]); + assert_eq!(res.value(2), expect_hour[2]); + assert_eq!(res.value(3), expect_hour[3]); + } +} diff --git a/libs/iceberg/src/transform/truncate.rs b/libs/iceberg/src/transform/truncate.rs new file mode 100644 index 0000000..a8ebda8 --- /dev/null +++ b/libs/iceberg/src/transform/truncate.rs @@ -0,0 +1,218 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::ArrayRef; +use arrow_schema::DataType; + +use crate::Error; + +use super::TransformFunction; + +#[derive(Debug)] +pub struct Truncate { + width: u32, +} + +impl Truncate { + pub fn new(width: u32) -> Self { + Self { width } + } + + fn truncate_str_by_char(s: &str, max_chars: usize) -> &str { + match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx], + } + } +} + +impl TransformFunction for Truncate { + fn transform(&self, input: ArrayRef) -> crate::Result { + match input.data_type() { + DataType::Int32 => { + let width: i32 = self.width.try_into().map_err(|_| { + Error::new( + crate::ErrorKind::DataInvalid, + "width is failed to convert to i32 when truncate Int32Array", + ) + })?; + let res: arrow_array::Int32Array = input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| v - v.rem_euclid(width)); + Ok(Arc::new(res)) + } + DataType::Int64 => { + let width = self.width as i64; + let res: arrow_array::Int64Array = input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| v - (((v % width) + width) % width)); + Ok(Arc::new(res)) + } + DataType::Decimal128(precision, scale) => { + let width = self.width as i128; + let res: arrow_array::Decimal128Array = input + .as_any() + .downcast_ref::() + .unwrap() + .unary(|v| v - (((v % width) + width) % width)) + .with_precision_and_scale(*precision, *scale) + .map_err(|err| Error::new(crate::ErrorKind::Unexpected, format!("{err}")))?; + Ok(Arc::new(res)) + } + DataType::Utf8 => { + let len = self.width as usize; + let res: arrow_array::StringArray = arrow_array::StringArray::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| v.map(|v| Self::truncate_str_by_char(v, len))), + ); + Ok(Arc::new(res)) + } + DataType::LargeUtf8 => { + let len = self.width as usize; + let res: arrow_array::LargeStringArray = arrow_array::LargeStringArray::from_iter( + input + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(|v| v.map(|v| Self::truncate_str_by_char(v, len))), + ); + Ok(Arc::new(res)) + } + _ => unreachable!("Truncate transform only supports (int,long,decimal,string) types"), + } + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use arrow_array::{ + builder::PrimitiveBuilder, types::Decimal128Type, Decimal128Array, Int32Array, Int64Array, + }; + + use crate::transform::TransformFunction; + + // Test case ref from: https://iceberg.apache.org/spec/#truncate-transform-details + #[test] + fn test_truncate_simple() { + // test truncate int + let input = Arc::new(Int32Array::from(vec![1, -1])); + let res = super::Truncate::new(10).transform(input).unwrap(); + assert_eq!( + res.as_any().downcast_ref::().unwrap().value(0), + 0 + ); + assert_eq!( + res.as_any().downcast_ref::().unwrap().value(1), + -10 + ); + + // test truncate long + let input = Arc::new(Int64Array::from(vec![1, -1])); + let res = super::Truncate::new(10).transform(input).unwrap(); + assert_eq!( + res.as_any().downcast_ref::().unwrap().value(0), + 0 + ); + assert_eq!( + res.as_any().downcast_ref::().unwrap().value(1), + -10 + ); + + // test decimal + let mut buidler = PrimitiveBuilder::::new() + .with_precision_and_scale(20, 2) + .unwrap(); + buidler.append_value(1065); + let input = Arc::new(buidler.finish()); + let res = super::Truncate::new(50).transform(input).unwrap(); + assert_eq!( + res.as_any() + .downcast_ref::() + .unwrap() + .value(0), + 1050 + ); + + // test string + let input = Arc::new(arrow_array::StringArray::from(vec!["iceberg"])); + let res = super::Truncate::new(3).transform(input).unwrap(); + assert_eq!( + res.as_any() + .downcast_ref::() + .unwrap() + .value(0), + "ice" + ); + + // test large string + let input = Arc::new(arrow_array::LargeStringArray::from(vec!["iceberg"])); + let res = super::Truncate::new(3).transform(input).unwrap(); + assert_eq!( + res.as_any() + .downcast_ref::() + .unwrap() + .value(0), + "ice" + ); + } + + #[test] + fn test_string_truncate() { + let test1 = "イロハニホヘト"; + let test1_2_expected = "イロ"; + assert_eq!( + super::Truncate::truncate_str_by_char(test1, 2), + test1_2_expected + ); + + let test1_3_expected = "イロハ"; + assert_eq!( + super::Truncate::truncate_str_by_char(test1, 3), + test1_3_expected + ); + + let test2 = "щщаεはчωいにπάほхεろへσκζ"; + let test2_7_expected = "щщаεはчω"; + assert_eq!( + super::Truncate::truncate_str_by_char(test2, 7), + test2_7_expected + ); + + let test3 = "\u{FFFF}\u{FFFF}"; + assert_eq!(super::Truncate::truncate_str_by_char(test3, 2), test3); + + let test4 = "\u{10000}\u{10000}"; + let test4_1_expected = "\u{10000}"; + assert_eq!( + super::Truncate::truncate_str_by_char(test4, 1), + test4_1_expected + ); + } +} diff --git a/libs/iceberg/src/transform/void.rs b/libs/iceberg/src/transform/void.rs new file mode 100644 index 0000000..d419430 --- /dev/null +++ b/libs/iceberg/src/transform/void.rs @@ -0,0 +1,30 @@ +// 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. + +use crate::Result; +use arrow_array::{new_null_array, ArrayRef}; + +use super::TransformFunction; + +#[derive(Debug)] +pub struct Void {} + +impl TransformFunction for Void { + fn transform(&self, input: ArrayRef) -> Result { + Ok(new_null_array(input.data_type(), input.len())) + } +} diff --git a/libs/iceberg/src/writer/file_writer/mod.rs b/libs/iceberg/src/writer/file_writer/mod.rs new file mode 100644 index 0000000..c8251fd --- /dev/null +++ b/libs/iceberg/src/writer/file_writer/mod.rs @@ -0,0 +1,39 @@ +// 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. + +//! This module contains the writer for data file format supported by iceberg: parquet, orc. + +use super::{CurrentFileStatus, DefaultOutput}; +use crate::Result; +use arrow_array::RecordBatch; +use futures::Future; + +/// File writer builder trait. +pub trait FileWriterBuilder: Send + Clone + 'static { + /// The associated file writer type. + type R: FileWriter; + /// Build file writer. + fn build(self) -> impl Future> + Send; +} + +/// File writer focus on writing record batch to different physical file format.(Such as parquet. orc) +pub trait FileWriter: Send + CurrentFileStatus + 'static { + /// Write record batch to file. + fn write(&mut self, batch: &RecordBatch) -> impl Future> + Send; + /// Close file writer. + fn close(self) -> impl Future> + Send; +} diff --git a/libs/iceberg/src/writer/mod.rs b/libs/iceberg/src/writer/mod.rs new file mode 100644 index 0000000..ac79d7b --- /dev/null +++ b/libs/iceberg/src/writer/mod.rs @@ -0,0 +1,35 @@ +// 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. + +//! The iceberg writer module. + +use crate::spec::DataFileBuilder; + +pub mod file_writer; + +type DefaultOutput = Vec; + +/// The current file status of iceberg writer. It implement for the writer which write a single +/// file. +pub trait CurrentFileStatus { + /// Get the current file path. + fn current_file_path(&self) -> String; + /// Get the current file row number. + fn current_row_num(&self) -> usize; + /// Get the current file written size. + fn current_written_size(&self) -> usize; +} diff --git a/libs/iceberg/testdata/avro_schema_manifest_entry.json b/libs/iceberg/testdata/avro_schema_manifest_entry.json new file mode 100644 index 0000000..876c5fa --- /dev/null +++ b/libs/iceberg/testdata/avro_schema_manifest_entry.json @@ -0,0 +1,286 @@ +{ + "type": "record", + "name": "manifest_entry", + "fields": [ + { + "name": "status", + "type": "int", + "field-id": 0 + }, + { + "name": "snapshot_id", + "type": [ + "null", + "long" + ], + "field-id": 1 + }, + { + "name": "data_file", + "type": { + "type": "record", + "name": "r2", + "fields": [ + { + "name": "file_path", + "type": "string", + "doc": "Location URI with FS scheme", + "field-id": 100 + }, + { + "name": "file_format", + "type": "string", + "doc": "File format name: avro, orc, or parquet", + "field-id": 101 + }, + { + "name": "partition", + "type": { + "type": "record", + "name": "r102", + "fields": [ + { + "field-id": 1000, + "name": "VendorID", + "type": [ + "null", + "int" + ] + }, + { + "field-id": 1001, + "name": "tpep_pickup_datetime", + "type": [ + "null", + { + "type": "int", + "logicalType": "date" + } + ] + } + ] + }, + "field-id": 102 + }, + { + "name": "record_count", + "type": "long", + "doc": "Number of records in the file", + "field-id": 103 + }, + { + "name": "file_size_in_bytes", + "type": "long", + "doc": "Total file size in bytes", + "field-id": 104 + }, + { + "name": "block_size_in_bytes", + "type": "long", + "field-id": 105 + }, + { + "name": "column_sizes", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k117_v118", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 117 + }, + { + "name": "value", + "type": "long", + "field-id": 118 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to total size on disk", + "field-id": 108 + }, + { + "name": "value_counts", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k119_v120", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 119 + }, + { + "name": "value", + "type": "long", + "field-id": 120 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to total count, including null and NaN", + "field-id": 109 + }, + { + "name": "null_value_counts", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k121_v122", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 121 + }, + { + "name": "value", + "type": "long", + "field-id": 122 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to null value count", + "field-id": 110 + }, + { + "name": "nan_value_counts", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k138_v139", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 138 + }, + { + "name": "value", + "type": "long", + "field-id": 139 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to number of NaN values in the column", + "field-id": 137 + }, + { + "name": "lower_bounds", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k126_v127", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 126 + }, + { + "name": "value", + "type": "bytes", + "field-id": 127 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to lower bound", + "field-id": 125 + }, + { + "name": "upper_bounds", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "k129_v130", + "fields": [ + { + "name": "key", + "type": "int", + "field-id": 129 + }, + { + "name": "value", + "type": "bytes", + "field-id": 130 + } + ] + }, + "logicalType": "map" + } + ], + "doc": "Map of column id to upper bound", + "field-id": 128 + }, + { + "name": "key_metadata", + "type": [ + "null", + "bytes" + ], + "doc": "Encryption key metadata blob", + "field-id": 131 + }, + { + "name": "split_offsets", + "type": [ + "null", + { + "type": "array", + "items": "long", + "element-id": 133 + } + ], + "doc": "Splittable offsets", + "field-id": 132 + }, + { + "name": "sort_order_id", + "type": [ + "null", + "int" + ], + "doc": "Sort order ID", + "field-id": 140 + } + ] + }, + "field-id": 2 + } + ] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/avro_schema_manifest_file_v1.json b/libs/iceberg/testdata/avro_schema_manifest_file_v1.json new file mode 100644 index 0000000..b185094 --- /dev/null +++ b/libs/iceberg/testdata/avro_schema_manifest_file_v1.json @@ -0,0 +1,139 @@ +{ + "type": "record", + "name": "manifest_file", + "fields": [ + { + "name": "manifest_path", + "type": "string", + "doc": "Location URI with FS scheme", + "field-id": 500 + }, + { + "name": "manifest_length", + "type": "long", + "doc": "Total file size in bytes", + "field-id": 501 + }, + { + "name": "partition_spec_id", + "type": "int", + "doc": "Spec ID used to write", + "field-id": 502 + }, + { + "name": "added_snapshot_id", + "type": [ + "null", + "long" + ], + "doc": "Snapshot ID that added the manifest", + "default": null, + "field-id": 503 + }, + { + "name": "added_data_files_count", + "type": [ + "null", + "int" + ], + "doc": "Added entry count", + "field-id": 504 + }, + { + "name": "existing_data_files_count", + "type": [ + "null", + "int" + ], + "doc": "Existing entry count", + "field-id": 505 + }, + { + "name": "deleted_data_files_count", + "type": [ + "null", + "int" + ], + "doc": "Deleted entry count", + "field-id": 506 + }, + { + "name": "partitions", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "r508", + "fields": [ + { + "name": "contains_null", + "type": "boolean", + "doc": "True if any file has a null partition value", + "field-id": 509 + }, + { + "name": "contains_nan", + "type": [ + "null", + "boolean" + ], + "doc": "True if any file has a nan partition value", + "field-id": 518 + }, + { + "name": "lower_bound", + "type": [ + "null", + "bytes" + ], + "doc": "Partition lower bound for all files", + "field-id": 510 + }, + { + "name": "upper_bound", + "type": [ + "null", + "bytes" + ], + "doc": "Partition upper bound for all files", + "field-id": 511 + } + ] + }, + "element-id": 508 + } + ], + "doc": "Summary for each partition", + "field-id": 507 + }, + { + "name": "added_rows_count", + "type": [ + "null", + "long" + ], + "doc": "Added rows count", + "field-id": 512 + }, + { + "name": "existing_rows_count", + "type": [ + "null", + "long" + ], + "doc": "Existing rows count", + "field-id": 513 + }, + { + "name": "deleted_rows_count", + "type": [ + "null", + "long" + ], + "doc": "Deleted rows count", + "field-id": 514 + } + ] +} diff --git a/libs/iceberg/testdata/avro_schema_manifest_file_v2.json b/libs/iceberg/testdata/avro_schema_manifest_file_v2.json new file mode 100644 index 0000000..34b97b9 --- /dev/null +++ b/libs/iceberg/testdata/avro_schema_manifest_file_v2.json @@ -0,0 +1,141 @@ +{ + "type": "record", + "name": "manifest_file", + "fields": [ + { + "name": "manifest_path", + "type": "string", + "doc": "Location URI with FS scheme", + "field-id": 500 + }, + { + "name": "manifest_length", + "type": "long", + "doc": "Total file size in bytes", + "field-id": 501 + }, + { + "name": "partition_spec_id", + "type": "int", + "doc": "Spec ID used to write", + "field-id": 502 + }, + { + "name": "content", + "type": "int", + "doc": "Contents of the manifest: 0=data, 1=deletes", + "field-id": 517 + }, + { + "name": "sequence_number", + "type": [ + "null", + "long" + ], + "doc": "Sequence number when the manifest was added", + "field-id": 515 + }, + { + "name": "min_sequence_number", + "type": [ + "null", + "long" + ], + "doc": "Lowest sequence number in the manifest", + "field-id": 516 + }, + { + "name": "added_snapshot_id", + "type": "long", + "doc": "Snapshot ID that added the manifest", + "field-id": 503 + }, + { + "name": "added_files_count", + "type": "int", + "doc": "Added entry count", + "field-id": 504 + }, + { + "name": "existing_files_count", + "type": "int", + "doc": "Existing entry count", + "field-id": 505 + }, + { + "name": "deleted_files_count", + "type": "int", + "doc": "Deleted entry count", + "field-id": 506 + }, + { + "name": "added_rows_count", + "type": "long", + "doc": "Added rows count", + "field-id": 512 + }, + { + "name": "existing_rows_count", + "type": "long", + "doc": "Existing rows count", + "field-id": 513 + }, + { + "name": "deleted_rows_count", + "type": "long", + "doc": "Deleted rows count", + "field-id": 514 + }, + { + "name": "partitions", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "r508", + "fields": [ + { + "name": "contains_null", + "type": "boolean", + "doc": "True if any file has a null partition value", + "field-id": 509 + }, + { + "name": "contains_nan", + "type": [ + "null", + "boolean" + ], + "doc": "True if any file has a nan partition value", + "field-id": 518 + }, + { + "name": "lower_bound", + "type": [ + "null", + "bytes" + ], + "doc": "Partition lower bound for all files", + "field-id": 510 + }, + { + "name": "upper_bound", + "type": [ + "null", + "bytes" + ], + "doc": "Partition upper bound for all files", + "field-id": 511 + } + ] + }, + "element-id": 508 + } + ], + "doc": "Summary for each partition", + "field-id": 507 + } + ] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/example_table_metadata_v2.json b/libs/iceberg/testdata/example_table_metadata_v2.json new file mode 100644 index 0000000..809c355 --- /dev/null +++ b/libs/iceberg/testdata/example_table_metadata_v2.json @@ -0,0 +1,61 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "{{ table_location }}", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 1, + "schemas": [ + {"type": "struct", "schema-id": 0, "fields": [{"id": 1, "name": "x", "required": true, "type": "long"}]}, + { + "type": "struct", + "schema-id": 1, + "identifier-field-ids": [1, 2], + "fields": [ + {"id": 1, "name": "x", "required": true, "type": "long"}, + {"id": 2, "name": "y", "required": true, "type": "long", "doc": "comment"}, + {"id": 3, "name": "z", "required": true, "type": "long"} + ] + } + ], + "default-spec-id": 0, + "partition-specs": [{"spec-id": 0, "fields": [{"name": "x", "transform": "identity", "source-id": 1, "field-id": 1000}]}], + "last-partition-id": 1000, + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + {"transform": "identity", "source-id": 2, "direction": "asc", "null-order": "nulls-first"}, + {"transform": "bucket[4]", "source-id": 3, "direction": "desc", "null-order": "nulls-last"} + ] + } + ], + "properties": {"read.split.target.size": "134217728"}, + "current-snapshot-id": 3055729675574597004, + "snapshots": [ + { + "snapshot-id": 3051729675574597004, + "timestamp-ms": 1515100955770, + "sequence-number": 0, + "summary": {"operation": "append"}, + "manifest-list": "{{ manifest_list_1_location }}" + }, + { + "snapshot-id": 3055729675574597004, + "parent-snapshot-id": 3051729675574597004, + "timestamp-ms": 1555100955770, + "sequence-number": 1, + "summary": {"operation": "append"}, + "manifest-list": "{{ manifest_list_2_location }}", + "schema-id": 1 + } + ], + "snapshot-log": [ + {"snapshot-id": 3051729675574597004, "timestamp-ms": 1515100955770}, + {"snapshot-id": 3055729675574597004, "timestamp-ms": 1555100955770} + ], + "metadata-log": [{"metadata-file": "{{ table_metadata_1_location }}", "timestamp-ms": 1515100}], + "refs": {"test": {"snapshot-id": 3051729675574597004, "type": "tag", "max-ref-age-ms": 10000000}} +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataUnsupportedVersion.json b/libs/iceberg/testdata/table_metadata/TableMetadataUnsupportedVersion.json new file mode 100644 index 0000000..0633a71 --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataUnsupportedVersion.json @@ -0,0 +1,36 @@ +{ + "format-version": 3, + "table-uuid": "d20125c8-7284-442c-9aea-15fee620737c", + "location": "s3://bucket/test/location", + "last-updated-ms": 1602638573874, + "last-sequence-number": 0, + "last-column-id": 3, + "schema": { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }, + "partition-spec": [], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV1Valid.json b/libs/iceberg/testdata/table_metadata/TableMetadataV1Valid.json new file mode 100644 index 0000000..0b55d51 --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV1Valid.json @@ -0,0 +1,42 @@ +{ + "format-version": 1, + "table-uuid": "d20125c8-7284-442c-9aea-15fee620737c", + "location": "s3://bucket/test/location", + "last-updated-ms": 1602638573874, + "last-column-id": 3, + "schema": { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }, + "partition-spec": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json new file mode 100644 index 0000000..d010785 --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json @@ -0,0 +1,88 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 2, + "schemas": [ + { + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + } + ] + }, + { + "type": "struct", + "schema-id": 1, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + } + ], + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "last-partition-id": 1000, + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json new file mode 100644 index 0000000..31c2b4c --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json @@ -0,0 +1,73 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 0, + "schemas": [{ + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }], + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json new file mode 100644 index 0000000..3ab0a7a --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json @@ -0,0 +1,67 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 0, + "schemas": [{ + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }], + "partition-spec": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ], + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSchemas.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSchemas.json new file mode 100644 index 0000000..3754354 --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSchemas.json @@ -0,0 +1,71 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "schema": { + "type": "struct", + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }, + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSortOrder.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSortOrder.json new file mode 100644 index 0000000..fbbcf41 --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2MissingSortOrder.json @@ -0,0 +1,54 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 0, + "schemas": [{ + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + }], + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "last-partition-id": 1000, + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2Valid.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2Valid.json new file mode 100644 index 0000000..0dc89de --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2Valid.json @@ -0,0 +1,122 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 1, + "schemas": [ + { + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + } + ] + }, + { + "type": "struct", + "schema-id": 1, + "identifier-field-ids": [ + 1, + 2 + ], + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + } + ], + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "last-partition-id": 1000, + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ], + "properties": {}, + "current-snapshot-id": 3055729675574597004, + "snapshots": [ + { + "snapshot-id": 3051729675574597004, + "timestamp-ms": 1515100955770, + "sequence-number": 0, + "summary": { + "operation": "append" + }, + "manifest-list": "s3://a/b/1.avro" + }, + { + "snapshot-id": 3055729675574597004, + "parent-snapshot-id": 3051729675574597004, + "timestamp-ms": 1555100955770, + "sequence-number": 1, + "summary": { + "operation": "append" + }, + "manifest-list": "s3://a/b/2.avro", + "schema-id": 1 + } + ], + "snapshot-log": [ + { + "snapshot-id": 3051729675574597004, + "timestamp-ms": 1515100955770 + }, + { + "snapshot-id": 3055729675574597004, + "timestamp-ms": 1555100955770 + } + ], + "metadata-log": [] +} \ No newline at end of file diff --git a/libs/iceberg/testdata/table_metadata/TableMetadataV2ValidMinimal.json b/libs/iceberg/testdata/table_metadata/TableMetadataV2ValidMinimal.json new file mode 100644 index 0000000..529b10d --- /dev/null +++ b/libs/iceberg/testdata/table_metadata/TableMetadataV2ValidMinimal.json @@ -0,0 +1,71 @@ +{ + "format-version": 2, + "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1", + "location": "s3://bucket/test/location", + "last-sequence-number": 34, + "last-updated-ms": 1602638573590, + "last-column-id": 3, + "current-schema-id": 0, + "schemas": [ + { + "type": "struct", + "schema-id": 0, + "fields": [ + { + "id": 1, + "name": "x", + "required": true, + "type": "long" + }, + { + "id": 2, + "name": "y", + "required": true, + "type": "long", + "doc": "comment" + }, + { + "id": 3, + "name": "z", + "required": true, + "type": "long" + } + ] + } + ], + "default-spec-id": 0, + "partition-specs": [ + { + "spec-id": 0, + "fields": [ + { + "name": "x", + "transform": "identity", + "source-id": 1, + "field-id": 1000 + } + ] + } + ], + "last-partition-id": 1000, + "default-sort-order-id": 3, + "sort-orders": [ + { + "order-id": 3, + "fields": [ + { + "transform": "identity", + "source-id": 2, + "direction": "asc", + "null-order": "nulls-first" + }, + { + "transform": "bucket[4]", + "source-id": 3, + "direction": "desc", + "null-order": "nulls-last" + } + ] + } + ] +} \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..54aa0cc --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2018" +max_width = 100 +tab_spaces = 2 +hard_tabs = false diff --git a/scripts/parse_dependencies.py b/scripts/parse_dependencies.py new file mode 100644 index 0000000..551f3a7 --- /dev/null +++ b/scripts/parse_dependencies.py @@ -0,0 +1,42 @@ +import os +import sys + +begin = False +package_version = {} +with open('./Cargo.toml') as f: + for line in f: + if '[' == line[0]: + begin = False + if 'dependencies' in line: + begin = True + continue + + if begin: + sep = line.find('=') + package_version[line[:sep-1].strip()] = line[sep+2:].strip() + +for dir_path in ["./libs/iceberg/", "./libs/rest/", "./libs/test_utils/"]: + r = open(dir_path + "Cargo.toml") + w = open(dir_path + "Cargo_n.toml", 'w') + begin = False + for r_line in r: + if '[' == r_line[0]: + begin = False + if 'dependencies' in r_line: + begin = True + w.write(r_line) + continue + + if begin: + sep = r_line.find('=') + package = r_line[:sep-1].strip() + if package in package_version: + w.writelines([f"{package} = {package_version[package]}", "\n"]) + else: + w.write(r_line) + else: + w.write(r_line) + r.close() + w.close() + os.remove(dir_path + "Cargo.toml") + os.rename(dir_path + "Cargo_n.toml", dir_path + "Cargo.toml") diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..2842023 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,5 @@ +use pickledb::PickleDb; + +struct db { + client: PickleDb, +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..7e105ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,30 @@ -fn main() { - println!("Hello, world!"); +#[macro_use] +extern crate rocket; + +mod server; +use server::routes::*; + +#[launch] +fn rocket() -> _ { + rocket::build().mount( + "/v1", + routes![ + namespace::get_namespace, + namespace::post_namespace, + namespace::head_namespace_by_name, + namespace::get_namespace_by_name, + namespace::delete_namespace_by_name, + namespace::post_namespace_properties, + table::get_table_by_namespace, + table::post_table_by_namespace, + table::register_table, + table::get_table, + table::post_table, + table::delete_table, + table::head_table, + table::rename_table, + metric::post_metrics, + config::get_config, + ], + ) } diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..6a664ab --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1 @@ +pub mod routes; diff --git a/src/server/routes/config.rs b/src/server/routes/config.rs new file mode 100644 index 0000000..22e875d --- /dev/null +++ b/src/server/routes/config.rs @@ -0,0 +1,4 @@ +#[get("/config")] +pub fn get_config() { + todo!("get_config") +} diff --git a/src/server/routes/metric.rs b/src/server/routes/metric.rs new file mode 100644 index 0000000..1c50f22 --- /dev/null +++ b/src/server/routes/metric.rs @@ -0,0 +1,5 @@ +/// Send a metrics report to this endpoint to be processed by the backend +#[post("/namespaces//tables/
/metrics")] +pub fn post_metrics(namespace: &str, table: &str) { + todo!("post_metrics") +} diff --git a/src/server/routes/mod.rs b/src/server/routes/mod.rs new file mode 100644 index 0000000..d93f96d --- /dev/null +++ b/src/server/routes/mod.rs @@ -0,0 +1,5 @@ +#[allow(dead_code)] +pub mod config; +pub mod metric; +pub mod namespace; +pub mod table; diff --git a/src/server/routes/namespace.rs b/src/server/routes/namespace.rs new file mode 100644 index 0000000..6c08ff0 --- /dev/null +++ b/src/server/routes/namespace.rs @@ -0,0 +1,35 @@ +/// List namespaces, optionally providing a parent namespace to list underneath +#[get("/namespaces")] +pub fn get_namespace() { + todo!("get_namespace") +} + +/// Create a namespace +#[post("/namespaces")] +pub fn post_namespace() { + todo!("post_namespace") +} + +/// Check if a namespace exists +#[head("/namespaces/")] +pub fn head_namespace_by_name(namespace: &str) { + todo!("head_namespace_by_name") +} + +/// Load the metadata properties for a namespace +#[get("/namespaces/")] +pub fn get_namespace_by_name(namespace: &str) { + todo!("get_namespace_by_name") +} + +/// Drop a namespace from the catalog. Namespace must be empty. +#[delete("/namespaces/")] +pub fn delete_namespace_by_name(namespace: &str) { + todo!("delete_namespace_by_name") +} + +/// Set or remove properties on a namespace +#[post("/namespaces//properties")] +pub fn post_namespace_properties(namespace: &str) { + todo!("post_namespace_properties") +} diff --git a/src/server/routes/table.rs b/src/server/routes/table.rs new file mode 100644 index 0000000..a68e9c3 --- /dev/null +++ b/src/server/routes/table.rs @@ -0,0 +1,47 @@ +/// List all table identifiers underneath a given namespace +#[get("/namespaces//tables")] +pub fn get_table_by_namespace(namespace: &str) { + todo!("get_table_by_namespace") +} + +/// Create a table in the given namespace +#[post("/namespaces//tables")] +pub fn post_table_by_namespace(namespace: &str) { + todo!("post_table_by_namespace") +} + +/// Register a table in the given namespace using given metadata file location +#[post("/namespaces//register")] +pub fn register_table(namespace: &str) { + todo!("register_table") +} + +/// Load a table from the catalog +#[get("/namespaces//tables/
")] +pub fn get_table(namespace: &str, table: &str) { + todo!("post_namespace_table") +} + +/// Commit updates to a table +#[post("/namespaces//tables/
")] +pub fn post_table(namespace: &str, table: &str) { + todo!("post_namespace_table") +} + +/// Drop a table from the catalog +#[delete("/namespaces//tables/
")] +pub fn delete_table(namespace: &str, table: &str) { + todo!("post_namespace_table") +} + +/// Check if a table exists +#[head("/namespaces//tables/
")] +pub fn head_table(namespace: &str, table: &str) { + todo!("post_namespace_table") +} + +/// Rename a table from its current name to a new name +#[post("/tables/rename")] +pub fn rename_table() { + todo!("rename_table") +} From 1283c3002bc24c933d675e6ad65fafc1369cdcc2 Mon Sep 17 00:00:00 2001 From: William Zhang Date: Sun, 31 Mar 2024 13:48:17 -0700 Subject: [PATCH 6/6] Patch codecov token. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5cf1978..66e47db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: - name: Upload to codecov uses: codecov/codecov-action@v3 with: - token: be8874e2-10d6-434f-9d52-db6094de31d6 + token: b74fae07-452c-41fb-8eb2-b164ed90340d files: lcov.info name: codecov-umbrella # optional fail_ci_if_error: true