parthchandra opened a new issue, #5411:
URL: https://github.com/apache/datafusion-comet/issues/5411

   # Proposal: converge the two Delta read paths into one plugin (clean 
architecture + performance)
   
   ## TL;DR
   
   We currently have two paths, under development, for reading Delta files.  
   
   - **native-parquet-reads** -- delta-spark plans, DataFusion's 
`ParquetSource` reads the bytes.
     This is the path in **PR #5365**.
   - **kernel-read** -- delta-kernel-rs reads the bytes and does the transforms 
+ CDF.
     This is the path in **PR #4366**.
   
   These are not competitors at the architecture level -- they are two *read 
strategies* that can
   live behind **one** Delta plugin. The native-parquet-reads path's last 
rebase already renamed its
   proto to `DeltaSpark*` and moved its native handler to 
`planner/delta_spark_scan.rs` as a "sibling
   of the kernel path's `delta_scan.rs`, dispatched by type_url." That is 
exactly what is needed for
   both to coexist. This proposal tries to make that explicit:
   
   - **One plugin, two read strategies**, selected per-scan.
   - **native-parquet-reads is the default** -> page-index/row-group pruning + 
filter pushdown +
     in-scan DV. This is the hot path.
   - **kernel-read is the capability fallback** -> CDF (`readChangeFeed`) and 
any transform the
     native path can't yet express. Declines route *here*, not to vanilla Spark.
   - **Zero default surface** -- both paths gated out of the default 
`libcomet`, claimed through the
     already-merged `CometScanContrib` SPI (#4952).
   
   Result: kernel-read's full feature coverage AND native-parquet-reads' 
performance, with a true
   plugin boundary.
   
   ## Why this works (and the one hard constraint)
   
   The only real divergence is *who reads the parquet bytes*. Page-index 
pruning, row-group pruning,
   and filter pushdown live in DataFusion's parquet reader. Kernel's reader 
does not have them.
   
   - native-parquet-reads' performance is therefore **not** retrofittable onto 
the kernel-read data
     path -- getting it means routing bytes through `ParquetSource`, which *is* 
the native-parquet
     design. So native-parquet-reads must be the performance path.
   - Conversely, most of kernel-read's extra features (column-mapping id mode, 
generated columns,
     row_index) are achievable on the native path with more work. **CDF is the 
exception** -- it wants
     kernel's `TableChanges`; reimplementing it natively is a large, 
error-prone effort. So keep
     kernel-read for CDF.
   
   ## Shared module layout
   
   ```
   contrib/delta/                      (single plugin module, -Pcontrib-delta / 
--features contrib-delta)
     scala/
       DeltaScanContrib.scala          implements CometScanContrib (the ONE 
claim seam)
       strategy/NativeParquetStrategy  native-parquet planning: emit 
ContribScan{type_url=...delta_spark.*}
       strategy/KernelStrategy         kernel-read planning:     emit 
ContribScan{type_url=...delta_kernel.*}
       DeltaCdfScanExec.scala          kernel-read CDF exec
     native/ (comet-contrib-delta crate, linked ONLY under the feature)
       delta_spark_scan.rs             native-parquet handler (calls 
core::build_parquet_scan)
       delta_dv.rs                     native-parquet roaring DV decode -> 
ParquetAccessPlan  (MOVED out of core)
       delta_scan.rs / kernel_scan.rs  kernel-read read path
       dv_reader.rs                    kernel-read DV masking
   ```
   
   Both native handlers register under the merged `ContribScan contrib_scan = 
200` envelope and are
   dispatched by `type_url`. The injector already supports multiple injectors 
