Skip to content

doc: design for columnar dataflow edge migration#37744

Draft
antiguru wants to merge 17 commits into
MaterializeInc:mainfrom
antiguru:moritz/columns-migration-spec
Draft

doc: design for columnar dataflow edge migration#37744
antiguru wants to merge 17 commits into
MaterializeInc:mainfrom
antiguru:moritz/columns-migration-spec

Conversation

@antiguru

Copy link
Copy Markdown
Member

Plan to migrate compute dataflow edges from VecCollection to columnar Column batches, following the CollectionEdge scaffolding in #36507.

The doc decomposes the migration into a dependency DAG of fine-grained, independently landable changes, one per stacked PR. Key decisions: single code path with no feature flag, the CollectionEdge enum stays as the representation carrier until a final compiler-enforced teardown deletes the Vec arm, and CollectionExt is left as-is rather than generalized.

Design only, no code changes.

🤖 Generated with Claude Code

antiguru and others added 3 commits July 20, 2026 11:09
Plan to migrate compute dataflow edges from VecCollection to columnar
Column batches, as a dependency DAG of fine-grained stacked PRs. Follows
up the CollectionEdge scaffolding in MaterializeInc#36507.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Producer-flip invariant (producers flip only after all consumers are
native), which removes the seam-heavy intermediate state and justifies
no feature flag. Scope the compiler-enforced completeness claim to
producers and add a whole-plan differential test as the consumer net.
Add the import-boundary producers (P9), preserve FlatMap fueling (C2),
expand LetRec scope and MVP spikes for the two unknowns (C7/C8), and
note introspection golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C7 (LetRec) and C8 (Union temporal-bucket) are sanctioned decode points
for now; the blocker for native C7 is the ResultsIn/Negate/leave_dynamic
feedback machinery, not branch_when. P7 (join output) confirmed native,
builder swaps only. C7/C8 no longer depend on F1. Gate passed: nothing
infeasible, T2 can complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
antiguru and others added 14 commits July 20, 2026 15:22
Fork PRs cannot stack across repos and gh-stack is strictly linear, so the
DAG is flattened into a single topological chain (C1..T3) managed with
gh-stack, each branch based on the previous. Replaces the multi-lane layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An adversarial trace of delta_join.rs confirmed delta join reads inputs only
via pre-built arrangements (arrangement(), and build_update_stream walks an
Arranged trace), never an unarranged CollectionEdge. The edge decode for its
inputs lives in arrange_collection, which C1 already migrated. Remove C5 from
the chain; the delta-join output flip remains as P7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sink already consumes bundle.collection (the CollectionEdge) directly and
leaf-decodes via into_vec at sinks.rs:71, with no pre-boundary Vec forcing.
When a producer flips columnar the decode lands at that leaf, which is C6's
goal, so no separate node exists. T2 must adapt the sinks.rs:71 call site when
into_vec becomes a free function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LetRec already consumes bundle.collection (the CollectionEdge) and leaf-decodes
via into_vec at render.rs:941/997, then runs consolidate + branch_when on the
Vec. That is the desired sanctioned-decode end state, so no separate node.
T2 must adapt those call sites when into_vec becomes a free function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C8 subsumed: Union already reads inputs as edges, temporal-bucket into_vec at
render.rs:1358 is a gated sanctioned leaf, and concat_many->consolidate_named
goes native via F1. Wave 1 is complete (C1/C3/C4/F1/C2 landed; C5-C8 subsumed).

Also correct T1: vec_to_columnar is NOT dead. It survives as a leaf encode
primitive for row-shaped producers (P9 imports) and the sanctioned-decode
returns (C7 LetRec, C8 Union temporal-bucket), which decode to Vec, operate via
branch_when / maybe_apply_temporal_bucketing, then re-encode. T1 now only
retires the concat_many mixed-variant upgrade branch; the teardown collapses the
enum, not the leaf conversions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standing rule for all producer flips: the pre-migration producers consolidated
duplicates within a batch (ConsolidatingContainerBuilder). Plain ColumnBuilder
drops that, a permanent regression on the broad Mfp-to-sink class. Columnar
builders copy row bytes from a borrowed push either way, so
ConsolidatingColumnBuilder keeps the borrowed push and restores within-batch
consolidation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidation needs owned staging, so ConsolidatingColumnBuilder is owned-give
only. Not a regression: a producer computes its Row fresh (mfp_plan.evaluate
already owns it), so the give is a move into staging, exactly as pre-P1's
ConsolidatingContainerBuilder. The borrowed-push/no-owned-Row property is a
consumer optimization (C1/C3/C4), not applicable to producers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owned-give staging holds N live Rows per batch. A ref-accepting builder that
stages bytes into a Column arena and consolidates via a sorted usize
permutation would hold ~1 live Row, an allocator/peak-memory win on the hot
Mfp-to-sink path. New primitive with its own correctness surface, so tracked as
a focused follow-up PR rather than blocking the producer wave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C1 made arrange_collection return a variant-preserving passthrough and P1's
raw-store rework dropped the last CollectionEdge::Vec force-wrap, so the
ArrangeBy path is already columnar end to end. The Vec force-wraps P3 targeted
do not exist in the tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…subsumed

Reduce/Threshold/TopK-bucketed are arrangement producers (from_columns /
from_expressions), not collection producers, so there is no per-operator
from_collections to flip. Their arrangement is columnar-internal (C1); their
output becomes Vec only at the shared as_specific_collection arrangement
materialization. Repurpose P5 to flip that one shared path (covers Reduce +
Threshold + TopK-bucketed), reduce P6 to TopK's from_collections monotonic/top1
paths, and drop P8 (subsumed by P5). Record the collection-vs-arrangement
producer taxonomy. P5 lands before T2 so T2 stays a mechanical enum collapse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The as_specific_collection Some(key) path reads an already-consolidated
arrangement cursor (distinct rows per time), so it is a consumer read, not a
producer computing new rows. Use plain ColumnBuilder + borrowed push (C1/C3/C4
pattern), not ConsolidatingColumnBuilder + owned give: consolidation would be
wasted sort and would change the deliberate non-consolidating semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mz_join_core cannot emit Column (bound + CapacityContainerBuilder), and its
stage output is intra-operator, so it stays Vec (columnar mz_join_core is a
differential-side fast-follow). Only the final linear-join edge goes columnar:
Some(closure) via flat_map_fallible + ConsolidatingColumnBuilder; None via
vec_to_columnar of the raw stage (leaf-encode, one VecToColumnar op on the
common identity join). Delta unchanged (half_join generic CB -> ColumnBuilder).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spike's "flip half_join generic CB to ColumnBuilder" was wrong: half_join
output carries Result<Row,_> (ColumnBuilder can't build it; ok_err demux runs
after) and isn't the node output anyway (per-path map + finalization, then oks
is their concat). Delta internals are all Vec-native/intra-operator, like
linear's mz_join_core. Flip = vec_to_columnar(oks) at the node output. Both
joins: Vec-internal operators, columnar edge produced by a leaf-encode at the
node output boundary; columnar join algorithms are a differential-side
fast-follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whole-branch review: GO, no correctness defects. Deleting the Vec arm reveals
legitimate internal Vec sites, so teardown expands to five ordered steps:
Ta temporal-bucketing re-encode (unblocks Td), Tb linear-join accumulator off
the edge (M1), Tc retire as_specific_collection flag-off (M2), Td retire
concat_many mixed branch (needs Ta), Te collapse enum + de-match. The leaf
conversions (into_vec/columnar_to_vec/vec_to_columnar) are never deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant