Skip to main content

spiral/
changes.rs

1//! Rows that changed between two versions of a table, read from a change plan.
2//!
3//! A change plan comes from version metadata alone. Reading the changed rows composes scans of
4//! both versions, each restricted to the plan's row IDs, into one [`Scan`].
5
6use std::collections::{HashMap, HashSet};
7use std::iter::repeat_n;
8use std::mem::take;
9use std::ops::Range;
10use std::sync::Arc;
11
12use arrow_array::cast::AsArray as _;
13use arrow_array::types::UInt64Type;
14use arrow_array::{ArrayRef, BooleanArray, RecordBatch, StringArray, UInt64Array, new_null_array};
15use arrow_schema::{DataType, Field, Schema, SchemaRef};
16use arrow_select::concat::concat_batches;
17use arrow_select::filter::filter_record_batch;
18use arrow_select::interleave::interleave;
19use async_trait::async_trait;
20use futures::stream::BoxStream;
21use futures::{StreamExt as _, TryStreamExt as _, stream};
22use uuid::Uuid;
23
24use crate::catalog_types::{
25    Commit as CatalogCommit, CommitNode, CommitSelector, CommitTarget, GraphTraversal,
26    ProjectCommitId, ReadCommitLogRequest, ResourceChange,
27};
28use crate::scan::{ScanInfo, ScanSource, SplitFilter};
29use crate::scandal::api::{ChangePlan as WireChangePlan, FragmentDelta as WireDelta, RowIdRange};
30use crate::scandal::types::FilterClause;
31use crate::table_write::ROW_ID_COLUMN;
32use crate::{BatchStream, Error, Scan, ScanOptions, Snapshot, Table, WorkItem};
33
34/// The column of a change read that says how each row changed: one of the
35/// [`ChangeKind`] names.
36pub const CHANGE_KIND_COLUMN: &str = "s:change_kind";
37
38/// How a row of a change read changed.
39#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub enum ChangeKind {
41    /// The row was added: `insert`.
42    Insert,
43    /// The row was deleted: `delete`.
44    Delete,
45    /// The row's values before an update: `update_before`.
46    UpdateBefore,
47    /// The row's values after an update: `update_after`.
48    UpdateAfter,
49}
50
51impl ChangeKind {
52    /// The name [`CHANGE_KIND_COLUMN`] holds for this kind.
53    pub fn as_str(self) -> &'static str {
54        match self {
55            Self::Insert => "insert",
56            Self::Delete => "delete",
57            Self::UpdateBefore => "update_before",
58            Self::UpdateAfter => "update_after",
59        }
60    }
61}
62
63/// How one version of a table changed since an earlier one, read from version metadata.
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct ChangePlan {
66    /// One entry for each fragment of the later version, in stored order.
67    pub fragments: Vec<FragmentChange>,
68    /// Fragments of the earlier version that the later one no longer has. Each of their rows
69    /// was deleted or carried into an added fragment.
70    pub removed_fragment_ids: Vec<Uuid>,
71    /// Requested columns whose type differs from their type in the earlier version, including
72    /// columns the earlier version lacks.
73    pub retyped_columns: Vec<String>,
74}
75
76/// One fragment of the later version of a [`ChangePlan`].
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct FragmentChange {
79    /// The fragment's stable ID.
80    pub fragment_id: Uuid,
81    /// The fragment's stored row count, deleted rows included.
82    pub row_count: u64,
83    /// How the fragment relates to the earlier version.
84    pub change: FragmentDelta,
85}
86
87/// How one fragment relates to the earlier version of a [`ChangePlan`]. Each set of row IDs is
88/// sorted, disjoint, half-open ranges.
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum FragmentDelta {
91    /// A fragment of the earlier version, at the same positions. Its other rows are unchanged.
92    Retained {
93        /// Rows kept whose requested columns a patch or column write changed.
94        patched: Vec<Range<u64>>,
95        /// Rows deleted since.
96        deleted: Vec<Range<u64>>,
97    },
98    /// A fragment written since the earlier version.
99    Added {
100        /// Rows added since, with row IDs past every row ID the earlier version allocated.
101        appended: Vec<Range<u64>>,
102        /// Rows of the earlier version that a rewrite, such as compaction, moved here.
103        carried: Vec<Range<u64>>,
104        /// Carried rows whose requested columns a patch changed.
105        patched: Vec<Range<u64>>,
106    },
107}
108
109impl TryFrom<WireChangePlan> for ChangePlan {
110    type Error = Error;
111
112    /// The change plan a scandal returned, as sorted row ID ranges.
113    fn try_from(plan: WireChangePlan) -> Result<Self, Error> {
114        let fragments = plan
115            .fragments
116            .into_iter()
117            .map(|fragment| {
118                let change = match fragment.change {
119                    WireDelta::Retained(retained) => FragmentDelta::Retained {
120                        patched: ranges(&retained.patched),
121                        deleted: ranges(&retained.deleted),
122                    },
123                    WireDelta::Added(added) => FragmentDelta::Added {
124                        appended: ranges(&added.appended),
125                        carried: ranges(&added.carried),
126                        patched: ranges(&added.patched),
127                    },
128                    WireDelta::Unknown { tag, .. } => {
129                        return Err(Error::Protocol(format!("unknown fragment change `{tag}`")));
130                    }
131                };
132                Ok(FragmentChange {
133                    fragment_id: fragment.fragment_id,
134                    row_count: fragment.row_count.0,
135                    change,
136                })
137            })
138            .collect::<Result<_, Error>>()?;
139        Ok(Self {
140            fragments,
141            removed_fragment_ids: plan.removed_fragment_ids,
142            retyped_columns: plan.retyped_columns,
143        })
144    }
145}
146
147fn ranges(wire: &[RowIdRange]) -> Vec<Range<u64>> {
148    wire.iter()
149        .map(|range| range.start.0..range.end.0)
150        .collect()
151}
152
153/// Which rows a change read returns, and how.
154#[derive(Clone, Debug, Default)]
155pub struct ChangeOptions {
156    /// The columns, projection and filter of each image. A change read sets the row IDs itself,
157    /// and takes no limit, row range, split token or emit.
158    pub scan: ScanOptions,
159    /// Which images of each changed row to return.
160    pub images: ChangeImages,
161}
162
163/// Which images of each changed row a change read returns. Every row carries its row ID in
164/// [`ROW_ID_COLUMN`] and its kind in [`CHANGE_KIND_COLUMN`].
165#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
166pub enum ChangeImages {
167    /// Only the kind and row ID, read from the change plan without reading column values.
168    /// Updates are reported as `update_after`. Takes no filter.
169    Keys,
170    /// Values of inserted rows and the new values of updated rows, as `update_after`; deleted
171    /// rows carry only their row ID.
172    #[default]
173    After,
174    /// Values of inserted and deleted rows, and adjacent `update_before` and `update_after`
175    /// images of each update.
176    BeforeAndAfter,
177}
178
179/// The changes one Catalog commit made to a table: the versions it advanced between, and the
180/// changed rows.
181pub struct TableCommitChanges {
182    /// The commit that advanced the table.
183    pub commit_id: ProjectCommitId,
184    /// The table's version before the commit.
185    pub since: Table,
186    /// The table's version the commit published.
187    pub table: Table,
188    /// The changed rows, as [`Table::changes_since`] reads them.
189    pub changes: Scan,
190}
191
192impl Table {
193    /// How this version changed since `since`, from version metadata only: for each fragment,
194    /// whether `since` has it at the same positions, with the row IDs patched and deleted since,
195    /// or whether it was added since, with the row IDs appended since and those a rewrite
196    /// carried over. `columns` names the columns whose changes count; all when absent. `since`
197    /// must be this version or one of its ancestors.
198    pub async fn plan_changes(
199        &self,
200        since: &Table,
201        columns: Option<Vec<String>>,
202    ) -> Result<ChangePlan, Error> {
203        if since.identity().resource_id != self.identity().resource_id {
204            return Err(Error::invalid("changes compare two versions of one table"));
205        }
206        let plan = self
207            .connection
208            .tables()?
209            .plan_changes(self.identity(), since.identity().version_id, columns)
210            .await?;
211        ChangePlan::try_from(plan)
212    }
213
214    /// The rows that changed since `since`, with their [`ChangeKind`] and row ID, as one scan.
215    ///
216    /// A row a rewrite carried without changing it is not reported; across an overwrite of the
217    /// whole table every old row is deleted and every new row inserted. The filter applies to
218    /// each image, and an update is reported when either image matches. Before images are read
219    /// from `since` with the same columns and must have this version's names and types there.
220    /// The scan has no splits to read by work item.
221    pub async fn changes_since(
222        &self,
223        since: &Table,
224        options: ChangeOptions,
225    ) -> Result<Scan, Error> {
226        check_change_options(&options)?;
227        let counted = match options.scan.projection {
228            Some(_) => None,
229            None => options.scan.columns.clone(),
230        };
231        let plan = self.plan_changes(since, counted).await?;
232        let reader = ChangeReader::open(
233            self.clone(),
234            since.clone(),
235            options.clone(),
236            ChangedRows::from_plan(&plan),
237        )
238        .await?;
239        let info = ScanInfo {
240            source_fields: None,
241            schema: reader.schema.as_ref().clone(),
242            filter_clauses: reader.filter_clauses.clone(),
243            write_layout: None,
244            statistics: None,
245        };
246        Scan::new(
247            self.identity(),
248            options.scan,
249            info,
250            Arc::new(ChangeSource(Arc::new(reader))),
251        )
252    }
253}
254
255impl Snapshot {
256    /// The changes to the table at `path` in each commit between `since` and this snapshot,
257    /// walking first-parent Catalog history from the oldest commit. Each commit that advanced
258    /// the table contributes one scan, read as [`Table::changes_since`] reads it. `since` must
259    /// be a first-parent ancestor of this snapshot, and `path` must name the same table in
260    /// both.
261    pub async fn table_changes(
262        &self,
263        path: &str,
264        since: &Snapshot,
265        options: ChangeOptions,
266    ) -> Result<Vec<TableCommitChanges>, Error> {
267        if since.project.id.as_str() != self.project.id.as_str() {
268            return Err(Error::invalid("changes compare two commits of one project"));
269        }
270        let resource_id = self.table(path).await?.identity().resource_id;
271        if since.table(path).await?.identity().resource_id != resource_id {
272            return Err(Error::invalid(format!(
273                "`{path}` names another table at {}",
274                since.commit_id
275            )));
276        }
277        let mut changes = Vec::new();
278        for node in self.first_parent_commits(since).await? {
279            if node.resource_change_count == 0 {
280                continue;
281            }
282            let commit = self
283                .project
284                .client
285                .connection
286                .catalog
287                .read_journal_commit(&self.project.id, node.commit_id)
288                .await?;
289            let Some((before, after)) = table_advance(&commit, resource_id)? else {
290                continue;
291            };
292            let before = self.project.client.table_by_id(resource_id, before);
293            let after = self.project.client.table_by_id(resource_id, after);
294            let scan = after.changes_since(&before, options.clone()).await?;
295            changes.push(TableCommitChanges {
296                commit_id: node.commit_id,
297                since: before,
298                table: after,
299                changes: scan,
300            });
301        }
302        Ok(changes)
303    }
304
305    /// The commits after `since` on this snapshot's first-parent chain, oldest first.
306    async fn first_parent_commits(&self, since: &Snapshot) -> Result<Vec<CommitNode>, Error> {
307        let catalog = &self.project.client.connection.catalog;
308        let excluded = vec![CommitSelector::Commit(CommitTarget {
309            commit_id: since.commit_id,
310        })];
311        let mut nodes = HashMap::new();
312        let mut starts = vec![self.commit_id];
313        while !starts.is_empty() {
314            let log = catalog
315                .read_commit_log(
316                    &self.project.id,
317                    &ReadCommitLogRequest {
318                        starts: starts
319                            .iter()
320                            .map(|commit_id| {
321                                CommitSelector::Commit(CommitTarget {
322                                    commit_id: *commit_id,
323                                })
324                            })
325                            .collect(),
326                        exclude_ancestors_of: Some(excluded.clone()),
327                        max_commits: None,
328                        traversal: Some(GraphTraversal::FirstParent),
329                    },
330                )
331                .await?;
332            if log.commits.is_empty() && !log.continuation_commit_ids.is_empty() {
333                return Err(Error::Protocol(
334                    "the commit log continued without returning commits".into(),
335                ));
336            }
337            for node in log.commits {
338                nodes.insert(node.commit_id.0, node);
339            }
340            starts = log
341                .continuation_commit_ids
342                .into_iter()
343                .filter(|commit_id| !nodes.contains_key(&commit_id.0))
344                .collect();
345        }
346        let not_ancestor = || {
347            Error::invalid(format!(
348                "commit {} is not a first-parent ancestor of {}",
349                since.commit_id, self.commit_id
350            ))
351        };
352        let mut chain = Vec::new();
353        let mut current = self.commit_id;
354        while current != since.commit_id {
355            let node = nodes.remove(&current.0).ok_or_else(not_ancestor)?;
356            current = *node.parent_commit_ids.first().ok_or_else(not_ancestor)?;
357            chain.push(node);
358        }
359        chain.reverse();
360        Ok(chain)
361    }
362}
363
364/// The versions a commit advanced the Table `resource_id` between, or `None` when it left the
365/// table as it was.
366fn table_advance(commit: &CatalogCommit, resource_id: Uuid) -> Result<Option<(Uuid, Uuid)>, Error> {
367    for change in &commit.resources {
368        match change {
369            ResourceChange::Advance(advance) if advance.resource_id.0 == resource_id => {
370                return Ok(Some((advance.expected.0, advance.replacement.0)));
371            }
372            ResourceChange::Create(create) if create.descriptor.resource_id.0 == resource_id => {
373                return Err(Error::invalid(format!(
374                    "commit {} created the table, so it has no earlier version",
375                    commit.commit_id
376                )));
377            }
378            ResourceChange::Retire(retire) if retire.resource_id.0 == resource_id => {
379                return Err(Error::invalid(format!(
380                    "commit {} retired the table",
381                    commit.commit_id
382                )));
383            }
384            _ => {}
385        }
386    }
387    Ok(None)
388}
389
390fn check_change_options(options: &ChangeOptions) -> Result<(), Error> {
391    let scan = &options.scan;
392    if scan.limit.is_some()
393        || scan.row_range.is_some()
394        || scan.split_token.is_some()
395        || scan.emit.is_some()
396        || scan.row_ids.is_some()
397    {
398        return Err(Error::invalid(
399            "a change read takes no limit, row range, split token, emit or row IDs",
400        ));
401    }
402    if scan
403        .row_id_column
404        .as_ref()
405        .is_some_and(|column| column != ROW_ID_COLUMN)
406    {
407        return Err(Error::invalid(format!(
408            "a change read names row IDs `{ROW_ID_COLUMN}`"
409        )));
410    }
411    if options.images == ChangeImages::Keys && scan.filter.is_some() {
412        return Err(Error::invalid(
413            "a change read of keys reads no values to filter",
414        ));
415    }
416    Ok(())
417}
418
419/// The row IDs a change plan reports, by what happened to them.
420#[derive(Clone, Debug, Default, Eq, PartialEq)]
421struct ChangedRows {
422    inserted: Vec<Range<u64>>,
423    updated: Vec<Range<u64>>,
424    /// Rows deleted from fragments both versions have.
425    deleted: Vec<Range<u64>>,
426    /// Fragments of the earlier version that the later one dropped. Their rows were deleted
427    /// unless carried.
428    removed_fragments: HashSet<Uuid>,
429    carried: Vec<Range<u64>>,
430}
431
432impl ChangedRows {
433    fn from_plan(plan: &ChangePlan) -> Self {
434        let mut rows = Self {
435            removed_fragments: plan.removed_fragment_ids.iter().copied().collect(),
436            ..Self::default()
437        };
438        for fragment in &plan.fragments {
439            match &fragment.change {
440                FragmentDelta::Retained { patched, deleted } => {
441                    rows.updated.extend(patched.iter().cloned());
442                    rows.deleted.extend(deleted.iter().cloned());
443                }
444                FragmentDelta::Added {
445                    appended,
446                    carried,
447                    patched,
448                } => {
449                    rows.inserted.extend(appended.iter().cloned());
450                    rows.carried.extend(carried.iter().cloned());
451                    rows.updated.extend(patched.iter().cloned());
452                }
453            }
454        }
455        Self {
456            inserted: union_ranges(rows.inserted),
457            updated: union_ranges(rows.updated),
458            deleted: union_ranges(rows.deleted),
459            carried: union_ranges(rows.carried),
460            removed_fragments: rows.removed_fragments,
461        }
462    }
463}
464
465/// Row IDs one image read selects, at most this many ranges at once.
466const RANGES_PER_READ: usize = 4096;
467/// Updates paired at once, by row ID, when reading before and after images.
468const PAIRED_ROWS: u64 = 65_536;
469/// Rows of each batch a change read builds from row IDs alone.
470const ID_BATCH_ROWS: u64 = 65_536;
471
472/// The version an image is read from.
473#[derive(Clone, Copy, Debug, Eq, PartialEq)]
474enum Image {
475    Before,
476    After,
477}
478
479/// What an image read returns for each row.
480#[derive(Clone, Copy, Debug, Eq, PartialEq)]
481enum Returns {
482    /// The image's columns and the row ID.
483    Values,
484    /// The row ID alone.
485    Ids,
486}
487
488type Batches = BoxStream<'static, Result<RecordBatch, Error>>;
489
490/// The reads of one change read: what changed, and how to read each image.
491struct ChangeReader {
492    after: Table,
493    before: Table,
494    images: ChangeImages,
495    /// The caller's columns, projection and filter, which every image read applies.
496    scan: ScanOptions,
497    rows: ChangedRows,
498    /// The image columns, nullable, then the row ID and the change kind.
499    schema: SchemaRef,
500    filter_clauses: Vec<FilterClause>,
501}
502
503impl ChangeReader {
504    /// Plan an image read of each version to learn the output schema and check the filter.
505    async fn open(
506        after: Table,
507        before: Table,
508        options: ChangeOptions,
509        rows: ChangedRows,
510    ) -> Result<Self, Error> {
511        let mut reader = Self {
512            after,
513            before,
514            images: options.images,
515            scan: options.scan,
516            rows,
517            schema: output_schema(&[]),
518            filter_clauses: Vec::new(),
519        };
520        if reader.images == ChangeImages::Keys {
521            return Ok(reader);
522        }
523        let probe = reader.read_options(Some(Vec::new()), Returns::Values);
524        let after_scan = reader.after.scan(probe.clone()).await?;
525        let image = after_scan.schema();
526        let image_fields = &image.fields()[..image.fields().len() - 1];
527        if reader.images == ChangeImages::BeforeAndAfter {
528            let before = reader.before.scan(probe).await?.schema();
529            if !same_columns(&image, &before) {
530                return Err(Error::invalid(
531                    "before images need the earlier version to hold the read columns with this \
532                     version's names and types",
533                ));
534            }
535        }
536        reader.schema = output_schema(
537            &image_fields
538                .iter()
539                .map(|field| Field::clone(field).with_nullable(true))
540                .collect::<Vec<_>>(),
541        );
542        reader.filter_clauses = after_scan.filter_clauses().to_vec();
543        Ok(reader)
544    }
545
546    /// Every changed row: deletions, then insertions, then updates.
547    fn read(self: Arc<Self>) -> Batches {
548        let parts = match self.images {
549            ChangeImages::Keys => vec![
550                Arc::clone(&self).deleted_rows(Returns::Ids),
551                Arc::clone(&self).id_rows(self.rows.inserted.clone(), ChangeKind::Insert),
552                Arc::clone(&self).id_rows(self.rows.updated.clone(), ChangeKind::UpdateAfter),
553            ],
554            ChangeImages::After => vec![
555                Arc::clone(&self).deleted_rows(Returns::Ids),
556                Arc::clone(&self).inserted_rows(),
557                Arc::clone(&self).updated_after(),
558            ],
559            ChangeImages::BeforeAndAfter => vec![
560                Arc::clone(&self).deleted_rows(Returns::Values),
561                Arc::clone(&self).inserted_rows(),
562                Arc::clone(&self).updated_pairs(),
563            ],
564        };
565        stream::iter(parts).flatten().boxed()
566    }
567
568    /// Rows deleted since the earlier version: those deleted from fragments both versions
569    /// have, then those of dropped fragments that no rewrite carried.
570    fn deleted_rows(self: Arc<Self>, returns: Returns) -> Batches {
571        let deleted = self.rows.deleted.clone();
572        let retained = if returns == Returns::Ids && self.scan.filter.is_none() {
573            Arc::clone(&self).id_rows(deleted, ChangeKind::Delete)
574        } else {
575            Arc::clone(&self).image_rows(Image::Before, deleted, returns, ChangeKind::Delete)
576        };
577        retained.chain(self.dropped_rows(returns)).boxed()
578    }
579
580    fn inserted_rows(self: Arc<Self>) -> Batches {
581        let inserted = self.rows.inserted.clone();
582        self.image_rows(Image::After, inserted, Returns::Values, ChangeKind::Insert)
583    }
584
585    /// The new values of updated rows. With a filter, an update is reported when either image
586    /// matches, so each group of updates first reads both images' matching row IDs.
587    fn updated_after(self: Arc<Self>) -> Batches {
588        let updated = self.rows.updated.clone();
589        if self.scan.filter.is_none() {
590            return self.image_rows(
591                Image::After,
592                updated,
593                Returns::Values,
594                ChangeKind::UpdateAfter,
595            );
596        }
597        stream::iter(chunk_ranges(&updated, PAIRED_ROWS))
598            .then(move |chunk| {
599                let reader = Arc::clone(&self);
600                async move {
601                    let matching = reader.matching_ids(chunk).await?;
602                    let after = reader.read_images(Image::After, matching).await?;
603                    after
604                        .into_iter()
605                        .map(|batch| {
606                            reader.output(&batch, kinds(ChangeKind::UpdateAfter, batch.num_rows()))
607                        })
608                        .collect::<Result<Vec<_>, _>>()
609                }
610            })
611            .map_ok(|batches| stream::iter(batches.into_iter().map(Ok)))
612            .try_flatten()
613            .boxed()
614    }
615
616    /// Adjacent before and after images of each update, a group of updates at a time.
617    fn updated_pairs(self: Arc<Self>) -> Batches {
618        let updated = self.rows.updated.clone();
619        stream::iter(chunk_ranges(&updated, PAIRED_ROWS))
620            .then(move |chunk| {
621                let reader = Arc::clone(&self);
622                async move {
623                    let matching = match reader.scan.filter {
624                        Some(_) => reader.matching_ids(chunk).await?,
625                        None => chunk,
626                    };
627                    let before = reader.read_images(Image::Before, matching.clone()).await?;
628                    let after = reader.read_images(Image::After, matching).await?;
629                    let (before, after) = (
630                        concat_batches(&image_schema(&reader.schema, &before)?, &before)
631                            .map_err(Error::arrow)?,
632                        concat_batches(&image_schema(&reader.schema, &after)?, &after)
633                            .map_err(Error::arrow)?,
634                    );
635                    let (paired, kinds) = pair_images(&before, &after)?;
636                    reader.output(&paired, kinds)
637                }
638            })
639            .boxed()
640    }
641
642    /// The rows of fragments the earlier version had and this one dropped, less those a
643    /// rewrite carried, read from the earlier version.
644    fn dropped_rows(self: Arc<Self>, returns: Returns) -> Batches {
645        if self.rows.removed_fragments.is_empty() {
646            return stream::empty().boxed();
647        }
648        stream::once(async move {
649            let scan = self.before.scan(self.read_options(None, returns)).await?;
650            let mut work = Vec::new();
651            for (split, planned) in scan.splits().iter().enumerate() {
652                let fragment_id = planned.fragment_id.ok_or_else(|| {
653                    Error::invalid("the scandal does not name the fragments a scan's splits read")
654                })?;
655                if self.rows.removed_fragments.contains(&fragment_id) {
656                    work.push(WorkItem {
657                        split,
658                        rows: planned.rows.clone(),
659                    });
660                }
661            }
662            if work.is_empty() {
663                return Ok::<Batches, Error>(stream::empty().boxed());
664            }
665            let batches = scan.read_work(work).await?;
666            Ok(batches
667                .map(move |read| {
668                    let read = read?;
669                    let deleted = without_rows(&read.batch, &self.rows.carried)?;
670                    self.output(&deleted, kinds(ChangeKind::Delete, deleted.num_rows()))
671                })
672                .boxed())
673        })
674        .try_flatten()
675        .boxed()
676    }
677
678    /// Rows of `kind` built from their row IDs alone, without reading either version.
679    fn id_rows(self: Arc<Self>, ids: Vec<Range<u64>>, kind: ChangeKind) -> Batches {
680        stream::iter(chunk_ranges(&ids, ID_BATCH_ROWS))
681            .map(move |chunk| {
682                let ids = UInt64Array::from_iter_values(chunk.into_iter().flatten());
683                let batch = RecordBatch::try_new(row_id_schema(), vec![Arc::new(ids)])
684                    .map_err(Error::arrow)?;
685                self.output(&batch, kinds(kind, batch.num_rows()))
686            })
687            .boxed()
688    }
689
690    /// Rows `ids` of one version, filtered, as rows of `kind`.
691    fn image_rows(
692        self: Arc<Self>,
693        image: Image,
694        ids: Vec<Range<u64>>,
695        returns: Returns,
696        kind: ChangeKind,
697    ) -> Batches {
698        stream::iter(
699            ids.chunks(RANGES_PER_READ)
700                .map(<[Range<u64>]>::to_vec)
701                .collect::<Vec<_>>(),
702        )
703        .then(move |chunk| {
704            let reader = Arc::clone(&self);
705            async move {
706                let options = reader.read_options(Some(chunk), returns);
707                let batches = reader.table(image).scan(options).await?.into_stream();
708                Ok::<_, Error>(batches.map(move |batch| {
709                    let batch = batch?;
710                    reader.output(&batch, kinds(kind, batch.num_rows()))
711                }))
712            }
713        })
714        .try_flatten()
715        .boxed()
716    }
717
718    /// The row IDs among `ids` whose image in either version matches the filter.
719    async fn matching_ids(&self, ids: Vec<Range<u64>>) -> Result<Vec<Range<u64>>, Error> {
720        let mut matching = Vec::new();
721        for image in [Image::Before, Image::After] {
722            let options = self.read_options(Some(ids.clone()), Returns::Ids);
723            let mut batches = self.table(image).scan(options).await?.into_stream();
724            while let Some(batch) = batches.next_batch().await {
725                matching.extend_from_slice(row_ids(&batch?)?.values());
726            }
727        }
728        Ok(ranges_of(matching))
729    }
730
731    /// Every row `ids` of one version, unfiltered, with the image's columns.
732    async fn read_images(
733        &self,
734        image: Image,
735        ids: Vec<Range<u64>>,
736    ) -> Result<Vec<RecordBatch>, Error> {
737        if ids.is_empty() {
738            return Ok(Vec::new());
739        }
740        let options = ScanOptions {
741            filter: None,
742            ..self.read_options(Some(ids), Returns::Values)
743        };
744        self.table(image)
745            .scan(options)
746            .await?
747            .into_stream()
748            .try_collect()
749            .await
750    }
751
752    fn table(&self, image: Image) -> &Table {
753        match image {
754            Image::Before => &self.before,
755            Image::After => &self.after,
756        }
757    }
758
759    /// Scan options that read `row_ids` with the caller's filter, returning the image's
760    /// columns or only row IDs.
761    fn read_options(&self, row_ids: Option<Vec<Range<u64>>>, returns: Returns) -> ScanOptions {
762        let (columns, projection) = match returns {
763            Returns::Values => (self.scan.columns.clone(), self.scan.projection.clone()),
764            Returns::Ids => (Some(Vec::new()), None),
765        };
766        ScanOptions {
767            columns,
768            projection,
769            filter: self.scan.filter.clone(),
770            batch_rows: self.scan.batch_rows,
771            collect_metrics: self.scan.collect_metrics,
772            row_ids,
773            row_id_column: Some(ROW_ID_COLUMN.to_owned()),
774            ..Default::default()
775        }
776    }
777
778    /// `batch`, image columns then row IDs or row IDs alone, as output rows of `kinds`.
779    fn output(&self, batch: &RecordBatch, kinds: ArrayRef) -> Result<RecordBatch, Error> {
780        let image_fields = &self.schema.fields()[..self.schema.fields().len() - 2];
781        let rows = batch.num_rows();
782        let mut columns = match batch.num_columns() {
783            count if count == image_fields.len() + 1 => batch.columns().to_vec(),
784            1 => {
785                let mut columns = image_fields
786                    .iter()
787                    .map(|field| new_null_array(field.data_type(), rows))
788                    .collect::<Vec<_>>();
789                columns.push(Arc::clone(batch.column(0)));
790                columns
791            }
792            count => {
793                return Err(Error::Protocol(format!(
794                    "a change read's image has {count} columns, not {}",
795                    image_fields.len() + 1
796                )));
797            }
798        };
799        columns.push(kinds);
800        RecordBatch::try_new(Arc::clone(&self.schema), columns).map_err(Error::arrow)
801    }
802}
803
804/// A change read as the source of a [`Scan`].
805struct ChangeSource(Arc<ChangeReader>);
806
807#[async_trait]
808impl ScanSource for ChangeSource {
809    async fn read(&self, _narrowing: Option<Arc<dyn SplitFilter>>) -> Result<BatchStream, Error> {
810        let schema = Arc::clone(&self.0.schema);
811        Ok(BatchStream::from_stream(schema, Arc::clone(&self.0).read()))
812    }
813}
814
815fn row_id_field() -> Field {
816    Field::new(ROW_ID_COLUMN, DataType::UInt64, false)
817}
818
819fn row_id_schema() -> SchemaRef {
820    Arc::new(Schema::new(vec![row_id_field()]))
821}
822
823/// `image_fields`, then the row ID and the change kind.
824fn output_schema(image_fields: &[Field]) -> SchemaRef {
825    let mut fields = image_fields.to_vec();
826    fields.push(row_id_field());
827    fields.push(Field::new(CHANGE_KIND_COLUMN, DataType::Utf8, false));
828    Arc::new(Schema::new(fields))
829}
830
831/// The schema of image batches, from the first of them or else the output's image columns.
832fn image_schema(output: &Schema, batches: &[RecordBatch]) -> Result<SchemaRef, Error> {
833    if let Some(batch) = batches.first() {
834        return Ok(batch.schema());
835    }
836    let fields = &output.fields()[..output.fields().len() - 1];
837    Ok(Arc::new(Schema::new(fields.to_vec())))
838}
839
840/// Whether two image schemas hold the same columns, by name and type.
841fn same_columns(left: &Schema, right: &Schema) -> bool {
842    left.fields().len() == right.fields().len()
843        && left
844            .fields()
845            .iter()
846            .zip(right.fields())
847            .all(|(left, right)| {
848                left.name() == right.name() && left.data_type() == right.data_type()
849            })
850}
851
852fn kinds(kind: ChangeKind, rows: usize) -> ArrayRef {
853    Arc::new(StringArray::from_iter_values(repeat_n(kind.as_str(), rows)))
854}
855
856/// The row IDs of an image batch, its last column.
857fn row_ids(batch: &RecordBatch) -> Result<&UInt64Array, Error> {
858    batch
859        .columns()
860        .last()
861        .and_then(|column| column.as_primitive_opt::<UInt64Type>())
862        .ok_or_else(|| Error::Protocol(format!("an image read returned no `{ROW_ID_COLUMN}`")))
863}
864
865/// Each update's before image followed by its after image, matched by row ID, and the kind of
866/// each output row.
867fn pair_images(
868    before: &RecordBatch,
869    after: &RecordBatch,
870) -> Result<(RecordBatch, ArrayRef), Error> {
871    let after_rows = row_ids(after)?
872        .values()
873        .iter()
874        .enumerate()
875        .map(|(row, id)| (*id, row))
876        .collect::<HashMap<_, _>>();
877    let mut indices = Vec::with_capacity(before.num_rows() * 2);
878    for (row, id) in row_ids(before)?.values().iter().enumerate() {
879        let paired = after_rows.get(id).ok_or_else(|| {
880            Error::Protocol(format!(
881                "updated row {id} has no image in the later version"
882            ))
883        })?;
884        indices.push((0, row));
885        indices.push((1, *paired));
886    }
887    if indices.len() != after.num_rows() * 2 {
888        return Err(Error::Protocol(
889            "an updated row has no image in the earlier version".into(),
890        ));
891    }
892    let columns = (0..before.num_columns())
893        .map(|column| {
894            interleave(
895                &[
896                    before.column(column).as_ref(),
897                    after.column(column).as_ref(),
898                ],
899                &indices,
900            )
901            .map_err(Error::arrow)
902        })
903        .collect::<Result<Vec<_>, _>>()?;
904    let paired = RecordBatch::try_new(after.schema(), columns).map_err(Error::arrow)?;
905    let kinds = StringArray::from_iter_values(indices.iter().map(|(image, _)| match image {
906        0 => ChangeKind::UpdateBefore.as_str(),
907        _ => ChangeKind::UpdateAfter.as_str(),
908    }));
909    Ok((paired, Arc::new(kinds)))
910}
911
912/// `batch` without the rows whose row IDs fall in the sorted, disjoint `ranges`.
913fn without_rows(batch: &RecordBatch, ranges: &[Range<u64>]) -> Result<RecordBatch, Error> {
914    if ranges.is_empty() {
915        return Ok(batch.clone());
916    }
917    let keep = row_ids(batch)?
918        .values()
919        .iter()
920        .map(|id| Some(!in_ranges(*id, ranges)))
921        .collect::<BooleanArray>();
922    filter_record_batch(batch, &keep).map_err(Error::arrow)
923}
924
925/// Whether `id` falls in the sorted, disjoint `ranges`.
926fn in_ranges(id: u64, ranges: &[Range<u64>]) -> bool {
927    let after = ranges.partition_point(|range| range.start <= id);
928    after > 0 && ranges[after - 1].contains(&id)
929}
930
931/// `ranges` sorted, with overlapping and adjacent ranges joined and empty ones dropped.
932fn union_ranges(mut ranges: Vec<Range<u64>>) -> Vec<Range<u64>> {
933    ranges.retain(|range| range.start < range.end);
934    ranges.sort_by_key(|range| range.start);
935    let mut joined: Vec<Range<u64>> = Vec::with_capacity(ranges.len());
936    for range in ranges {
937        match joined.last_mut() {
938            Some(last) if range.start <= last.end => last.end = last.end.max(range.end),
939            _ => joined.push(range),
940        }
941    }
942    joined
943}
944
945/// The row IDs `ids` as sorted, disjoint ranges.
946fn ranges_of(mut ids: Vec<u64>) -> Vec<Range<u64>> {
947    ids.sort_unstable();
948    ids.dedup();
949    union_ranges(ids.into_iter().map(|id| id..id + 1).collect())
950}
951
952/// The sorted, disjoint `ranges` in groups of at most `ids` row IDs, splitting ranges at group
953/// boundaries.
954fn chunk_ranges(ranges: &[Range<u64>], ids: u64) -> Vec<Vec<Range<u64>>> {
955    let mut chunks = Vec::new();
956    let (mut chunk, mut held) = (Vec::new(), 0);
957    for range in ranges {
958        let mut start = range.start;
959        while start < range.end {
960            let end = range.end.min(start + (ids - held));
961            chunk.push(start..end);
962            held += end - start;
963            start = end;
964            if held == ids {
965                chunks.push(take(&mut chunk));
966                held = 0;
967            }
968        }
969    }
970    if !chunk.is_empty() {
971        chunks.push(chunk);
972    }
973    chunks
974}
975
976#[cfg(test)]
977#[expect(
978    clippy::single_range_in_vec_init,
979    reason = "Row-ID sets are lists of ranges; one range is a one-element list."
980)]
981mod tests {
982    use arrow_array::Int64Array;
983    use arrow_array::types::Int64Type;
984    use serde_json::json;
985
986    use super::*;
987
988    #[test]
989    fn row_id_ranges_join_split_and_test_membership() {
990        assert_eq!(
991            union_ranges(vec![5..7, 0..2, 2..3, 6..9, 4..4]),
992            [0..3, 5..9]
993        );
994        assert_eq!(ranges_of(vec![4, 1, 2, 2, 7]), [1..3, 4..5, 7..8]);
995        assert_eq!(
996            chunk_ranges(&[0..3, 10..15], 4),
997            [vec![0..3, 10..11], vec![11..15]]
998        );
999        assert!(chunk_ranges(&[], 4).is_empty());
1000        let ranges = [2..4, 8..9];
1001        assert_eq!(
1002            (0..10)
1003                .filter(|id| in_ranges(*id, &ranges))
1004                .collect::<Vec<_>>(),
1005            [2, 3, 8]
1006        );
1007    }
1008
1009    #[test]
1010    fn a_plan_sorts_its_row_ids_by_what_happened_to_them() {
1011        let range = |start: u64, end: u64| json!({"start": start, "end": end});
1012        let removed = Uuid::now_v7();
1013        let wire: WireChangePlan = serde_json::from_value(json!({
1014            "fragments": [
1015                {"fragmentId": Uuid::now_v7(), "rowCount": 10, "change": {
1016                    "type": "retained",
1017                    "retained": {"patched": [range(3, 4)], "deleted": [range(0, 2)]}
1018                }},
1019                {"fragmentId": Uuid::now_v7(), "rowCount": 6, "change": {
1020                    "type": "added",
1021                    "added": {
1022                        "appended": [range(100, 103)],
1023                        "carried": [range(20, 23)],
1024                        "patched": [range(21, 22)]
1025                    }
1026                }}
1027            ],
1028            "removedFragmentIds": [removed],
1029            "retypedColumns": []
1030        }))
1031        .unwrap();
1032        let plan = ChangePlan::try_from(wire).unwrap();
1033        let rows = ChangedRows::from_plan(&plan);
1034        assert_eq!(rows.inserted, [100..103]);
1035        assert_eq!(rows.updated, [3..4, 21..22]);
1036        assert_eq!(rows.deleted, [0..2]);
1037        assert_eq!(rows.carried, [20..23]);
1038        assert!(rows.removed_fragments.contains(&removed));
1039    }
1040
1041    fn image(values: Vec<i64>, ids: Vec<u64>) -> RecordBatch {
1042        let schema = Schema::new(vec![
1043            Field::new("value", DataType::Int64, true),
1044            row_id_field(),
1045        ]);
1046        RecordBatch::try_new(
1047            Arc::new(schema),
1048            vec![
1049                Arc::new(Int64Array::from(values)),
1050                Arc::new(UInt64Array::from(ids)),
1051            ],
1052        )
1053        .unwrap()
1054    }
1055
1056    #[test]
1057    fn updates_pair_their_images_by_row_id() {
1058        let (paired, kinds) = pair_images(
1059            &image(vec![1, 2], vec![7, 9]),
1060            &image(vec![20, 10], vec![9, 7]),
1061        )
1062        .unwrap();
1063        assert_eq!(
1064            paired.column(0).as_primitive::<Int64Type>().values(),
1065            &[1, 10, 2, 20]
1066        );
1067        assert_eq!(row_ids(&paired).unwrap().values(), &[7, 7, 9, 9]);
1068        assert_eq!(
1069            kinds
1070                .as_string::<i32>()
1071                .iter()
1072                .flatten()
1073                .collect::<Vec<_>>(),
1074            [
1075                "update_before",
1076                "update_after",
1077                "update_before",
1078                "update_after"
1079            ]
1080        );
1081        assert!(pair_images(&image(vec![1], vec![7]), &image(vec![2], vec![8])).is_err());
1082        assert!(pair_images(&image(vec![1], vec![7]), &image(vec![2, 3], vec![7, 8])).is_err());
1083    }
1084
1085    #[test]
1086    fn dropped_fragments_lose_their_carried_rows() {
1087        let kept = without_rows(&image(vec![1, 2, 3], vec![20, 21, 30]), &[20..22]).unwrap();
1088        assert_eq!(row_ids(&kept).unwrap().values(), &[30]);
1089    }
1090
1091    #[test]
1092    fn change_reads_set_their_own_row_ids() {
1093        let keys = ChangeOptions {
1094            images: ChangeImages::Keys,
1095            ..Default::default()
1096        };
1097        assert!(check_change_options(&keys).is_ok());
1098        let limited = ChangeOptions {
1099            scan: ScanOptions {
1100                limit: Some(1),
1101                ..Default::default()
1102            },
1103            ..Default::default()
1104        };
1105        assert!(check_change_options(&limited).is_err());
1106        let renamed = ChangeOptions {
1107            scan: ScanOptions {
1108                row_id_column: Some("id".into()),
1109                ..Default::default()
1110            },
1111            ..Default::default()
1112        };
1113        assert!(check_change_options(&renamed).is_err());
1114    }
1115}