Repository navigation
Conversation
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Fold description contract crates/einfold-ir/src/fold.rs, crates/einfold-ir/src/lib.rs |
Adds public fold, operand, dimension, fill, and key-equality types. Fold::try_new validates descriptions and exposes fold metadata. |
DataFusion logical fold node crates/einfold-datafusion/src/node.rs |
Adds FoldNode, which validates its inputs and derives its output schema. It implements DataFusion’s logical-node interface and formats the fold for explain output. |
Hash fold and physical planning crates/einfold-datafusion/src/hash.rs, crates/einfold-datafusion/src/exec.rs, crates/einfold-datafusion/src/lib.rs, crates/einfold-datafusion/Cargo.toml, Cargo.toml, .github/workflows/ci.yml, CONTRIBUTING.md |
Adds hash-based fold execution and DataFusion planning for FoldNode. Updates the DataFusion pin to 55.2.0 and the minimum Rust version to 1.94. Adds async-trait and futures dependencies. |
Behavior and SQL-oracle validation crates/einfold-datafusion/tests/*, docs/lessons.md |
Adds tests for DataFusion float-key and NULL-join behavior, plus randomized comparisons of fold results against a SQL oracle. Updates the documented DataFusion float-key behavior. |
Priority:
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant PlanProducer
participant EinFoldQueryPlanner
participant DefaultPhysicalPlanner
participant EinFoldPlanner
participant EinFoldExec
participant FoldHash
PlanProducer->>EinFoldQueryPlanner: Submit logical plan containing FoldNode
EinFoldQueryPlanner->>DefaultPhysicalPlanner: Request physical plan
DefaultPhysicalPlanner->>EinFoldPlanner: Plan FoldNode
EinFoldPlanner->>EinFoldExec: Create operator with fold and inputs
EinFoldExec->>EinFoldExec: Collect input partitions
EinFoldExec->>FoldHash: Compute fold
FoldHash-->>EinFoldExec: Return folded batch
EinFoldExec-->>PlanProducer: Emit result slices
Possibly related issues
- Emit einfold's Fold relation for contraction steps (the interface for einfold's M1) ddx#208: This PR adds the Fold description,
FoldNode, andEinFoldExeccomponents named in the issue; it does not itself add ddx fold emission. - DataFusion 55: dictionary-encoded floats keep -0.0 and 0.0 apart, unlike plain floats — draft for upstream #41: The PR tests DataFusion float-key behavior and normalizes signed zero in the fold hash path.
Possibly related PRs
- xqlsystems/einfold#40: Updates the same DataFusion dependency and Rust minimum version, and describes float and NULL behavior covered by this PR’s tests.
Merge Risk: 
Minimal · up to 53daa
-
This change adds a new fold planning and execution path in the DataFusion adapter, along with a Rust and DataFusion version bump. No concrete merge-blocking problem was found in the supplied evidence. The existing randomized comparison against SQL and the DataFusion behavior tests cover the main semantic risks.
Security architecture risk: 
Moderate · up to 53daa
-
The new execution path validates fold inputs and isolates each execution’s state, but buffers complete inputs and results without operator-level memory accounting. Large folds can also run lengthy computation without yielding. This creates a resource-exhaustion risk for applications processing untrusted data, although no externally exposed deployment was established.
Retained concerns
- Medium · security · inferred: The newly introduced fold operator materializes all inputs and the complete result without accounting for its own allocations through DataFusion’s memory pool. Its synchronous hash phase can perform every matching operand pair and retain every distinct output group without yielding. When callers permit attacker-influenced operand data or fold shapes, this can exhaust the host process or monopolize an executor worker despite output batch sizing; dropping the stream cannot cooperatively interrupt a hash phase already running. The planner is opt-in, and deployed attacker reachability remains unestablished.
Security review details
-
Security Blast Radius
- inferred — The resource-containment concern affects the process executing an activated fold. Other queries sharing that process or executor worker could be affected by exhaustion or starvation. Whether this reaches multiple tenants or an externally accessible service is unknown.
Security Findings and Attack Paths
- inferred — A caller able to influence operand cardinality or a producer’s fold shape can drive large allocations and pair enumeration. A valid fold with no shared dimensions matches every operand pair; distinct output keys can also retain a group for every pair. These allocations and computation occur before the first output batch, so output batch size is not an execution budget.
Trust Boundaries and Controls
- observed — Logical validation protects positional and type consistency, while explicit planner installation controls activation. Neither control bounds execution resources. The operand row-count guard protects u32 indexing and runs only after input collection; it is not a practical memory or work quota.
Resilience and Maintainability Implications
- observed — TaskContext is forwarded into child collection, so upstream execution retains that integration. After collection completes, however, the fold runs synchronously without a context, cancellation argument, or yield point. The cancellation limitation is established for this hash phase, not for DataFusion’s collection helper.
Hardening Proposals
- proposed — Account for input buffering, encoded keys, groups, and output through memory reservations with controlled failure or spill behavior. Bound pair work and group growth, and make CPU execution cancellation-cooperative rather than relying on output batch size or moving an uninterrupted loop to another thread.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Description check
-
Explanation
The description gives detailed motivation, implementation scope, testing coverage, DataFusion compatibility notes, and out-of-scope work. It does not include a completed "Closes #" issue link or the exact template headings, but it provides the required information in equivalent sections.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 78 functions across 8 files. (5 skipped: 5 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches: 💡 1
-
📝 Generate docstrings: 💡
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
ddx's workspace pins datafusion =55.2.0, and einfold-datafusion's API takes DataFusion's own types, so the two must resolve the same release before ddx can plan einfold's Fold nodes. DataFusion 55.2 needs Rust 1.94, which is also ddx's minimum, so einfold's MSRV and CI's MSRV entry move with it. The workspace comment no longer says ddx links einfold-datafusion: it doesn't yet. That is what M1 adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
DataFusion 55 treats -0.0 and 0.0 as one key when it joins or groups on a DOUBLE, and emits such a group as 0.0; 54.1 kept them apart. einfold's hash path follows the host's key equality, so the change has to be visible. datafusion_behaviors.rs pins it, with NaN keys, the dictionary-encoded case that still keeps the zeros apart, and the two null-equality bugs in join chains (#21, #22), which still reproduce on 55.2. Each test fails when DataFusion changes the behavior. lessons.md now describes 55.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
A fold over a join (a join followed by SUM of a product, grouped by output dimensions) can now be put in a DataFusion plan directly, by a producer that knows it is writing one, such as ddx. einfold-ir describes the fold by its operands, dimensions and outputs, with an optional fill relation whose rows the output has. einfold-datafusion adds the FoldNode logical node, the EinFoldExec operator, and planners. The operator's hash path follows SQL's bag semantics, NULL keys and group existence, and visits rows in an order fixed by their contents, so its bits don't depend on arrival. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
Read each input's partitions concurrently with collect_partitioned, and the inputs themselves concurrently, so that an upstream repartition isn't left buffering every partition but one. Check child and input counts instead of indexing past them, and guard a batch size of zero. The hash path now tests NULL keys and NULL fill values logically, so a dictionary's NULL value counts as NULL, as it does in SQL; the property test gains dictionary keys whose NULLs are dictionary values. It encodes each set of key columns once and keys its tables on the encoded bytes, with no per-row copies, and returns an error, rather than truncating, for an operand of 2^32 rows or more. The property test now scrambles each seed with SplitMix64: `seed | 1` mapped pairs of consecutive seeds onto one case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
DataFusion 55 passes planners a `&dyn Session` and a `PhysicalPlanningContext` rather than a `SessionState`, and asks every ExecutionPlan to list its physical expressions. EinFoldExec names its operands' columns by position and holds none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
DataFusion 55 joins -0.0 to 0.0 and groups them together, emitting the group as 0.0. The hash path compared keys by their bits, as DataFusion 54 did, so it split such a group in two. It now folds -0.0 into 0.0 with DataFusion's own helper before encoding join and group keys, and emits the folded keys. A fill row keeps its own key, as a left join from the fill does, but finds its group by the folded one. The property test caught the change (seed 1592651821). Each of the three parts, removed, makes it fail; a 3,000-case sweep from seed 2600000 passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
🪭 Claude (einfold) Moves einfold from Apache DataFusion 54.1.0 to 55.2.0, the release ddx (an XQL Systems project for automatic differentiation of SQL queries) pins in its root `Cargo.toml`. einfold-datafusion's API takes and returns DataFusion's own types, so ddx can plan einfold's `Fold` nodes only if both resolve the same `datafusion`. What changed: - `datafusion = "=55.2.0"`, and `Cargo.lock` updated to match. The only code change is the test that pins the version. - **MSRV 1.88 → 1.94.** DataFusion 55.2 declares `rust-version = 1.94`, which is also ddx's. `Cargo.toml`, CI's MSRV matrix entry and `CONTRIBUTING.md` move together. - The workspace comment said "ddx links einfold-datafusion". It doesn't yet: nothing in ddx references einfold today. The comment now says it's what ddx's adapter will do. **A behavior change einfold has to follow.** DataFusion 55 treats `-0.0` and `0.0` as one key when it joins or groups on a `DOUBLE`, and emits such a group as `0.0`. 54.1 compared float keys by their bits. #37's property test caught it on the rebase. `crates/einfold-datafusion/tests/datafusion_behaviors.rs` now pins the host behaviors einfold's operator must reproduce, and each test fails when DataFusion changes one: - float keys in `GROUP BY` (one and two key columns, partial aggregates merged across partitions), `COUNT(DISTINCT)` and joins: `-0.0` equals `0.0` and is emitted as `0.0`; NaN equals NaN; a NaN with its sign bit set is another key; - a dictionary-encoded `DOUBLE` still keeps `-0.0` and `0.0` apart, which looks like an oversight upstream (drafted as #41); - #21 and #22, the null-equality bugs in join chains, which still reproduce on 55.2. `lessons.md`'s section on DataFusion's behaviors now describes 55.2. This goes before #37 and #38. Both are rebased onto this branch, and their bases now point here. Checked locally on Rust 1.99: `cargo fmt --check`, `clippy -D warnings`, `cargo test --workspace`, `cargo doc` with `-D warnings`, and `reuse lint`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit - **Compatibility** - The minimum supported Rust version is now 1.94. - Updated the supported DataFusion version to 55.2.0, including its documented behavior for floating-point grouping and joins. - **Documentation** - Updated Rust setup guidance to reflect the new minimum version. - Clarified how signed zeros, NaN values, and dictionary-encoded floating-point values behave during grouping. - **Development** - CI now tests against Rust 1.94, beta, and stable releases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
🪭 Claude (einfold) Closed in favor of the restacked M1, which follows design v17 (#42) and the review of this pull request: #43 (IR), #44 (kernels: hash), #45 (kernels: dense), #46 (DataFusion node and operator), #48 (detection), #49 (gate benchmark). Every fix made here carried over, including the DataFusion 55 float-key change and the |
🪭 Claude (einfold) First pull request of the restacked M1, following design v17 (#42) and the review of #37 and #38. It replaces #37's IR with one whose concepts match the design's vocabulary (§6) and `lessons.md`: - **The aggregate has parts.** `Aggregate` is a list of monoid parts (`Monoid::Sum`) and a final function (`Finish::Part`), the shape of Gray et al.'s aggregates and of Haskell's `foldl` library, with the row value (`RowValue::Product`) apart from it. `SUM` is one part; `AVG` will be `SUM` and `COUNT` with a division; M2's softmax, two scaled-exponential sums. Whether a fold is an einsum is derived (`Fold::is_einsum`). - **Typed indices.** `DimensionId` and `ColumnIndex` are distinct types, so a dimension can't be passed for a column, or the other way round. - **Outputs are apart from dimensions.** An `Output` is a dimension and, with a domain, the domain column it is matched with. A dimension holds only how its join keys compare. Each dimension is shown at most once; a query showing it in two columns copies it in a projection above. - **A fold names no columns.** Its inputs' columns are positions (`ColumnIndex`), and so are its result's: the outputs' keys, then the value. Names are the host's business, as for a Substrait relation, so two folds that compute the same thing are equal whatever a query called their columns. That is what caching plans by the fold's canonical form needs (§8.5). - **An operand may be a mask** (`factor: None`): its rows decide which rows join, and it contributes no factor. Masks come in M2. Nothing executes them yet. - **The fill becomes a `Domain`:** a plain `LEFT JOIN` from a relation, carrying the domain's columns, with an optional `COALESCE`. #37's fill (ddx's name for these rows; the design now says domain too, so as not to clash with Zarr's fill value) also gave NULL where the fill row's own value is NULL. That's ddx's convention, and a projection above the node can express it. Each output names its domain key and equality, `=` or `IS NOT DISTINCT FROM`. Spike S28 found ddx's left joins are NULL-safe. `Fold::try_new` checks the description, so holding a `Fold` is evidence it is consistent. It adds checks that some operand has a factor, that the aggregate's final function names one of its parts, and that outputs have domain keys exactly when the fold has a domain. A fold with no outputs has no row when nothing joins, unlike SQL's ungrouped `SUM`; the module docs say so. `Domain` compares `missing` by its bits, so that it can be `Eq` and `Hash`, which DataFusion's logical nodes require. Tests are hand tests only. The invariants arrive with the kernels, in the next pull request. Stacked next: `einfold-kernels` (EinFold's algorithms over Arrow arrays, no engine), then the DataFusion node and operator, then detection. #37 and #38 will be closed in favor of the stack, with their review history kept. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added support for describing folds over joined data, with dimension matching, operand factors, outputs, and aggregate operations. * Added configurable key equality and output domains, including carried values and missing-value handling. * Folds can report whether they represent an einsum. * **Bug Fixes** * Invalid fold descriptions are rejected when dimensions, outputs, aggregate settings, or domain keys fail validation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
🪭 Claude (einfold) Second pull request of the restacked M1 (design v17, #42), stacked on #43. It adds `einfold-kernels`, a crate of EinFold's algorithms over Arrow arrays with no query-engine dependency. An engine that keeps its data as Arrow arrays can run them: DataFusion now, and a DuckDB extension such as duckdb-zarr later (#39). The first algorithm is the hash path, ported from #37 to the new IR. **What's here** - `schema(fold, operands, domain)`: the result's schema, with the type checks #37's `FoldNode` made. The checks now live with the kernels, so every engine gets them. Its columns are named by position (`"0"`, `"1"`, …), since a fold names none; the kernels read only the schema's types, so a caller names the columns as it likes. - `fold(fold, a, b, domain, schema, host)`: Gustavson's sparse product ("Two Fast Algorithms for Sparse Matrices", 1978), generalized to folds. It follows SQL's bag semantics, group existence (a group reached only by NULL products exists, with a NULL sum), NULL keys per dimension, and a fixed visiting order, so results don't depend on how rows arrive. - **The domain is a plain left join.** Each domain row gets the group its keys match, each key under its own `=` or `IS NOT DISTINCT FROM`, then `COALESCE(value, missing)` if a default is given. #37's ddx-specific "NULL where the fill's value is NULL" is gone. A projection above the node can express it. - **`Host`** carries what SQL leaves to the engine. For now that's `FloatKeys`: whether `-0.0` and `0.0` are one key. DataFusion 55 says yes, except for dictionary-encoded floats, which einfold#41 drafts for upstream. About 380 non-test lines. **Invariants tested** (`tests/kernels_vs_sql.rs`, ported from #37 and #38). On random folds of one to four dimensions, with `BIGINT`, `VARCHAR`, `DOUBLE` and dictionary keys, NULL keys, duplicate tuples, missing rows, and factors that are NULL, NaN, ±inf, `-0.0` or ill-conditioned, half of them with a domain: 1. the same rows as the SQL the fold stands for, run by DataFusion, the domain as a `LEFT JOIN` with `COALESCE`; 2. the same values within float summation's bound, and exactly SQL's NULLs, NaNs, infinities and sign of zero; 3. the same bits when the rows arrive in another order and other batches. The oracle writes `IS NOT DISTINCT FROM` as `=` on `(key IS NULL, COALESCE(key, sentinel))`, which avoids mixing both kinds of equality in one join chain (#21). <!-- - [x] 400 default cases, and a 3,000-case sweep from seed 5900000: all pass - [x] mutations, each caught: a domain NULL key under `=` matching; dropping COALESCE; bitwise float keys; existence tied to a nonzero sum; folding the domain's carried keys - [x] fmt, clippy -D warnings, doc -D warnings, reuse lint --> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added a fold representation for describing joins, outputs, and aggregation, with validation for invalid configurations. * Added a hash-based kernel for folding two operands, including support for optional domains, NULL handling, and configurable float-key comparison. * Added APIs for deriving output schemas and computing fold results. * **Tests** * Added broad comparisons against SQL results, covering varied keys, NULLs, batching, and numeric edge cases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…els (#46) 🪭 Claude (einfold) Fourth pull request of the restacked M1 (design v17, #42), stacked on #45. It replaces #37's and #38's DataFusion code with a thin layer over `einfold-kernels`, and fixes the operator problems the review of #37 and #38 found. **What's here** - `FoldNode`: a fold in a logical plan. Its inputs are the two operands, then the domain, and its schema is `einfold_kernels::schema`'s. - `EinFoldExec`: runs the kernels, with `HOST` (DataFusion 55's key equality, pinned by `tests/datafusion_behaviors.rs`). `EinFoldPlanner` and `EinFoldQueryPlanner` plan the node. **Behaving like a DataFusion operator** (the review's findings): - **Memory:** it reserves its inputs' memory from the session's memory pool, then the concatenated operands' and the output's, releasing the parts as they're concatenated. It fails with DataFusion's "Resources exhausted" when the pool can't give it the memory, rather than overrunning. - **Off the async runtime:** the kernels run in a blocking task (`SpawnedTask::spawn_blocking`). Before, the dense kernel's scoped threads blocked a Tokio worker. - **Metrics:** `dense_folds`, `hash_folds`, `elapsed_compute` and `output_rows`. - **Still to come:** streaming operands into the dense arrays as they arrive. The operator still holds its inputs whole, now counted against the pool. **Speed.** `examples/matmul_gradient.rs` runs ddx's two `matmul` gradients as a fold with a domain, plus the `CASE` that gives NULL where the domain's value is NULL, against the SQL ddx writes. Best of 5, 12 threads, i7-8700, DataFusion 55.2: | n | gradient | SQL | `Fold` | speedup | |---:|---|---:|---:|---:| | 50,000 | Ā = Ȳ·Wᵀ | 86–88 ms | 10.7–11.0 ms | 8× | | 50,000 | W̄ = Aᵀ·Ȳ | 27 ms | 14.3 ms | 2× | Run back to back, #38's operator took 9.6–10.0 ms and 13.2–14.8 ms on the same shapes. The extra millisecond buys the reservation, the blocking task, and the `CASE`, which is now a projection rather than part of the fold. **Tests.** The kernels' property test (#44, #45) carries the semantics. `tests/operator.rs` adds hand tests of what the operator does: - ddx's gradient through `FoldNode` plus the projection equals ddx's SQL exactly (integer values, so every sum is exact), with 1, 3 or 5 input partitions, 1, 2 or 6 threads, and batch sizes of 5, 64 and 8,192; - `EXPLAIN ANALYZE` shows the fold and `dense_folds=1`; - a 4 KiB memory pool fails cleanly. Writing that SQL turned up a parser quirk: `a IS NOT DISTINCT FROM b AND c` parses as `a IS NOT DISTINCT FROM (b AND c)`, where PostgreSQL binds `IS DISTINCT FROM` tighter than `AND`. The test parenthesizes it. I'll draft it for upstream separately. About 330 non-test lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added support for planning and running fold operations in DataFusion, including folds with an optional domain. * Fold execution combines partitioned inputs and produces results in batches, with memory tracking and execution metrics. * Added an example comparing fold-based matrix gradient calculations with equivalent SQL queries across configurable input sizes. * Fold operations are available through DataFusion’s query planner and can be inspected in query plans using einsum-style notation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
🧭 Claude, continued by 🪭 Claude (einfold)
Stacked on #40 (DataFusion 55.2.0). First pull request of milestone M1, explicit folds for ddx (design §16). ddx is an XQL Systems project for automatic differentiation of SQL queries. M1 lets a plan producer like ddx emit a fold over a join directly, rather than SQL that einfold has to recognize. This pull request adds the node and a correct, general executor. The fast dense path comes next.
What a fold is here. A sum of products over a join of two operands, grouped by output dimensions:
Each dimension's join compares with
=orIS NOT DISTINCT FROM, as the producer says. A fold may carry a fill: a third relation whose rows the output has, each getting its group's sum, 0 where nothing reached the group, and NULL where the fill row's value is NULL. This is ddx's gradient step, which left-joins every input row to its gradient (dense_gradientin ddx-ad).What changed.
einfold-ir:Fold,Operand,Dimension,KeyEquality,Fill.Fold::try_newchecks that the description is consistent, so holding aFoldis evidence that it is.einfold-datafusion:FoldNode, a logical node, checks the inputs' columns and types, and derives the output schema.EinFoldExecwith the hash path: Gustavson's row-by-row sparse multiply, generalized to folds, so join rows never exist. It follows SQL's bag semantics (duplicates count), group existence (a group reached only by NULL products exists, and its sum is NULL), NULL keys per dimension (judged logically, so a dictionary's NULL value is NULL), and DataFusion's key equality: bitwise, except that-0.0and0.0are one key, emitted as0.0. DataFusion 55 made that change, and Pin DataFusion 55.2.0, in lockstep with ddx #40 pins it.EinFoldPlannerandEinFoldQueryPlanner, which plan the node.EinFoldExecreads its inputs, and every partition of each, concurrently, so an upstream repartition never buffers all its partitions but one.Invariants tested (
tests/fold_vs_sql.rs, a property test). 400 random folds: one to four dimensions withBIGINT,VARCHAR,DOUBLE(including-0.0beside0.0, and NaN) or dictionary-encodedVARCHARkeys, the last with NULLs stored as a NULL dictionary value rather than a NULL index; each dimension in one or both operands; random outputs; a fill half the time. Tables have NULL keys, duplicate key tuples, missing rows, and factors that are NULL, NaN, ±inf,-0.0or ill-conditioned (±1e16 beside small values), split across random partitions and batches. Each case checks:2·(n−1)·u·Σ|terms|, and exactly SQL's NULLs, NaNs, infinities and sign of zero;The oracle compares
IS NOT DISTINCT FROMkeys as the pair(key IS NULL, COALESCE(key, sentinel))with=. DataFusion's hash join has one NULL-equality setting per join, and mixing both kinds in one join chain is bug #21.Rebased onto DataFusion 55.2.0 (#40). Two commits on top:
&dyn Sessionand aPhysicalPlanningContext, andExecutionPlanlists its physical expressions (EinFoldExechas none).GROUP BYon aDOUBLEinto-0.0and0.0groups, which DataFusion now merges. The hash path now folds-0.0into0.0with DataFusion's ownnormalize_float_zerobefore encoding join and group keys, and emits the folded keys. A fill row keeps its own key, as a left join from the fill does, but finds its group by the folded one. Removing any of the three parts makes the property test fail.Not in this pull request (stacked next):
EinFoldExeca full DataFusion operator: reserving memory from the memory pool, using every core, and reporting metrics. It collects its inputs whole and runs on one thread for now;FoldSubstrait relation, and ddx emitting it.Size. About 615 lines of non-test code, not counting comments; over AGENTS.md's 400. If you'd rather, I can split the IR into its own pull request first.
🤖 Generated with Claude Code
https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
Summary by CodeRabbit
New Features
Compatibility