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

   ## Summary
   
   Comet's native `core` crate has grown into a ~31k-line monolith that fuses 
five distinct concerns (planner, readers, writers, storage, and the JNI entry 
layer). This makes rebuilds slow, keeps a single 7141-line `planner.rs` at the 
center of the codebase, and blurs the boundaries that the JVM side already 
expresses cleanly through packages. This issue proposes reorganizing both the 
Rust and JVM sides around one shared vocabulary of seven axes, with a strictly 
downward dependency flow modeled on Apache Spark's module layout.
   
   This is a structural refactor. It changes no behavior.
   
   ## Motivation
   
   1. **`core` is a monolith.** Five concerns live in one crate. Editing the 
Parquet reader recompiles the planner. The planner itself is one 7141-line file 
doing four jobs (expression building, operator building, file-scan parsing, 
shuffle-writer config).
   2. **Concerns are named on the JVM but not on the native side.** The JVM 
already separates `rules` (planner), `serde` (expressions), `parquet` 
(readers), objectstore/cloud (storage), shuffle (writers), and `Native` (jni) 
by package. The native side does not, so the same concept has no shared name 
across the language boundary.
   3. **Three layering inversions block clean extraction.**
      - Core error types (`CometError`, `ExecutionError`) live in `jni-bridge`, 
so all error handling transitively depends on JNI.
      - `SparkError` and `QueryContext` live in the `spark-expr` crate, making 
it a de-facto foundation crate that lower layers reach up into.
      - Expressions have two homes: the `spark-expr` crate and 
`core/execution/expressions/`.
   4. **A decomposition is already in flight.** `ExpressionRegistry` and 
`OperatorRegistry` (with `macros.rs`) let expressions and operators 
self-register instead of living in a giant match. Only `projection` and 
`arithmetic` are migrated. This registry is the mechanism to finish the split 
without the planner needing a hard dependency on every operator.
   
   ## Current native structure
   
   ```mermaid
   graph TD
     core["comet (core): ~31k LOC<br/>planner + readers + writers + storage + 
jni-entry + runtime"]
     core --> proto[comet-proto]
     core --> common[comet-common]
     core --> jnibridge[comet-jni-bridge]
     core --> sparkexpr[spark-expr]
     core --> shuffle[comet-shuffle]
     sparkexpr --> jnibridge
     sparkexpr -. "error/context types live here (inversion)" .-> common
     jnibridge -. "error types live here (inversion)" .-> common
   ```
   
   ## Goals
   
   - One shared axis vocabulary across native and JVM.
   - A strictly acyclic, downward dependency flow.
   - Shrink `core` to a thin JNI and orchestration shell.
   - Finish the registry-based decomposition already started.
   
   ## Non-goals
   
   - No behavior or wire-format changes.
   - No broad split of the JVM `spark` module into Maven modules. The 
multi-version source layout (`spark-3.4` through `spark-4.2` plus shims) makes 
that too costly. Package discipline is used instead.
   
   ## Shared vocabulary: one name per axis, on both sides
   
   | Axis | Native crate (target) | JVM package (target) |
   |---|---|---|
   | proto | `comet-proto` (exists) | generated into `o.a.comet.proto` |
   | expressions | `comet-expr` (`spark-expr` plus 
`core/execution/expressions`) | `o.a.comet.serde` and `o.a.comet.expressions` |
   | planner | `comet-planner` (new, from `planner.rs`) | `o.a.comet.rules` and 
`o.a.spark.sql.comet` execs |
   | readers | `comet-readers` (new) | `o.a.spark.sql.comet` scan execs and 
`o.a.comet.parquet` |
   | writers | `comet-writers` and `comet-shuffle` (shuffle exists) | 
`o.a.spark.sql.comet.execution.shuffle` and write execs |
   | storage | `comet-storage` (new) | `o.a.comet.objectstore` and 
`o.a.comet.cloud.s3` |
   | jni | `comet` shell and `comet-jni-bridge` (exists) | 
`o.a.comet.{Native,NativeBase}` and `vector.NativeUtil` |
   
   ## Target native crate graph
   
   Arrows mean "depends on". Foundation sits at the bottom, the JNI shell at 
