1use 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
34pub const CHANGE_KIND_COLUMN: &str = "s:change_kind";
37
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub enum ChangeKind {
41 Insert,
43 Delete,
45 UpdateBefore,
47 UpdateAfter,
49}
50
51impl ChangeKind {
52 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#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct ChangePlan {
66 pub fragments: Vec<FragmentChange>,
68 pub removed_fragment_ids: Vec<Uuid>,
71 pub retyped_columns: Vec<String>,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct FragmentChange {
79 pub fragment_id: Uuid,
81 pub row_count: u64,
83 pub change: FragmentDelta,
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum FragmentDelta {
91 Retained {
93 patched: Vec<Range<u64>>,
95 deleted: Vec<Range<u64>>,
97 },
98 Added {
100 appended: Vec<Range<u64>>,
102 carried: Vec<Range<u64>>,
104 patched: Vec<Range<u64>>,
106 },
107}
108
109impl TryFrom<WireChangePlan> for ChangePlan {
110 type Error = Error;
111
112 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#[derive(Clone, Debug, Default)]
155pub struct ChangeOptions {
156 pub scan: ScanOptions,
159 pub images: ChangeImages,
161}
162
163#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
166pub enum ChangeImages {
167 Keys,
170 #[default]
173 After,
174 BeforeAndAfter,
177}
178
179pub struct TableCommitChanges {
182 pub commit_id: ProjectCommitId,
184 pub since: Table,
186 pub table: Table,
188 pub changes: Scan,
190}
191
192impl Table {
193 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 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 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 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(¤t.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
364fn 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
421struct ChangedRows {
422 inserted: Vec<Range<u64>>,
423 updated: Vec<Range<u64>>,
424 deleted: Vec<Range<u64>>,
426 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
465const RANGES_PER_READ: usize = 4096;
467const PAIRED_ROWS: u64 = 65_536;
469const ID_BATCH_ROWS: u64 = 65_536;
471
472#[derive(Clone, Copy, Debug, Eq, PartialEq)]
474enum Image {
475 Before,
476 After,
477}
478
479#[derive(Clone, Copy, Debug, Eq, PartialEq)]
481enum Returns {
482 Values,
484 Ids,
486}
487
488type Batches = BoxStream<'static, Result<RecordBatch, Error>>;
489
490struct ChangeReader {
492 after: Table,
493 before: Table,
494 images: ChangeImages,
495 scan: ScanOptions,
497 rows: ChangedRows,
498 schema: SchemaRef,
500 filter_clauses: Vec<FilterClause>,
501}
502
503impl ChangeReader {
504 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 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 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 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 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 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 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 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 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 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 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 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
804struct 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
823fn 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
831fn 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
840fn 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
856fn 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
865fn 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
912fn 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
925fn 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
931fn 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
945fn 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
952fn 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}