storage: write the persist source's decode straight into columns - #38780
Conversation
ec9fd9d to
6ddfa8b
Compare
72aadae to
4056cf5
Compare
| for (d, t, r) in self.borrow().into_index_iter() { | ||
| let t = T2::to_inner(T1::into_owned(t)); | ||
| inner.push((d, &t, r)); | ||
| } |
There was a problem hiding this comment.
This should go column-by-column, avoiding the reconstruction of the unchanged D and R columns.
There was a problem hiding this comment.
Done. A Column::Typed now moves its data and diff columns across whole and rebuilds only the times. A serialized column owns no typed sub-containers, so all three are rebuilt there, but one column at a time rather than record by record.
Worth noting which variant this path actually sees: the compute import's columns come from ConsolidatingColumnBuilder, whose flush_aligned mints Column::Align, so the serialized arm is the common one and the move only applies to the finish() case.
Posted by Claude Code.
4056cf5 to
3c5e76a
Compare
| // A serialized column owns no typed sub-containers, so all three are rebuilt, | ||
| // one column at a time rather than record by record. | ||
| serialized => { | ||
| let (borrowed_data, borrowed_times, borrowed_diffs) = serialized.borrow(); |
There was a problem hiding this comment.
why not just return serialized here and just pay the cost of materializing the time column
There was a problem hiding this comment.
Done, that works and it is better than what I had. The serialized arm now materializes only the time column and hands the data and diff columns to the output as borrowed views:
let (borrowed_data, borrowed_times, borrowed_diffs) = serialized.borrow();
let mut times = T2::Container::default();
for time in borrowed_times.into_index_iter() {
times.push(&T2::to_inner(T1::into_owned(time)));
}
let view = (borrowed_data, times.borrow(), borrowed_diffs);
let words = indexed::length_in_words(&view);
let mut alloc: Vec<u64> = Vec::with_capacity(words);
indexed::encode(&mut alloc, &view);
Column::Align(alloc)The result is Column::Align rather than Column::Typed, and data and diff cost one copy per column into the output allocation instead of a decode-and-re-encode per record. <(D, T2, R)>::Borrowed is just the tuple of the three borrowed sub-containers, so the mixed view encodes without any extra bounds.
That matters for this path in particular: the compute import's columns come from ConsolidatingColumnBuilder::flush_aligned, so Align is the variant that actually arrives here, and the Typed arm is the rarer finish() case. I kept the Typed arm because there data and diff move across with no copy at all.
Posted by Claude Code.
fa2d5d7 to
c4a3685
Compare
Pull Request is not mergeable
Pull Request is not mergeable
3dbb08c to
d6f0b6c
Compare
d6f0b6c to
6733dce
Compare
6733dce to
b1e8641
Compare
Translating a data shard's physical frontier into its logical one has two parts: a subscription to the txns shard, and a passthrough operator that delays a stream's capability by what the subscription reports. Only the passthrough is per-stream, so `TxnsProgress::new` renders the subscription and `TxnsProgress::translate` renders one passthrough operator per stream. Several streams read off one data shard then share a single subscription rather than opening one each. The container type sits on `translate` rather than on the subscription, so one subscription serves streams of different shapes. `txns_progress` keeps its signature as the single-stream wrapper over both halves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Collection::enter` needs a container-level `Enter` impl to rewrite record times into the inner timestamp. Provide one for `Column<(D, T, R)>`. Only the times change, so a typed column moves its data and diff columns across whole, and a serialized column materializes the times alone and encodes the other two from their borrowed views into the output allocation. Neither path decodes and re-encodes a record. Without this a columnar collection cannot enter the iterative scope of a `WITH MUTUALLY RECURSIVE` dataflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`decode_and_mfp` wrote both sides of a persist read into one container as `Result`s, so every consumer of `persist_source` ran an `ok_err` demux, and a consumer that wanted columns paid a second pass to re-encode the ok side. The decode operator gets a second output instead, with the ok side generic over a container builder, and the compute import asks for columns directly. The storage sink and the materialized-view read-back ask for row vectors and get the containers they had before, without the demux. `record_time` picks what a record stores for its time. Compute passes `|time| time.0`, dropping the `Subtime` coordinate: it refines the capabilities so flow control can pace parts within a millisecond, it stays on the capabilities either way, and no reader of those streams distinguishes times that finely. `persist_source_core` passes `|time| time` and keeps it, because `upsert` builds its collection inside the refined scope, then joins the two sides back with `map` and `concat`. That costs a move per record on a path that had none, and buys one decode implementation rather than two. The two streams share one subscription to the txns shard: `TxnsProgress::new` renders it once and `translate` renders a passthrough operator per stream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b1e8641 to
03fde80
Compare
decode_and_mfpwrote both sides of a persist read into one container asResults, so everyconsumer of
persist_sourceran anok_errdemux, and a consumer that wanted columns paid asecond pass to re-encode the ok side. The decode operator gets a second output instead, with the
ok side generic over a container builder. The compute import asks for columns and gets them
straight from the decode; the storage sink and the materialized-view read-back ask for row
vectors and get the containers they had before, without the demux.
There is one decode implementation.
record_timepicks what a record stores for its time.Compute passes
|time| time.0, dropping theSubtimecoordinate: it refines the capabilities soflow control can pace parts within a millisecond, it stays on the capabilities either way, and no
reader of those streams distinguishes times that finely.
persist_source_corepasses|time| timeand keeps it, becauseupsertbuilds its collection inside the refined scope, thenjoins the two sides back with
mapandconcat. That is a move per record on a path that hadnone, and it is the reason there is one decode operator rather than two.
Three commits, separable if you would rather review them apart:
txn-wal:txns_progress_remapandtxns_progress_frontiersare exposed separately, so thetwo streams share one subscription to the txns shard.
txns_progress_frontiersbecomes genericover the container it passes through.
timely-util: anEnterimpl forColumn<(D, T, R)>, which a columnar collection needs toenter the iterative scope of a
WITH MUTUALLY RECURSIVEdataflow.storage-operatorsandcompute: the split itself.On the table-import dataflow this removes the
OkErrandVecToColumnaroperators and fourVec<(Result<Row, E>, (Timestamp, Subtime), Diff)>channels, visible in the change totest/sqllogictest/introspection/relations.slt.Not measured. The
map/concaton the upsert rehydration read is the one place this adds workrather than removing it, and
delay_sources_past_rehydrationexists because that path islatency-sensitive, so it is worth a look.
🤖 Generated with Claude Code