sunchao commented on code in PR #4952: URL: https://github.com/apache/datafusion-comet/pull/4952#discussion_r3807494654
########## dev/verify-contrib-delta-gate.sh: ########## @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# +# 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. +# +# Verify the `contrib-delta` build gate keeps Delta surface out of default builds. +# +# Three independent layers are checked: +# 1. Cargo: default `cargo build` doesn't compile `comet-contrib-delta` and +# doesn't pull `delta_kernel` into the dependency tree. +# 2. Maven: default `mvn ... package` doesn't compile any +# `org/apache/comet/contrib/` classes and doesn't pull `io.delta:*` deps. +# 3. Symbol/size: the resulting `libcomet` (`.so` on Linux, `.dylib` on macOS) from the default build is +# meaningfully smaller than the contrib-enabled build, and carries no +# `comet_contrib_delta`/`delta_kernel`/etc. external symbols. +# +# Exit non-zero on the first failure. Designed to be wired into CI so a future +# change that leaks Delta into core gets caught immediately. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NATIVE_DIR="$ROOT/native" +SPARK_DIR="$ROOT/spark" + +# CI containers check out the repo as a different user than the job runs as, so git refuses to +# operate on it ("detected dubious ownership") -- which can make Maven plugins that read git +# metadata fail fast with no useful output. Mark the tree safe up front (best-effort). +git config --global --add safe.directory "$ROOT" 2>/dev/null || true +git config --global --add safe.directory '*' 2>/dev/null || true Review Comment: [P2] Keep the Git ownership exception scoped to this checkout Please remove the global wildcard. Running this developer-facing script appends `safe.directory=*` to the caller's persistent Git configuration before the build starts, so the setting remains even if a later check fails. It disables Git's ownership safeguard for unrelated repositories as well. The preceding line already trusts the intended checkout; any additional CI exception should be scoped to that checkout or process. See the [Git documentation](https://git-scm.com/docs/git-config#Documentation/git-config.txt-safedirectory). ########## spark/pom.xml: ########## @@ -339,6 +371,69 @@ under the License. </dependency> </dependencies> </profile> + <!-- + Delta Lake integration. Activates the contrib/delta/ subtree: + compiles contrib/delta/src/main/scala/ into comet-spark.jar + pulls in io.delta:delta-spark at compile/test scope so the contrib's + reflective helpers and tests have the types they need + Default builds (no -P) carry zero Delta surface. Pair with the matching + 'features contrib-delta' Cargo flag when building the native library. + --> + <profile> + <id>contrib-delta</id> Review Comment: [P2] Preserve the implicit Spark profile when enabling Delta Activating `contrib-delta` disables this POM's `activeByDefault` `spark-4.1` profile. I compared the effective POMs: `-Pcontrib-delta` still targets Spark 4.1 and adds Delta, but drops the Iceberg runtime and Jetty test dependencies; `-Pspark-4.1,contrib-delta` restores them. Existing test sources such as `CometEqualityDeletes.scala` directly import Iceberg types, so the advertised `./mvnw package -Pcontrib-delta -DskipTests` invocation lacks classes needed during test compilation. Please preserve or explicitly require the matching Spark profile and cover the implicit-default invocation in the gate. This was verified through the Maven model, not a full compile. ########## contrib/delta/native/Cargo.lock: ########## @@ -0,0 +1,2765 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrow" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378530e55cd479eda3c14eb345310799717e6f76d0c332041e8487022166b471" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ab212d2c1886e802f51c5212d78ebbcbb0bec980fff9dadc1eb8d45cd0b738" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "chrono-tz", + "half", + "hashbrown 0.17.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5aefb56a2c02e9e2b30746241058b85f8983f0fcff2ba0c6d09006e1cded7f" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e8cf7e517657a52b91ea1263acf38c4ca62a84655d72458a3359b12ab97de" +dependencies = [ + "arrow-array", + "arrow-cast", + "arrow-schema", + "chrono", + "csv", + "csv-core", + "regex", +] + +[[package]] +name = "arrow-data" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ipc" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238438f0834483703d88896db6fe5a7138b2230debc31b34c0336c2996e3c64f" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "flatbuffers", + "lz4_flex", + "zstd", +] + +[[package]] +name = "arrow-json" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "205ca2119e6d679d5c133c6f30e68f027738d95ed948cf77677ea69c7800036b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-ord", + "arrow-schema", + "arrow-select", + "chrono", + "half", + "indexmap", + "itoa", + "lexical-core", + "memchr", + "num-traits", + "ryu", + "serde_core", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bffd8fd2579286a5d63bac898159873e5094a79009940bcb42bbfce4f19f1d0" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5994731204603c73ba69267616c50f80780774c6bb0476f1f830625115e0c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" +dependencies = [ + "serde_core", + "serde_json", +] + +[[package]] +name = "arrow-select" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29dd7cda3ab9692f43a2e4acc444d760cc17b12bb6d8232ddf64e9bab7c06b42" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "comet-contrib-delta" +version = "0.18.0" +dependencies = [ + "datafusion", + "datafusion-comet-proto", +] + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "datafusion" +version = "54.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997a31e15872606a49478e670c58302094c97cb96abb0a7d60720f8e92170040" +dependencies = [ + "arrow", + "arrow-schema", + "async-trait", + "chrono", + "datafusion-catalog", + "datafusion-catalog-listing", + "datafusion-common", + "datafusion-common-runtime", + "datafusion-datasource", + "datafusion-datasource-arrow", + "datafusion-datasource-csv", + "datafusion-datasource-json", + "datafusion-datasource-parquet", + "datafusion-execution", + "datafusion-expr", + "datafusion-expr-common", + "datafusion-functions", + "datafusion-functions-aggregate", + "datafusion-functions-table", + "datafusion-functions-window", + "datafusion-optimizer", + "datafusion-physical-expr", + "datafusion-physical-expr-adapter", + "datafusion-physical-expr-common", + "datafusion-physical-optimizer", + "datafusion-physical-plan", + "datafusion-session", + "futures", + "indexmap", + "itertools", + "log", + "object_store", + "parking_lot", + "parquet", + "tempfile", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "datafusion-catalog" +version = "54.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7dd61161508f8f5fa1107774ea687bd753c22d83a32eebf963549f89de14139" +dependencies = [ + "arrow", + "async-trait", + "dashmap", + "datafusion-common", + "datafusion-common-runtime", + "datafusion-datasource", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-expr", + "datafusion-physical-plan", + "datafusion-session", + "futures", + "itertools", + "log", + "object_store", + "parking_lot", + "tokio", +] + +[[package]] +name = "datafusion-catalog-listing" +version = "54.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897c70f871277f9ce99aa38347be0d679bbe3e617156c4d2a8378cec8a2a0891" +dependencies = [ + "arrow", + "async-trait", + "datafusion-catalog", + "datafusion-common", + "datafusion-datasource", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-expr", + "datafusion-physical-expr-adapter", + "datafusion-physical-expr-common", + "datafusion-physical-plan", + "futures", + "itertools", + "log", + "object_store", +] + +[[package]] +name = "datafusion-comet-proto" +version = "1.0.0" Review Comment: [P3] Refresh the standalone contrib lockfile This entry still records the path dependency `datafusion-comet-proto` as `1.0.0`, while `native/proto` now inherits workspace version `1.1.0`. `cargo tree --manifest-path contrib/delta/native/Cargo.toml --locked --offline --prefix none --depth 1` exits 101 because Cargo needs to update this lockfile. Please regenerate it so standalone locked crate commands work. The core build uses the separate, valid `native/Cargo.lock`, so this is not an enabled-core build failure. ########## spark/src/main/scala/org/apache/comet/rules/CometScanContrib.scala: ########## @@ -0,0 +1,210 @@ +/* + * 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 org.apache.comet.rules + +import java.util.ServiceLoader + +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal + +import org.apache.spark.internal.Logging +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.execution.{FileSourceScanExec, SparkPlan} +import org.apache.spark.sql.execution.datasources.HadoopFsRelation +import org.apache.spark.sql.execution.datasources.v2.BatchScanExec + +import org.apache.comet.CometConf +import org.apache.comet.serde.CometOperatorSerde + +/** + * Format-agnostic hook that lets an optional, out-of-tree contrib (Delta, Lance, ...) claim a + * scan before Comet's built-in scan handling runs. This is the scan-rule counterpart to + * [[org.apache.spark.sql.comet.PlanDataInjector]]: core holds no compile-time reference to any + * contrib and names none of them -- implementations are discovered at runtime via the JDK + * [[ServiceLoader]], so default builds (which ship no service file) see an empty registry and + * zero contrib surface. + * + * A contrib ships: + * - an implementation of this trait (e.g. `contrib/delta/.../DeltaScanRuleContrib`), and + * - a `META-INF/services/org.apache.comet.rules.CometScanContrib` resource naming it. + * + * Both hooks default to `None` so a contrib overrides only the scan kind(s) it handles. + * + * ==Ownership contract== + * + * An implementation '''MUST''' return `None` for any scan it does not own. Registered contribs + * are offered a scan one at a time and the '''first''' claim wins, so a contrib that claims a + * scan belonging to another format does not merely mis-handle that scan -- it hides it from the + * contrib that could have read it, and the outcome depends on ServiceLoader ordering, which is + * not specified. Decide ownership from something definitive about the scan itself (the relation's + * `fileFormat` class, the table's provider, a catalog type) rather than from a heuristic such as + * a path or table-name pattern, which another format may also match. + * + * "Own but cannot handle" is a distinct case from "not mine", and both are expressible: to + * decline a scan it owns, a contrib returns `Some(withFallbackReason(scanExec, "..."))` -- + * claiming the scan and terminating it with a diagnosable reason -- rather than returning `None` + * and letting Comet's built-in handling attempt a format it does not understand. + */ +trait CometScanContrib { + + /** + * V1 (`FileSourceScanExec`) hook. Return `Some(plan)` to claim the scan -- either a transformed + * plan node or an explicit fallback the contrib produced via `withFallbackReason`. Return + * `None` to pass, letting Comet's generic V1 handling proceed; see the ownership contract + * above. A claiming contrib is responsible for its own metadata-column handling (the generic + * guard in [[CometScanRule]] runs only on the pass path). + */ + def tryTransformV1( + plan: SparkPlan, + session: SparkSession, + scanExec: FileSourceScanExec, + relation: HadoopFsRelation): Option[SparkPlan] = None + + /** + * V2 (`BatchScanExec`) hook. Return `Some(plan)` to claim the scan, `None` to pass; see the + * ownership contract above. + */ + def tryTransformV2(scanExec: BatchScanExec): Option[SparkPlan] = None +} + +object CometScanContrib extends Logging { + + // Discovered contrib scan handlers. Mirrors `PlanDataInjector.injectors`: the standard JDK + // ServiceLoader forces the provider iterator here so a misbuilt contrib jar (malformed service + // file, or a listed provider that can't be instantiated) surfaces as a warning rather than + // taking down the planner. Default builds carry no service file, so this is empty and there is + // no contrib surface at runtime. There are no built-in (in-core) contribs today; the format + // built-ins (Parquet/Iceberg) keep their existing paths in CometScanRule. + private[comet] lazy val contribs: Seq[CometScanContrib] = + try { + ServiceLoader.load(classOf[CometScanContrib], getClass.getClassLoader).asScala.toSeq Review Comment: [P2] Discover separate contrib jars through Spark's context classloader When Comet is installed on `spark.driver.extraClassPath` and a separate contrib is supplied through `--jars`, Comet's defining loader is the parent of Spark's user-jar loader. Passing that parent explicitly means the child-only service is invisible, so the registry silently ignores the contrib. An isolated test against this unchanged class returns `None` through the production registry while the thread-context loader discovers and dispatches the same provider. Please use Spark's context classloader with an appropriate fallback, as the existing `org.apache.comet.util.ClassLoaders` helper does, and exercise the production registry in the regression test. Contrib classes compiled into the same core jar are unaffected. ########## spark/src/main/scala/org/apache/comet/rules/CometScanContrib.scala: ########## @@ -0,0 +1,210 @@ +/* + * 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 org.apache.comet.rules + +import java.util.ServiceLoader + +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal + +import org.apache.spark.internal.Logging +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.execution.{FileSourceScanExec, SparkPlan} +import org.apache.spark.sql.execution.datasources.HadoopFsRelation +import org.apache.spark.sql.execution.datasources.v2.BatchScanExec + +import org.apache.comet.CometConf +import org.apache.comet.serde.CometOperatorSerde + +/** + * Format-agnostic hook that lets an optional, out-of-tree contrib (Delta, Lance, ...) claim a + * scan before Comet's built-in scan handling runs. This is the scan-rule counterpart to + * [[org.apache.spark.sql.comet.PlanDataInjector]]: core holds no compile-time reference to any + * contrib and names none of them -- implementations are discovered at runtime via the JDK + * [[ServiceLoader]], so default builds (which ship no service file) see an empty registry and + * zero contrib surface. + * + * A contrib ships: + * - an implementation of this trait (e.g. `contrib/delta/.../DeltaScanRuleContrib`), and + * - a `META-INF/services/org.apache.comet.rules.CometScanContrib` resource naming it. + * + * Both hooks default to `None` so a contrib overrides only the scan kind(s) it handles. + * + * ==Ownership contract== + * + * An implementation '''MUST''' return `None` for any scan it does not own. Registered contribs + * are offered a scan one at a time and the '''first''' claim wins, so a contrib that claims a + * scan belonging to another format does not merely mis-handle that scan -- it hides it from the + * contrib that could have read it, and the outcome depends on ServiceLoader ordering, which is + * not specified. Decide ownership from something definitive about the scan itself (the relation's + * `fileFormat` class, the table's provider, a catalog type) rather than from a heuristic such as + * a path or table-name pattern, which another format may also match. + * + * "Own but cannot handle" is a distinct case from "not mine", and both are expressible: to + * decline a scan it owns, a contrib returns `Some(withFallbackReason(scanExec, "..."))` -- + * claiming the scan and terminating it with a diagnosable reason -- rather than returning `None` + * and letting Comet's built-in handling attempt a format it does not understand. + */ +trait CometScanContrib { + + /** + * V1 (`FileSourceScanExec`) hook. Return `Some(plan)` to claim the scan -- either a transformed + * plan node or an explicit fallback the contrib produced via `withFallbackReason`. Return + * `None` to pass, letting Comet's generic V1 handling proceed; see the ownership contract + * above. A claiming contrib is responsible for its own metadata-column handling (the generic + * guard in [[CometScanRule]] runs only on the pass path). + */ + def tryTransformV1( + plan: SparkPlan, + session: SparkSession, + scanExec: FileSourceScanExec, + relation: HadoopFsRelation): Option[SparkPlan] = None + + /** + * V2 (`BatchScanExec`) hook. Return `Some(plan)` to claim the scan, `None` to pass; see the + * ownership contract above. + */ + def tryTransformV2(scanExec: BatchScanExec): Option[SparkPlan] = None +} + +object CometScanContrib extends Logging { + + // Discovered contrib scan handlers. Mirrors `PlanDataInjector.injectors`: the standard JDK + // ServiceLoader forces the provider iterator here so a misbuilt contrib jar (malformed service + // file, or a listed provider that can't be instantiated) surfaces as a warning rather than + // taking down the planner. Default builds carry no service file, so this is empty and there is + // no contrib surface at runtime. There are no built-in (in-core) contribs today; the format + // built-ins (Parquet/Iceberg) keep their existing paths in CometScanRule. + private[comet] lazy val contribs: Seq[CometScanContrib] = + try { + ServiceLoader.load(classOf[CometScanContrib], getClass.getClassLoader).asScala.toSeq Review Comment: [P2] Materialize service discovery inside the exception handler On supported Scala 2.12, `.asScala.toSeq` produces a lazy `Stream`. With a valid declining provider followed by a missing or uninstantiable provider, this `try` returns before the bad entry is visited. `firstClaimFrom` later forces it outside the discovery/per-hook catches, so `ServiceConfigurationError` fails an otherwise runnable scan. I reproduced this with the unchanged class on Scala 2.12.18; replacing `toSeq` with `toList` makes the existing catch work. Please use a strict collection here and in the new `CometConfigProvider` registry, and test a malformed later service entry. Conflict-detection mode can expose the same failure even when the first provider claims the scan. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
