diff --git a/.github/workflows/pr_build_linux.yml b/.github/workflows/pr_build_linux.yml index 5264a58f676..88e63b9b077 100644 --- a/.github/workflows/pr_build_linux.yml +++ b/.github/workflows/pr_build_linux.yml @@ -506,6 +506,7 @@ jobs: org.apache.comet.CometVariantProjectionSuite org.apache.spark.sql.CometVariantShreddingSuite org.apache.comet.CometIcebergNativeSuite + org.apache.comet.CometIcebergHdfsSuite org.apache.comet.CometBroadcastBuildSideSuite org.apache.comet.CometIcebergEncryptionSuite org.apache.comet.CometIcebergRewriteActionSuite diff --git a/.github/workflows/pr_build_macos.yml b/.github/workflows/pr_build_macos.yml index 55f04de3cd3..4adf8ed22c4 100644 --- a/.github/workflows/pr_build_macos.yml +++ b/.github/workflows/pr_build_macos.yml @@ -172,6 +172,7 @@ jobs: org.apache.comet.CometVariantProjectionSuite org.apache.spark.sql.CometVariantShreddingSuite org.apache.comet.CometIcebergNativeSuite + org.apache.comet.CometIcebergHdfsSuite org.apache.comet.CometBroadcastBuildSideSuite org.apache.comet.CometIcebergEncryptionSuite org.apache.comet.CometIcebergRewriteActionSuite diff --git a/docs/source/contributor-guide/roadmap.md b/docs/source/contributor-guide/roadmap.md index 06bff3435dd..2724910e85f 100644 --- a/docs/source/contributor-guide/roadmap.md +++ b/docs/source/contributor-guide/roadmap.md @@ -68,9 +68,8 @@ Comet's native Iceberg scans read V3 tables, including encrypted tables ([#4991] ([#5853]). We want to add the remaining V3 features so that these scans don't fall back to Spark: row lineage metadata columns, column default values, and the new V3 types (`variant`, `geometry`, `geography`, and `unknown`). The work is tracked in [#3376], and upstream `iceberg-rust` support in [iceberg-rust #2411]. Native Iceberg scans -also don't support HDFS-backed tables today: Comet's native Iceberg storage layer handles only local files, S3 and -S3-compatible stores, GCS, and OSS, and would need an HDFS `StorageFactory` upstream in `iceberg-storage-opendal`. -We're scoping what that work would take. +read HDFS-backed tables through iceberg-rust's experimental `hdfs-native` backend, but fall back to Spark on clusters +with Kerberos, because that backend cannot authenticate with delegation tokens yet. [#3376]: https://github.com/apache/datafusion-comet/issues/3376 [#4991]: https://github.com/apache/datafusion-comet/pull/4991 diff --git a/docs/source/contributor-guide/s3-credential-provider-design.md b/docs/source/contributor-guide/s3-credential-provider-design.md index a30548566bd..416e2f8e50f 100644 --- a/docs/source/contributor-guide/s3-credential-provider-design.md +++ b/docs/source/contributor-guide/s3-credential-provider-design.md @@ -142,7 +142,7 @@ The 5-minute fallback on the Iceberg path bounds how long reqsign reuses a crede ## Property-bag handling on the Iceberg path -The full unfiltered FileIO property bag crosses JNI as `catalog_properties`. The storage-prefix filter (`s3.`/`gcs.`/`adls.`/`client.`/`opendal.`) is applied native-side in `iceberg_common.rs::build_file_io` immediately before `FileIOBuilder.with_prop`. This means the bridge sees `credentials.uri`, OAuth tokens, and any vendor-custom keys with no parallel field on the operator and no driver-side broadcast. Vendors set their own keys on the catalog config and read them back inside `initialize(Map)`. +The full unfiltered FileIO property bag crosses JNI as `catalog_properties`. The storage-prefix filter (`s3.`/`gcs.`/`adls.`/`client.`/`opendal.`/`hdfs.`/`hadoop.`) is applied native-side in `iceberg_common.rs::build_file_io` immediately before `FileIOBuilder.with_prop`. This means the bridge sees `credentials.uri`, OAuth tokens, and any vendor-custom keys with no parallel field on the operator and no driver-side broadcast. Vendors set their own keys on the catalog config and read them back inside `initialize(Map)`. `IcebergScanExec` derives a redacting `Debug`, and the `FileIO` cache key's `Debug` omits the property bag, so plan dumps and tracing do not leak it. diff --git a/docs/source/user-guide/latest/datasources.md b/docs/source/user-guide/latest/datasources.md index b9754029911..862f42c24c3 100644 --- a/docs/source/user-guide/latest/datasources.md +++ b/docs/source/user-guide/latest/datasources.md @@ -166,8 +166,8 @@ attaches its own worker threads, so a worker that has read from HDFS can crash t the HDFS read itself, typically while an unrelated query is running. ``` -Native Iceberg scans do not support HDFS-backed tables; those scans fall back to Spark. See the -[Iceberg](iceberg.md) page. +Native Iceberg scans read HDFS through a different client, iceberg-rust's `hdfs-native` backend. See +the [Iceberg](iceberg.md#hdfs) page. ### Building Comet with HDFS support diff --git a/docs/source/user-guide/latest/iceberg.md b/docs/source/user-guide/latest/iceberg.md index fd333043013..1dd08092bba 100644 --- a/docs/source/user-guide/latest/iceberg.md +++ b/docs/source/user-guide/latest/iceberg.md @@ -138,8 +138,7 @@ safe partial pruning is tracked in [#5883](https://github.com/apache/datafusion- - Local filesystem - S3-compatible storage (AWS S3, MinIO) - Google Cloud Storage (`gs`) and Alibaba Cloud OSS (`oss`) - -HDFS-backed tables are not supported by the native Iceberg reader and fall back to Spark. +- HDFS (`hdfs`), experimental (see [HDFS](#hdfs)) ### REST Catalog @@ -194,6 +193,32 @@ For a custom S3-compatible endpoint, configure the catalog with the endpoint, pa These `s3.*` storage properties are not specific to the Hive catalog shown here. When `s3.access-key-id` / `s3.secret-access-key` are omitted, credentials come from the standard AWS chain (environment variables, instance profiles, and so on). The region is not auto-detected: when neither the catalog (`client.region` or `s3.region`) nor the executor environment (`AWS_REGION` or `AWS_DEFAULT_REGION`) supplies one, Comet uses `us-east-1`, so set it for AWS buckets in any other region. If your REST catalog vends temporary credentials, the native reader does not consume them automatically, and wiring that requires the credential provider bridge. See Iceberg's [S3 FileIO](https://iceberg.apache.org/docs/latest/aws/#s3-fileio) docs for the full property list, and [S3 Credential Providers](s3-credential-providers.md) for vended or per-request credentials. +### HDFS + +The native reader reads HDFS through iceberg-rust's experimental `hdfs-native` backend, a Rust HDFS +client that uses neither libhdfs nor the JVM. The client reads `core-site.xml` and `hdfs-site.xml` +from `$HADOOP_CONF_DIR`, or from `$HADOOP_HOME/etc/hadoop`, on each executor, so `spark.hadoop.*` +settings do not reach it. The one exception is an HA nameservice. A location such as +`hdfs://ns1/warehouse` has no port, and Comet passes the client the NameNodes that the session's +Hadoop configuration declares for `ns1` with `dfs.ha.namenodes.ns1` and +`dfs.namenode.rpc-address.ns1.`. A portless location that the configuration does not declare +is a single NameNode on Hadoop's default port, 8020, as it is to the JVM client. + +A scan of HDFS files falls back to Spark when `hadoop.security.authentication` is not `simple`, +because the client cannot authenticate with the delegation tokens that Spark executors use on a +Kerberos cluster. It also falls back when a file location has no port and names a nameservice that +the Hadoop configuration does not declare, or when it has no authority (`hdfs:///path`), and when a +data or delete file lies in an HDFS encryption zone: the client is built without KMS support and +cannot decrypt it. Comet checks that at planning, on clusters whose NameNode advertises a key +provider, with one directory listing per parent of the data directories, cached for the session. + +On a cluster without Kerberos, the client connects as `HADOOP_USER_NAME`, or as the executor's +operating system user when that variable is unset, rather than as the Spark user. Comet therefore +claims an HDFS scan only when the executors are known to read as the Spark user: set +`spark.executorEnv.HADOOP_USER_NAME` to the user that submits the job (in local mode, the JVM's +`HADOOP_USER_NAME` or operating system user must be that user). Otherwise the scan falls back with +a reason that names the setting. Writes to HDFS use Iceberg's own writer. + ### Current limitations The following scenarios will fall back to the JVM Iceberg reader: @@ -210,6 +235,12 @@ The following scenarios will fall back to the JVM Iceberg reader: - Queries that use `input_file_name()`, `input_file_block_start()` or `input_file_block_length()` - Scans whose data or delete files span more than one S3 bucket (the native reader uses one object-store configuration per scan) +- HDFS scans on a cluster with Kerberos, or whose executors are not known to read as the Spark + user; HDFS file locations that name a nameservice without NameNodes or that have no authority; + and HDFS files in an encryption zone (see [HDFS](#hdfs)) +- Scans whose data or delete files are on a different storage backend than the table metadata, for + example a local or REST-served metadata location with `write.data.path` on S3 or HDFS. The native + reader opens every file of a scan through one storage, chosen from the metadata location - Scans where Iceberg reports an output ordering (for example with `spark.sql.iceberg.planning.preserve-data-ordering=true`). Spark may then skip the sort above the scan, and the native reader does not preserve file order @@ -800,8 +831,11 @@ Which entries apply depends on which native paths are enabled: ### Reads that differ from Spark -- **Data files with neither field ids nor a name mapping.** Spark reads every nested field of such - a file as NULL, and the native scan returns the values stored in the file +- **Data files with neither field ids nor a name mapping.** Neither reader can match such a + file's nested fields to the table's, so a struct's fields read as NULL in both. The containers + differ: Spark's reader returns NULL for an optional struct itself, where the native scan + returns a struct of NULL fields, and an empty list for a list of structs, where the native scan + returns the list with each element's fields NULL ([#6794](https://github.com/apache/datafusion-comet/issues/6794)). - **`VERSION AS OF` reads of a column renamed after the snapshot.** The renamed column reads as NULL, and two columns that swapped names return each other's values diff --git a/native/Cargo.lock b/native/Cargo.lock index 80741b85406..69754efb0cc 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -34,10 +34,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.1", +] + [[package]] name = "aes-gcm" version = "0.10.3" @@ -45,9 +56,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes", - "cipher", - "ctr", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", "ghash", "subtle", ] @@ -1168,6 +1179,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "blocking" version = "1.7.0" @@ -1299,7 +1319,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", ] [[package]] @@ -1395,7 +1424,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.5", ] [[package]] @@ -1603,6 +1643,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1621,6 +1667,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc-fast" version = "1.10.0" @@ -1781,7 +1842,16 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", ] [[package]] @@ -2017,7 +2087,7 @@ dependencies = [ "jni", "lazy_static", "libc", - "libloading", + "libloading 0.8.9", "log", "log4rs", "mimalloc", @@ -2876,6 +2946,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "destructure_traitobject" version = "0.2.0" @@ -2926,6 +3005,18 @@ dependencies = [ "const-random", ] +[[package]] +name = "dns-lookup" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "windows-sys 0.60.2", +] + [[package]] name = "dunce" version = "1.0.5" @@ -3214,6 +3305,34 @@ dependencies = [ "slab", ] +[[package]] +name = "g2gen" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a7e0eb46f83a20260b850117d204366674e85d3a908d90865c78df9a6b1dfc" +dependencies = [ + "g2poly", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "g2p" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539e2644c030d3bf4cd208cb842d2ce2f80e82e6e8472390bcef83ceba0d80ad" +dependencies = [ + "g2gen", + "g2poly", +] + +[[package]] +name = "g2poly" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312d2295c7302019c395cfb90dacd00a82a2eabd700429bba9c7a3f38dbbe11b" + [[package]] name = "generic-array" version = "0.14.7" @@ -3233,7 +3352,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -3360,6 +3479,48 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hdfs-native" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5141c4b745cab57a0018daa9c2759c92308bcf4e41e0c617d05fbf9639c16754" +dependencies = [ + "aes 0.9.3", + "base64 0.22.1", + "bitflags 2.13.2", + "bumpalo", + "bytes", + "cbc 0.2.1", + "chrono", + "cipher 0.5.2", + "crc", + "ctr 0.10.1", + "des", + "dns-lookup", + "futures", + "g2p", + "hex", + "hmac 0.13.0", + "libc", + "libloading 0.9.0", + "log", + "md-5 0.11.0", + "num-traits", + "once_cell", + "prost", + "prost-types", + "rand 0.10.3", + "regex", + "roxmltree", + "socket2", + "thiserror", + "tokio", + "tokio-util", + "url", + "uuid", + "whoami", +] + [[package]] name = "hdfs-sys" version = "0.3.1" @@ -3568,7 +3729,7 @@ dependencies = [ [[package]] name = "iceberg" version = "0.10.1" -source = "git+https://github.com/apache/iceberg-rust?rev=1f3bc34058e3f3e45336bc5428f37b92649a608b#1f3bc34058e3f3e45336bc5428f37b92649a608b" +source = "git+https://github.com/apache/iceberg-rust?rev=de0a6ac8471da43f63382d66fac27d7e56fd273e#de0a6ac8471da43f63382d66fac27d7e56fd273e" dependencies = [ "aes-gcm", "anyhow", @@ -3626,7 +3787,7 @@ dependencies = [ [[package]] name = "iceberg-property-macro" version = "0.10.1" -source = "git+https://github.com/apache/iceberg-rust?rev=1f3bc34058e3f3e45336bc5428f37b92649a608b#1f3bc34058e3f3e45336bc5428f37b92649a608b" +source = "git+https://github.com/apache/iceberg-rust?rev=de0a6ac8471da43f63382d66fac27d7e56fd273e#de0a6ac8471da43f63382d66fac27d7e56fd273e" dependencies = [ "proc-macro2", "quote", @@ -3636,7 +3797,7 @@ dependencies = [ [[package]] name = "iceberg-storage-opendal" version = "0.10.1" -source = "git+https://github.com/apache/iceberg-rust?rev=1f3bc34058e3f3e45336bc5428f37b92649a608b#1f3bc34058e3f3e45336bc5428f37b92649a608b" +source = "git+https://github.com/apache/iceberg-rust?rev=de0a6ac8471da43f63382d66fac27d7e56fd273e#de0a6ac8471da43f63382d66fac27d7e56fd273e" dependencies = [ "anyhow", "async-trait", @@ -3649,6 +3810,8 @@ dependencies = [ "reqsign-aws-v4", "reqsign-core", "serde", + "tokio", + "tracing", "typetag", "url", ] @@ -3810,10 +3973,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", + "block-padding 0.3.3", "generic-array", ] +[[package]] +name = "inout" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + [[package]] name = "inventory" version = "0.3.25" @@ -3955,7 +4128,7 @@ dependencies = [ "java-locator", "jni-macros", "jni-sys", - "libloading", + "libloading 0.8.9", "log", "simd_cesu8", "thiserror", @@ -4113,6 +4286,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "liblzma" version = "0.4.9" @@ -4148,6 +4331,15 @@ dependencies = [ "cc", ] +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + [[package]] name = "link-section" version = "0.19.3" @@ -4321,7 +4513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -4490,6 +4682,24 @@ dependencies = [ "libm", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -4597,6 +4807,7 @@ dependencies = [ "opendal-service-fs", "opendal-service-gcs", "opendal-service-hdfs", + "opendal-service-hdfs-native", "opendal-service-oss", "opendal-service-s3", ] @@ -4765,6 +4976,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "opendal-service-hdfs-native" +version = "0.58.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebbf956ea9e64d70fc8d1f25381604c02528805c23bd55443b8e924b438222" +dependencies = [ + "bytes", + "futures", + "hdfs-native", + "log", + "opendal-core", + "serde", +] + [[package]] name = "opendal-service-oss" version = "0.58.2" @@ -5112,8 +5337,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" dependencies = [ - "aes", - "cbc", + "aes 0.8.4", + "cbc 0.1.2", "der", "pbkdf2", "scrypt", @@ -5962,6 +6187,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "rsa" version = "0.9.10" @@ -6121,7 +6355,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -7180,6 +7414,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -7189,6 +7432,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -7300,6 +7552,19 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7396,7 +7661,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -7414,14 +7688,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -7430,48 +7721,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/native/Cargo.toml b/native/Cargo.toml index c858f9e715b..24cd6e3fb69 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -85,8 +85,8 @@ aws-sdk-sts = { version = "1.114.0", default-features = false } # Pinned to a revision rather than a release because no published iceberg-rust # version is on our arrow: 0.10.1 requires arrow/parquet ^58. Move to a plain # version requirement once a release ships on arrow 59. Bump policy: #5645. -iceberg = { git = "https://github.com/apache/iceberg-rust", rev = "1f3bc34058e3f3e45336bc5428f37b92649a608b" } -iceberg-storage-opendal = { git = "https://github.com/apache/iceberg-rust", rev = "1f3bc34058e3f3e45336bc5428f37b92649a608b", features = ["opendal-memory", "opendal-fs", "opendal-s3", "opendal-gcs", "opendal-oss", "opendal-azdls"] } +iceberg = { git = "https://github.com/apache/iceberg-rust", rev = "de0a6ac8471da43f63382d66fac27d7e56fd273e" } +iceberg-storage-opendal = { git = "https://github.com/apache/iceberg-rust", rev = "de0a6ac8471da43f63382d66fac27d7e56fd273e", features = ["opendal-memory", "opendal-fs", "opendal-s3", "opendal-gcs", "opendal-oss", "opendal-azdls", "opendal-hdfs-native"] } reqsign-core = "3" [profile.release] diff --git a/native/core/src/execution/operators/iceberg_common.rs b/native/core/src/execution/operators/iceberg_common.rs index a17eb076e0d..f3f2286bb8a 100644 --- a/native/core/src/execution/operators/iceberg_common.rs +++ b/native/core/src/execution/operators/iceberg_common.rs @@ -45,8 +45,11 @@ const ICEBERG_PROVIDER_CLASS_PROPERTY: &str = "s3.comet.credential.provider.clas /// Key prefixes forwarded to iceberg-rust's `FileIO`. The full unfiltered catalog bag (catalog /// URI, OAuth tokens, credentials.uri, tenant-id, etc.) is kept upstream so /// `CometS3CredentialBridge` can read whatever the vendor needs. `opendal.` carries -/// iceberg-storage-opendal's own settings, such as `opendal.io-timeout-ms`. -const STORAGE_PROPERTY_PREFIXES: &[&str] = &["s3.", "gcs.", "adls.", "client.", "opendal."]; +/// iceberg-storage-opendal's own settings, such as `opendal.io-timeout-ms`. `hdfs.` and `hadoop.` +/// carry the HDFS backend's NameNode declarations and Hadoop client settings. +const STORAGE_PROPERTY_PREFIXES: &[&str] = &[ + "s3.", "gcs.", "adls.", "client.", "opendal.", "hdfs.", "hadoop.", +]; /// Pick an OpenDAL storage backend for a URI whose scheme `builtin_storage_schemes` lists for /// `access_mode`, or that is an opted-in S3-compliant alias written in lowercase; anything else is @@ -79,6 +82,7 @@ pub(crate) fn storage_factory_for( "memory" => Ok((Arc::new(OpenDalStorageFactory::Memory), true)), "gs" => Ok((Arc::new(OpenDalStorageFactory::Gcs), true)), "oss" => Ok((Arc::new(OpenDalStorageFactory::Oss), true)), + "hdfs" => Ok((Arc::new(OpenDalStorageFactory::HdfsNative), true)), // s3, s3a, and any opted-in s3-compliant alias (e.g. blob) route to the S3 backend. Listed // last so the built-in backends above stay authoritative even if one of their schemes is // also named in `fs.comet.s3Compliant.schemes`. An alias additionally gets a wrapper that @@ -302,9 +306,10 @@ fn cached_file_io( /// mode; the JVM read and write gates load these lists over JNI. `memory` is write-only: an OpenDAL /// memory backend is a fresh empty in-process store the write path assembles manifests in, so a /// read finds nothing. `oss` is read-only: no `oss.*` property is forwarded and no test covers it. +/// `hdfs` is read-only: the write path has not been tested against it. pub(crate) fn builtin_storage_schemes(access_mode: AccessMode) -> &'static [&'static str] { match access_mode { - AccessMode::Read => &["file", "gs", "oss", "s3", "s3a"], + AccessMode::Read => &["file", "gs", "hdfs", "oss", "s3", "s3a"], AccessMode::Write => &["file", "memory", "gs", "s3", "s3a"], } } @@ -847,6 +852,158 @@ mod tests { ); } + #[test] + fn hdfs_scheme_is_readable_but_not_writable() { + assert!(factory_result("hdfs://nn:8020/db/table", AccessMode::Read).is_ok()); + let err = factory_result("hdfs://nn:8020/db/table", AccessMode::Write).unwrap_err(); + assert!( + err.contains("Unsupported storage scheme: hdfs"), + "unexpected error: {err}" + ); + } + + /// The JVM declares a nameservice as `hdfs.name-node.`, and a catalog can declare one with + /// Hadoop's own keys under `hadoop.`. + #[test] + fn hdfs_properties_reach_the_file_io() { + let props = HashMap::from([ + ("hdfs.name-node.ns1".to_string(), "nn1:8020".to_string()), + ("hadoop.dfs.ha.namenodes.ns2".to_string(), "a".to_string()), + ( + "hadoop.dfs.namenode.rpc-address.ns2.a".to_string(), + "nn2:8020".to_string(), + ), + ]); + let (file_io, _) = + build_file_io(&props, "hdfs://ns2/warehouse", "", AccessMode::Read).unwrap(); + for (key, value) in &props { + assert_eq!(file_io.config().get(key), Some(value), "{key}"); + } + } + + /// The cache is shared per executor, and an HDFS `FileIO` keeps its NameNode clients for life. + /// Two catalogs that differ only in how they reach HDFS must therefore never share a key, or + /// the second would be served by the first one's clients. A regression pin on the key, which + /// hashes every property. + #[test] + fn cache_key_separates_hdfs_catalogs() { + let table = "hdfs://nameservice1/warehouse/db/t"; + let props = |name_node: &str, random_order: &str| { + HashMap::from([ + // What the JVM declares for an `hdfs://nameservice1/...` table. + ( + "hdfs.name-node.nameservice1".to_string(), + name_node.to_string(), + ), + ( + "hadoop.dfs.client.failover.random.order".to_string(), + random_order.to_string(), + ), + ]) + }; + let key = |props: &HashMap| { + FileIoCacheKey::new(props, table, "cat", AccessMode::Read) + }; + let base = props("nn1:8020,nn2:8020", "true"); + + // Control: the same properties always give the same key, so the inequalities below are + // down to the one property that changed. + assert!(key(&base).is_some()); + assert_eq!(key(&base), key(&base.clone())); + + // Only the declaration differs: another NameNode, another list, the same list reordered + // (which changes the failover order), and no list at all. + for other in [ + props("nn1:8020,nn3:8020", "true"), + props("nn1:8020", "true"), + props("nn2:8020,nn1:8020", "true"), + ] { + assert_ne!(key(&base), key(&other), "{other:?}"); + } + let mut without_name_node = base.clone(); + without_name_node.remove("hdfs.name-node.nameservice1"); + assert_ne!(key(&base), key(&without_name_node)); + + // Only the `hadoop.dfs.client.failover.*` overrides differ: a changed value, an added + // override and a removed one. + let mut other_failover = base.clone(); + other_failover.insert( + "hadoop.dfs.client.failover.proxy.provider.nameservice1".to_string(), + "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider".to_string(), + ); + let mut without_failover = base.clone(); + without_failover.remove("hadoop.dfs.client.failover.random.order"); + for other in [ + props("nn1:8020,nn2:8020", "false"), + other_failover, + without_failover, + ] { + assert_ne!(key(&base), key(&other), "{other:?}"); + } + } + + /// Builds the real HDFS storage from one property the way a `FileIO` does on first use. + /// Parsing is all it does: an operator, and with it a NameNode connection, is only made by + /// the first storage call, so this needs neither a NameNode nor a runtime. The storage is + /// inspected through its serde form, so this is a contract test on the pinned iceberg-rust + /// revision and its config field names. + fn build_hdfs_storage(key: &str, value: &str) -> Result { + let config = + StorageConfig::from_props(HashMap::from([(key.to_string(), value.to_string())])); + let storage = OpenDalStorageFactory::HdfsNative + .build(&config) + .map_err(|e| e.to_string())?; + Ok(serde_json::to_value(&*storage).expect("the storage serializes")) + } + + /// One Hadoop option of the parsed storage config. + fn parsed_option<'a>(storage: &'a serde_json::Value, key: &str) -> Option<&'a str> { + storage + .pointer("/HdfsNative/config/options") + .and_then(|options| options.get(key)) + .and_then(serde_json::Value::as_str) + } + + /// The contract `CometIcebergNativeScan.hadoopToIcebergHdfsProperties` relies on: it declares + /// an HA nameservice as `hdfs.name-node.`, and iceberg-rust expands that into + /// Hadoop's own `dfs.ha.namenodes.` / `dfs.namenode.rpc-address..`, the keys its + /// resolver reads for a portless path authority. A portless entry is refused when the + /// storage is built, naming the key and the entry. + #[test] + fn hdfs_nameservice_declaration_is_parsed_by_the_real_storage() { + for input in [ + "nn1:8020,nn2:8020", + // Whitespace around entries, a trailing slash and the `hdfs://` prefix are tolerated. + " hdfs://nn1:8020/ , nn2:8020 ", + ] { + let storage = build_hdfs_storage("hdfs.name-node.ns1", input) + .unwrap_or_else(|e| panic!("`{input}` should be accepted: {e}")); + assert_eq!( + parsed_option(&storage, "dfs.ha.namenodes.ns1"), + Some("nn0,nn1"), + "input `{input}`: {storage}" + ); + assert_eq!( + parsed_option(&storage, "dfs.namenode.rpc-address.ns1.nn0"), + Some("nn1:8020"), + "input `{input}`: {storage}" + ); + assert_eq!( + parsed_option(&storage, "dfs.namenode.rpc-address.ns1.nn1"), + Some("nn2:8020"), + "input `{input}`: {storage}" + ); + } + + // A portless entry would only fail at the first I/O, so it is refused here, and the + // error names the offending key and entry rather than the whole list. + let err = build_hdfs_storage("hdfs.name-node.ns1", "nn1:8020,nn2").unwrap_err(); + assert!( + err.contains("hdfs.name-node.ns1") && err.contains("nn2") && !err.contains("nn1:8020"), + "unexpected error: {err}" + ); + } + #[test] fn memory_scheme_is_writable_but_not_readable() { // The write path assembles manifests in a fresh in-process memory store. A read against @@ -948,7 +1105,7 @@ mod tests { #[test] fn unknown_scheme_is_rejected() { - let err = factory_result("hdfs://nn/db/table", AccessMode::Read).unwrap_err(); + let err = factory_result("viewfs://cluster/db/table", AccessMode::Read).unwrap_err(); assert!( err.contains("Unsupported storage scheme"), "unexpected error: {err}" @@ -997,7 +1154,7 @@ mod tests { #[test] fn unlisted_schemes_are_rejected_for_both_modes() { let unlisted = [ - "hdfs", "abfs", "abfss", "wasb", "wasbs", "gcs", "http", "https", "azure", + "viewfs", "abfs", "abfss", "wasb", "wasbs", "gcs", "http", "https", "azure", ]; for mode in [AccessMode::Read, AccessMode::Write] { for scheme in unlisted { diff --git a/native/core/src/execution/operators/iceberg_partition_path.rs b/native/core/src/execution/operators/iceberg_partition_path.rs index c9ead454b0f..dba99bc2b6f 100644 --- a/native/core/src/execution/operators/iceberg_partition_path.rs +++ b/native/core/src/execution/operators/iceberg_partition_path.rs @@ -152,7 +152,7 @@ const NULL: &str = "null"; /// | `timestamp` | `1969-12-31T23:59:58.5` | `1969-12-31 23:59:58.500` | /// | `timestamptz` | `1969-12-31T23:59:58.5+00:00` | panics for a negative value with a sub-second part; otherwise `1969-12-31 23:59:58.500 UTC` | /// | `binary` / `fixed` | base64 | uppercase hex | -/// | `float` / `double` | `1.0`, `1.0E20` | `1`, `100000000000000000000` | +/// | `float` / `double` | `1.0`, `1.0E20` | `1.0`, `1.0E20` since #3358; `Double::MAX` still renders as 309 digits (see below) | /// /// The nanosecond timestamp types get the same treatment for the same reason. Spark cannot plan a /// write to them today, and `CometIcebergNativeWrite`'s column-type rule declines them as well; the diff --git a/pom.xml b/pom.xml index 292fc21d702..b8c777e7a57 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,10 @@ under the License. 3.25.5 1.16.0 provided - 3.3.4 + + 3.4.2 18.3.0 1.9.13 2.43.0 @@ -106,7 +109,7 @@ under the License. + it down to 3.3.x (SDK v1). --> 3.4.1