the top. `core` shrinks to the shell.
   
   ```mermaid
   graph TD
     shell["comet (cdylib)<br/>JNI entry points, memory_pools runtime"]
     planner["comet-planner<br/>PhysicalPlanner, serde, spark_plan, registries"]
     readers["comet-readers<br/>parquet / csv / iceberg scan, columnar_to_row"]
     writers["comet-writers<br/>parquet write"]
     shuffle["comet-shuffle<br/>shuffle write, partitioners"]
     operators["comet-operators<br/>expand, explode, projection, sample, 
rank_limit, copy"]
     expr["comet-expr<br/>all Spark-compatible expressions"]
     storage["comet-storage<br/>object_store build, S3/Azure, credential glue"]
     jnibridge["comet-jni-bridge<br/>Rust to JVM callbacks"]
     common["comet-common<br/>errors, schema, arrow-convert, query_context, 
tracing, metrics"]
     proto["comet-proto<br/>generated protobuf"]
   
     shell --> planner
     shell --> jnibridge
     planner --> proto
     planner --> expr
     planner --> readers
     planner --> writers
     planner --> shuffle
     planner --> operators
     planner --> storage
     readers --> expr
     readers --> storage
     writers --> storage
     shuffle --> expr
     operators --> expr
     expr --> jnibridge
     storage --> jnibridge
     jnibridge --> common
     expr --> common
     readers --> common
     writers --> common
     storage --> common
     operators --> common
   ```
   
   ## Target JVM package dependency direction
   
   No Maven split. Enforce the direction below with one ArchUnit or Scalafix 
dependency test. Grow the near-empty `common` module into a contract tier (like 
Spark's `sql/api`) that holds the `Native` signatures, `DataTypeSupport`, and 
the exception classes.
   
   ```mermaid
   graph TD
     planner["rules + Comet*Exec<br/>PLANNER"]
     serde["serde + expressions<br/>EXPRESSIONS"]
     readers["scan execs<br/>READERS"]
     writers["shuffle + write execs<br/>WRITERS"]
     storage["objectstore + cloud.s3<br/>STORAGE"]
     jni["Native / NativeBase / NativeUtil<br/>JNI"]
     proto["generated protobuf<br/>PROTO"]
     common["common: CometConf, Native contract, exceptions<br/>CONTRACT TIER"]
   
     planner --> serde
     planner --> readers
     planner --> writers
     serde --> proto
     readers --> storage
     writers --> storage
     readers --> jni
     writers --> jni
     storage --> jni
     serde --> common
     planner --> common
     jni --> common
   ```
   
   ## Cross-cutting residuals (need an explicit home)
   
   - Native: `metrics`, `tracing`, `utils` move into `comet-common`. 
`memory_pools` stays in the shell because it is executor runtime bound to the 
task-memory-manager callback.
   - JVM: the Arrow `vector` layer, the `codegen` batch-kernel path, Arrow and 
Python interop, metrics, and testing infra. The largest is the version-shim 
tree, which is the strongest argument against a broad Maven split. Give it a 
`shims` bucket per axis rather than a module.
   
   ## Migration plan
   
   Each phase ships with a green build and can merge independently. Do every 
file relocation as a pure `git mv` commit with no logic change so blame follows 
and review stays trivial.
   
   - [ ] **Phase 1: fix the layering inversions.** Move `CometError` and 
`ExecutionError` from `jni-bridge` into `comet-common`. Move `SparkError` and 
`QueryContext` from `spark-expr` into `comet-common`.
   - [ ] **Phase 2: extract `comet-storage`.** Move 
`parquet/objectstore/{s3,azure}` and `cloud/s3`.
   - [ ] **Phase 3: extract `comet-readers` and `comet-writers`.** Split the 
`operators/` grab-bag along the read/write line. Move the pure transforms into 
`comet-operators`.
   - [ ] **Phase 4: consolidate expressions.** Fold 
`core/execution/expressions/` into `comet-expr`.
   - [ ] **Phase 5: extract `comet-planner`.** Split `planner.rs` by concern 
(expression building, operator building, file-scan and partition parsing, 
shuffle-writer config) as it moves. Finish the `ExpressionRegistry` and 
`OperatorRegistry` migration.
   - [ ] **Phase 6: reduce `core` to the JNI shell.**
   - [ ] **Phase 7 (JVM): adopt the axis vocabulary and add the 
dependency-direction enforcement test.** Consolidate the two known overlaps 
(`serde/operator/*` serializes both scans and sinks, scan execs double as 
operator wrappers). Extract `comet-storage` and proto as modules only if the 
boundary holds.
   
   ## Risks and tradeoffs
   
   - More Rust crates means a larger build graph. In exchange, incremental 
rebuilds shrink and the 7141-line file problem disappears. Net win for a 
31k-line monolith.
   - Cyclic dependencies are a real risk. Keep dependencies one-way (planner to 
building blocks), which the existing registry supports.
   - A broad JVM Maven split fights the version-shim structure and is out of 
scope.
   - Large moves churn open PRs. The `git mv`-only commit rule keeps this 
manageable.
   - Note for consistency: the JVM side generates protobuf at build time, while 
the native side commits `native/proto/src/generated/*.rs`. Spark commits 
neither. Reconciling this is optional follow-up.
   


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