feat: add native Delta Lake scan contrib module (page/row-group pruning) - #5365
feat: add native Delta Lake scan contrib module (page/row-group pruning)#5365dwsmith1983 wants to merge 12 commits into
Conversation
|
Update: pushed two follow-up commits extending the scan's pruning and object-store behavior.
|
…ng + in-scan DVs) Adds contrib/delta behind a -Pdelta profile: delta-spark keeps all planning (log replay, snapshot, partition pruning); Comet claims the DSv1 scan via a new CometScanRuleExtension SPI and reads data through the same native DataFusion parquet path as CometNativeScanExec, inheriting row-group stats pruning, page-index pruning, and filter pushdown. Deletion vectors are decoded natively into per-file ParquetAccessPlans that intersect with page-index pruning, so deleted rows are skipped in-scan; DV blob and footer fetches run concurrently and footers go through the scan's shared FileMetadataCache (no extra metadata round-trips for DV files). Scalar-subquery data filters are resolved at execution time and pushed to the native reader — a capability stock Spark 3.x lacks entirely. Column mapping name mode, DPP, time travel, checkpoints, schema evolution, and INT96 covered. The `delta` native feature ships in the default set: runtime stays double-gated (contrib jar via ServiceLoader + conf), so it is inert for non-Delta users; roaring is the only net-new default dependency. CI runs the contrib suites on Spark 3.5/4.0/4.1, byte-compiles the dev scripts on Python 3.11-3.14, and keeps the feature-off error path tested. Verified: 39-test differential suite green on Spark 3.5/4.0/4.1; Delta's own suites with Comet injected fully green, 1156/1156 (DeletionVectors, TimeTravel, ColumnMapping, DeleteSQL, UpdateSQL, MergeIntoSQL — one test-only harness patch maps the Comet scan node to its originalPlan for Delta's ScanReportHelper). Local bench (20M rows, release): 1.35x vs stock at 9.4% of bytes on literal bounds; 3.35x on subquery bounds (stock scans 100%, contrib 5%). Supersedes apache#4366 (delta-kernel-rs contrib) and apache#4669 (plain-table native scan), deliberately building on both. Co-authored-by: Scott Schenkein <schenksj@yahoo.com> Co-authored-by: Aditya Vaish <adivaish@microsoft.com>
888e4a7 to
7fd81aa
Compare
|
HI @andygrove, Can you review this as it adds Delta functionality? |
Two deficiencies surfaced by 'make release PROFILES="-Pspark-3.5,delta"' (the path a vendor uses to package the contrib), neither visible in CI: - CometScanRuleExtension scaladoc violated spotless line-wrapping (mvn spotless:apply; CI's contrib job builds deps with -Dspotless.check.skip=true so it never checked this file). - BanDuplicateClasses fired for five comet-common exception classes: the comet-spark shaded jar bundles comet-common, so inside a single reactor the contrib sees both artifacts (the dependency-reduced pom only shields repository consumers), and shade's ASM pass renumbers some constant pools so ignoreWhenIdentical cannot collapse them. Ignore the org.apache.comet.* overlap for that pair explicitly. Verified: ./mvnw install -Prelease -DskipTests -Pspark-3.5,delta now completes with no skip flags.
isDeltaScan used classOf[DeltaParquetFileFormat], which resolves the Delta class on the FIRST V1 scan the extension inspects. With the contrib jar deployed but delta-spark not on the classpath, that raises NoClassDefFoundError inside CometScanRule and takes down every parquet scan in the session - the exact opposite of the module's inert-by- default contract (found live: a parquet-only benchmark arm with the contrib jar staged died on its first query). Compare the class NAME instead: no Delta type is touched until the name matches, and a match proves delta-spark is present (the instance exists), so every Delta reference past this gate stays safe. Exact string equality preserves the previous exact-class semantics. Verified: compiled bytecode of isDeltaScan carries only a string constant (javap: getName + ldc + String.equals - no Delta constant- pool entry); compile, spotless and scalastyle green.
|
Hi @dwsmith1983 Thanks for putting this together! We are also actively looking at Delta support for Comet, and it'd be great if we can collaborate on this effort! Since #4952 is already approved and close to landing, what do you think about using it as the shared foundation for this work? Ideally, the same In addition, would it also make sense to land this in smaller pieces, for easier review and iterating? For example:
Starting to support this in Spark 4 & Delta 4 would be a useful first milestone. Curious how you see the relationship between the two PRs and whether that direction makes sense to you. Thanks. |
|
Hi @sunchao, On #4952 as the foundation: we already share more than it might look like. This PR builds on part 1 of that same breakup (#4700's CometScanWithPlanData / PlanDataInjector SPI) and keeps #4366's contrib shape, decline-gate philosophy, and test catalog, with co-authored-by credit to both earlier efforts. The remaining overlap is contrib infrastructure, and I'm glad to reconcile it once #4952 lands: adopt its contrib-delta profile and feature naming, the per-Spark delta.version matrix, the verify-gate script, and unify the proto slot (this PR is at 119, #4952 at 118). For the claim hook I'd suggest the generic CometScanRuleExtension SPI from this PR, since it keeps core free of Delta-specific code and the kernel path can register through it the same way. I do see the two read paths as different layers rather than one thing to converge on. By the time CometScanRule sees the scan, delta-spark has already done log replay, time travel, and partition pruning, so this path reuses Comet's existing native parquet scan and gets row-group pruning, page-index pruning, and filter pushdown for free. DVs become ParquetAccessPlans that DataFusion intersects with page-index pruning, so DV skips and page skips compose in one scan. As far as I know no vectorized Delta reader does all of that today, including kernel's, which has no page-index pruning. I'd want convergence to keep this as the default read path, with the kernel path covering what JVM planning can't reach (DSv2, non-Spark frontends, likely CDF and row tracking). On splitting: I'd push back on slicing by feature, for two reasons. First, the features aren't independent. Several decline gates only exist because DVs, column mapping, and Delta's own suites ran together. For example, Delta's findTouchedFiles scan looks like a plain read, and if a basic-reads slice claims it, DELETE silently rewrites files instead of writing DVs. Second, the proof is holistic: this branch runs Delta's own suites at 1156/1156 and the contrib suites at 39/39 on Spark 3.5, 4.0, and 4.1. Feature slices would decline most tables and couldn't run that meaningfully. What I can do is split along review surfaces instead: core SPI additions, native DV decode with its unit tests, the contrib module and read path, and the regression harness and CI, keeping the read path itself (DVs, column mapping, gates) as one reviewable unit. If it lands whole, Comet ships the only vectorized Delta reader with complete skipping. The Spark 4 milestone is already met, the suites are green on 4.0 and 4.1 today. Row tracking and CDF are out of scope here and seem like a natural place for the kernel work to lead. Happy to set up a chat with you and @schenksj to work out the details. |
|
Thanks @dwsmith1983 Your proposed split by review surface sounds reasonable. I agree that the reader, its safety gates, and the essential DML/fallback tests should stay together. Thanks also for being open to aligning with #4952 once it lands. We can leave row tracking and CDF for later discussions rather than expand this PR’s scope. The main additional point I’d like us to settle is keeping experimental Delta support explicitly opt-in. |
Yeah, agreed on explicit opt-in. It's mostly already set up that way. All the Delta code lives in a separate comet-contrib-delta jar that never gets bundled into comet-spark, so a stock Comet install has no Delta surface at all. If we publish that jar with releases, trying it out is just --packages and a conf, nobody has to build from source. Right now the conf defaults to on when the jar is present though, so I'll flip spark.comet.scan.delta.enabled to default false to make the opt-in explicit. The one spot where I'd differ from #4952's gate is the native binary. The Delta bits in libcomet are tiny (DV decoding plus a hand-off to the existing parquet scan, no delta-kernel dependency) and can't be reached without the jar and the conf. I'd rather keep them in the default build than make people compile their own native binary to try an experimental feature. Sound reasonable? |
Which issue does this PR close?
Part of #174 (Explore integration with Delta Lake). It does not close #174, that issue also tracks writes, CDF, and broader integration; this PR delivers the native read path.
Supersedes two earlier efforts, and deliberately builds on both (both given co-authored by since ideas were learned and borrowed):
Rationale for this change
Comet currently falls back to Spark's reader for all Delta tables (
isFileFormatSupportedrequires exactParquetFileFormat, andDeltaParquetFileFormatis a subclass). That forfeits native execution and all of Comet's parquet pruning on one of the most common table formats.Key observation: delta-spark has already done log replay, snapshot resolution, time travel, and partition pruning by the time
CometScanRulesees theFileSourceScanExec. So no Delta planning is needed on the native side at all, the scan can route through the exact same DataFusionParquetSourcepath asCometNativeScanExec, inheriting row-group stats pruning, page-index pruning (#5142), and filter pushdown (#4722) for free. The only genuinely Delta-specific native code is deletion-vector decoding: DV bitmaps are decoded into per-fileParquetAccessPlans, which DataFusion intersects with page-index pruning, so deleted rows are skipped in-scan and DV skips compose with page skips.Local benchmark (20M rows, selective predicate): 1.44x faster than stock Spark 9.4% of bytes read; DV tables at time parity with in-scan DV application.
What changes are included in this PR?
contrib/delta/new Maven module behind a-Pdeltaprofile: scan rule, decline gates, serde,CometDeltaNativeScanExec(split-mode partition serialization, DPP via derived scan helper), ServiceLoader registrations, differential test suites, Delta own-suite regression harness, benchmark script.CometScanRuleExtensionSPI + ServiceLoader hook at the top oftransformV1Scan;CometNativeScan.convert body extracted into reusablebuildNativeScanCommon`.deltacargo feature:DeltaScanproto + planner arm delegati the shared parquet scan builder;delta_dv.rsfor DV blob unframing (CRC verified), roaring decode (portable + native magic), and access-plan construction.input_file_name().How are these changes tested?
page_index_rows_pruned > 0,row_groups_pruned_statistics > 0), not benchmark notes.useMetadataRowIndexmodes.1 TB benchmark (real S3, this branch)
Independent run on TPC-DS-derived
store_salesat 1 TB (2.75 B rows), hilbert-clustered, on S3 (ap-southeast-1). The same physical parquet files are read through three paths, raw parquet (recursive glob), the Delta table, and an Iceberg table registered over the identical files viaadd_files, so the table-format scan path is isolated on byte-identical data. Spark 3.5.6 standalone, 252 executor cores (Graviton m7g), this branch at7fd81aa9built with-Pspark-3.5,delta. Four selective query families x 20 queries each;rows_scannedsummed from executed-plan scan metrics; warm = median of 3 runs. Control = same session with Comet disabled and the vectorized reader off (parquet-mr row reader, which prunes pages honestly).Fraction of rows decoded (Comet-native / control), and Comet bytes read per query:
Warm query times, Comet-native vs the row-reader control:
Takeaways:
ParquetSourcepath, inherit row-group + page-index pruning) holds at 1 TB on real S3.CometIcebergNativeScanExecreports post-filter-pushdown rows inoutput_rows, so its row fractions are not comparable to the other arms,bytes_scannedis the honest cross-arm metric (Iceberg reads ~2-3x the bytes of the parquet/Delta arms here).Deployment note for anyone staging this on a standalone cluster: core discovers the contrib via
ServiceLoaderon its own classloader, and the contrib links Delta types from that same loader — so when comet-spark ridesspark.{driver,executor}.extraClassPath(required forCometShuffleManager), the contrib jar and delta-spark/delta-storage must be real files on that same classpath;--packagesjars land in Spark's child loader where neither lookup can see them.Co-authored-by: Scott Schenkein schenksj@yahoo.com
Co-authored-by: Aditya Vaish adivaish@microsoft.com