per kind.
   
   ## Which scan strategy routes where
   
   The `DeltaScanContrib.tryTransformV1` (and CDF's V2/row-source hook) 
inspects the plan and picks:
   
   | Scan shape | Path | Why |
   |---|---|---|
   | Plain / partitioned read, no column mapping | **native-parquet** | full 
pruning + pushdown |
   | Deletion vectors (inline or on-disk) | **native-parquet** | DV -> 
`ParquetAccessPlan`, intersects page pruning |
   | Column mapping **name mode** (incl. nested) | **native-parquet** | 
SchemaMapper by name |
   | Column mapping **id mode** | **native-parquet** (target) / kernel-read 
(interim) | SchemaMapper by field id; land on native, kernel until then |
   | Time travel, checkpoints, OPTIMIZE'd, schema evolution, INT96, 
special-char paths | **native-parquet** | all inherited from `ParquetSource` |
   | DPP | **native-parquet** | `CometScanWithPlanData` derived-scan helper 
(#4700) |
   | `_metadata.row_index` consumed by plan | **kernel-read** (interim) / 
native-parquet (target) | native row-index emit is doable but not yet wired |
   | Row tracking (`row_id`, `row_commit_version`) | **kernel-read** | needs 
Delta row-tracking metadata logic |
   | **CDF (`readChangeFeed`)** | **kernel-read** | `TableChanges`; native 
reimpl not worth it |
   | Generated-column partition filters | **kernel-read** (interim) / 
native-parquet (target) | doable on native, defer |
   | Encryption, `input_file_name()`, unknown reader features | **Decline** 
(vanilla Spark) | neither path supports |
   
   Rule of thumb: **native-parquet by default; kernel-read only for what native 
can't do; Spark only
   for what neither can do.** Every decline carries a `withFallbackReason` so 
EXPLAIN shows why.
   
   ## Core-surface budget (the clean-plugin part)
   
   The default `libcomet` and core modules must carry **zero** Delta surface. 
Concretely:
   
   1. **Reuse the merged `CometScanContrib`** (#4952). Delete 
native-parquet-reads' parallel
      `CometScanRuleExtension` + its hook + suite. One SPI, not two.
   2. **Ride the merged `ContribScan` type_url envelope.** Drop 
native-parquet-reads' `DeltaSpark*`
      messages from `operator.proto` core; the plugin defines its own messages 
packed into the
      envelope's `value`.
   3. **Move `delta_dv.rs` + `roaring`/`crc32fast` into the contrib crate.** 
Remove `delta` from the
      default cargo feature set. Default build -> no Delta symbols (assert via
      `verify-contrib-delta-gate.sh`, which the kernel-read path already ships).
   4. **Keep native-parquet-reads' `planner.rs` shared-builder extraction, but 
expose it as
      format-neutral** `pub fn build_parquet_scan(common, files, 
Option<Vec<ParquetAccessPlan>>)` --
      no Delta arm, no `#[cfg(feature="delta")]` Delta dispatch inside core. 
The contrib crate's
      handler calls it.
   5. The one *new* core concept -- a **generic per-file row-selection / access 
plan** on the native
      scan -- is justified because it is format-neutral (Iceberg 
positional/equality deletes want the
      same thing). Frame it as "core parquet scan supports row skipping," not 
"core has a Delta hook."
   
   Net core delta after this: the already-merged SPI + one reusable pub builder 
fn + an optional
   generic access-plan field. That matches the kernel-read path's 
zero-default-surface contract while
   preserving native-parquet-reads' full `ParquetSource` performance.
   
   ## Migration steps
   
   **For native-parquet-reads (do first -- it becomes the default path):**
   1. Replace `CometScanRuleExtension` usage with 
`CometScanContrib.tryTransformV1`.
   2. Remove `delta` from default cargo features; move `delta_dv.rs` + deps 
into the contrib crate.
   3. Turn the `planner.rs` Delta arm into a format-neutral 
`build_parquet_scan` and have the contrib
      crate's `delta_spark_scan.rs` call it.
   4. Relocate module to `contrib/delta/` (shared with kernel-read) instead of 
`contrib/delta-spark/`.
   
   **For kernel-read (becomes the fallback layer):**
   5. Land its Rust driver/executor + Scala claim/serde + CDF units as the 
kernel strategy inside the
      same plugin, registered under a distinct `type_url` (`...delta_kernel.*`).
   6. Restrict the kernel-read claim to the shapes in the table above (CDF, row 
tracking, id mode
      interim) -- it no longer claims plain reads (native-parquet owns those).
   
   **Joint:**
   7. `DeltaScanContrib` is the single claim point; it dispatches to the 
native-parquet vs kernel-read
      strategy.
   8. One test battery, run against both strategies for the shapes each owns; 
one CI workflow.
   
   ## Open questions
   
   - Is "generic per-file access plan on the native scan" acceptable core 
surface, or should it be
     produced entirely inside the contrib crate's `build_parquet_scan` call 
(leaving core with only
     the pub builder fn)?
   - Is native CDF a near-term requirement? If **no** then fallback to Spark 
until CDF is implemented.
   - Do the two paths agree on the layout above?
   


-- 
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]

Reply via email to