Skip to main content

spiral/
merge.rs

1//! Merging source rows into a table by key, as one version.
2//!
3//! The join runs in the host: the source is held in memory, keyed by its `on` columns in
4//! Arrow's row format, and the table's keys stream past it. Matched rows become patches or
5//! deletions and unmatched source rows become appended rows, all in one [`TableWrite`].
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use arrow_array::cast::AsArray as _;
11use arrow_array::types::{
12    Int8Type, Int16Type, Int32Type, Int64Type, UInt8Type, UInt16Type, UInt32Type, UInt64Type,
13};
14use arrow_array::{Array, ArrayRef, ArrowPrimitiveType, RecordBatch, UInt32Array};
15use arrow_row::{RowConverter, Rows, SortField};
16use arrow_schema::{DataType, Field, Schema, SchemaRef};
17use arrow_select::concat::concat_batches;
18use arrow_select::take::take;
19use futures::{StreamExt as _, TryStreamExt as _, stream};
20use tokio::sync::mpsc;
21use tokio::task::{JoinHandle, spawn};
22use vortex_array::expr::{
23    Expression as VortexExpression, and, and_collect, col, gt_eq, lit, lt_eq,
24};
25use vortex_array::scalar::Scalar;
26
27use crate::scandal::RecordBatchStream;
28use crate::scandal::error::ScandalError;
29use crate::scandal::types::FragmentReceipt;
30use crate::scandal_backend::{RowEdit, reader_stream};
31use crate::table_write::ROW_ID_COLUMN;
32use crate::{Error, Expression, IntoArrowStream, ScanOptions, Table, TableWrite};
33
34/// What a merge does with each kind of row.
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct MergeActions {
37    /// What happens to a table row whose key a source row has.
38    pub matched: Matched,
39    /// The columns a match changes; every source column but the keys when absent.
40    pub columns: Option<Vec<String>>,
41    /// What happens to a source row whose key no table row has.
42    pub not_matched: NotMatched,
43    /// What happens to a table row whose key no source row has.
44    pub not_matched_by_source: BySource,
45}
46
47impl Default for MergeActions {
48    /// An upsert: patch matched rows, insert unmatched source rows, keep the other table rows.
49    fn default() -> Self {
50        Self {
51            matched: Matched::Patch,
52            columns: None,
53            not_matched: NotMatched::Insert,
54            not_matched_by_source: BySource::Keep,
55        }
56    }
57}
58
59/// What a merge does with a table row whose key a source row has.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum Matched {
62    /// Record the source row's values beside the data.
63    Patch,
64    /// Delete the table row.
65    Delete,
66    /// Leave the table row as it is.
67    Ignore,
68}
69
70/// What a merge does with a source row whose key no table row has.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum NotMatched {
73    /// Append the source row, which must carry the table's full schema.
74    Insert,
75    /// Leave the source row out.
76    Ignore,
77}
78
79/// What a merge does with a table row whose key no source row has.
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum BySource {
82    /// Leave the table row as it is.
83    Keep,
84    /// Delete the table row.
85    Delete,
86}
87
88/// Rows a merge changed, and the version with the changes.
89#[derive(Clone, Debug)]
90pub struct MergedRows {
91    /// The version with the changes; the original table when nothing changed.
92    pub table: Table,
93    /// Source rows appended.
94    pub inserted: u64,
95    /// Table rows whose values a match changed.
96    pub updated: u64,
97    /// Table rows deleted.
98    pub deleted: u64,
99}
100
101impl Table {
102    /// Write the next version of this table with `source` merged into it by the `on` columns,
103    /// as `actions` say: one version with every patch, deletion and appended row.
104    ///
105    /// The source is held in memory, so a merge suits batches of upserts; join larger sources in
106    /// DataFusion. Duplicate source keys are an error, and null keys match nothing. One source
107    /// row may match several table rows and updates all of them. A matched row whose values
108    /// are unchanged is not written. Unless unmatched table rows are deleted, the table scan is
109    /// pruned by the bounds of the source's integer and string keys. When nothing changes,
110    /// nothing is written and the result holds this table.
111    pub async fn merge_rows(
112        &self,
113        source: impl IntoArrowStream,
114        on: &[&str],
115        actions: MergeActions,
116    ) -> Result<MergedRows, Error> {
117        let reader = source.into_arrow_stream()?;
118        let source_schema = reader.schema();
119        let batches = reader_stream(reader).try_collect::<Vec<_>>().await?;
120        let source = concat_batches(&source_schema, &batches).map_err(Error::arrow)?;
121        let schema = self.schema().await?;
122        let merge = Merge::new(&schema, source, on, &actions)?;
123        merge.write(self).await
124    }
125}
126
127/// A merge's source rows, keyed by their `on` columns, and the actions that apply to them.
128struct Merge {
129    source: RecordBatch,
130    on: Vec<String>,
131    keys: RowConverter,
132    /// The source row of each non-null key, by the key's row format.
133    index: HashMap<Box<[u8]>, usize>,
134    matched: Matched,
135    not_matched: NotMatched,
136    by_source: BySource,
137    /// The columns a match updates, with the table's fields, and their source values in the
138    /// row format, which compares them with the table's values.
139    updates: Option<Updates>,
140    /// The table's schema, which inserted rows take.
141    table_schema: SchemaRef,
142}
143
144/// The columns a patch of matched rows writes.
145struct Updates {
146    /// The row ID column and the updated columns, with the table's fields.
147    schema: SchemaRef,
148    converter: RowConverter,
149    values: Rows,
150}
151
152/// What a merge writes for one batch of table rows.
153#[derive(Debug, Default)]
154struct BatchEdits {
155    /// Row IDs and new values of matched rows whose values change.
156    updates: Option<RecordBatch>,
157    /// Row IDs of the rows to delete.
158    deletions: Option<RecordBatch>,
159}
160
161impl Merge {
162    /// Check `source` against the table's `schema` and the `actions`, and key its rows.
163    fn new(
164        schema: &Schema,
165        source: RecordBatch,
166        on: &[&str],
167        actions: &MergeActions,
168    ) -> Result<Self, Error> {
169        if on.is_empty() {
170            return Err(Error::invalid(
171                "a merge matches rows on at least one column",
172            ));
173        }
174        if u32::try_from(source.num_rows()).is_err() {
175            return Err(Error::invalid("a merge source holds fewer than 2^32 rows"));
176        }
177        let source_schema = source.schema();
178        for key in on {
179            same_type(schema, &source_schema, key)?;
180        }
181        let key_columns = columns(&source, on)?;
182        let keys = converter(&key_columns)?;
183        let index = key_index(&keys, &key_columns)?;
184        let updates = match actions.matched {
185            Matched::Patch => Updates::new(schema, &source, on, actions)?,
186            Matched::Delete | Matched::Ignore => None,
187        };
188        if actions.not_matched == NotMatched::Insert {
189            check_insertable(schema, &source_schema)?;
190        }
191        Ok(Self {
192            source,
193            on: on.iter().map(|key| (*key).to_owned()).collect(),
194            keys,
195            index,
196            matched: actions.matched,
197            not_matched: actions.not_matched,
198            by_source: actions.not_matched_by_source,
199            updates,
200            table_schema: Arc::new(schema.clone()),
201        })
202    }
203
204    /// Scan the table's keys, compared columns and row IDs, write what each batch changes, then
205    /// append the unmatched source rows.
206    async fn write(self, table: &Table) -> Result<MergedRows, Error> {
207        let mut write = table.writer().await?;
208        let mut matched_sources = vec![false; self.source.num_rows()];
209        let (mut updated, mut deleted) = (0, 0);
210        if self.reads_table() {
211            let scan = table
212                .scan(ScanOptions {
213                    columns: Some(self.scanned_columns()),
214                    filter: self.key_filter()?,
215                    row_id_column: Some(ROW_ID_COLUMN.to_owned()),
216                    ..Default::default()
217                })
218                .await?;
219            let mut updates = self
220                .updates
221                .as_ref()
222                .map(|updates| LazyPart::new(RowEdit::Patch, Arc::clone(&updates.schema)));
223            let mut deletions = LazyPart::new(RowEdit::Delete, row_id_schema());
224            let mut batches = scan.into_stream();
225            while let Some(batch) = batches.next_batch().await {
226                let edits = self.edit_batch(&batch?, &mut matched_sources)?;
227                if let (Some(part), Some(rows)) = (updates.as_mut(), edits.updates) {
228                    updated += rows.num_rows() as u64;
229                    part.send(&mut write, rows).await?;
230                }
231                if let Some(rows) = edits.deletions {
232                    deleted += rows.num_rows() as u64;
233                    deletions.send(&mut write, rows).await?;
234                }
235            }
236            if let Some(part) = updates {
237                write.add_receipts(part.finish().await?);
238            }
239            write.add_receipts(deletions.finish().await?);
240        }
241        let inserts = self.inserted_rows(&matched_sources)?;
242        let inserted = inserts.as_ref().map_or(0, |rows| rows.num_rows() as u64);
243        if let Some(rows) = inserts {
244            write.append(rows).await?;
245        }
246        if inserted + updated + deleted == 0 {
247            return Ok(MergedRows {
248                table: table.clone(),
249                inserted,
250                updated,
251                deleted,
252            });
253        }
254        Ok(MergedRows {
255            table: write.finish().await?,
256            inserted,
257            updated,
258            deleted,
259        })
260    }
261
262    /// Whether any table row can be changed or can tell a source row it matched.
263    fn reads_table(&self) -> bool {
264        let changes_matches = self.matched == Matched::Delete || self.updates.is_some();
265        self.by_source == BySource::Delete
266            || (!self.index.is_empty()
267                && (changes_matches || self.not_matched == NotMatched::Insert))
268    }
269
270    /// The keys, then the updated columns that are not keys.
271    fn scanned_columns(&self) -> Vec<String> {
272        let mut scanned = self.on.clone();
273        if let Some(updates) = &self.updates {
274            scanned.extend(
275                updates
276                    .updated_columns()
277                    .filter(|column| !self.on.contains(column))
278                    .cloned(),
279            );
280        }
281        scanned
282    }
283
284    /// Bounds of the source's integer and string keys, which prune table files no source key
285    /// can match. A merge that deletes unmatched table rows reads them all.
286    fn key_filter(&self) -> Result<Option<Expression>, Error> {
287        if self.by_source == BySource::Delete {
288            return Ok(None);
289        }
290        let terms = self
291            .on
292            .iter()
293            .filter_map(|key| {
294                let column = self.source.column_by_name(key)?;
295                let (min, max) = key_bounds(column.as_ref())?;
296                Some(and(
297                    gt_eq(col(key.as_str()), min),
298                    lt_eq(col(key.as_str()), max),
299                ))
300            })
301            .collect::<Vec<_>>();
302        Ok(and_collect(terms).map(Expression::new).transpose()?)
303    }
304
305    /// The patches and deletions of one batch of table rows, marking the source
306    /// rows it matched in `matched_sources`.
307    fn edit_batch(
308        &self,
309        batch: &RecordBatch,
310        matched_sources: &mut [bool],
311    ) -> Result<BatchEdits, Error> {
312        let row_ids = batch
313            .column_by_name(ROW_ID_COLUMN)
314            .ok_or_else(|| Error::Protocol(format!("the scan returned no `{ROW_ID_COLUMN}`")))?;
315        let key_columns = columns(batch, &self.on)?;
316        let keys = self
317            .keys
318            .convert_columns(&key_columns)
319            .map_err(Error::arrow)?;
320        let (mut changed, mut sources, mut deleted) = (Vec::new(), Vec::new(), Vec::new());
321        for row in 0..batch.num_rows() {
322            let source = key_columns
323                .iter()
324                .all(|key| key.is_valid(row))
325                .then(|| self.index.get(keys.row(row).as_ref()))
326                .flatten();
327            let row = u32::try_from(row).map_err(Error::arrow)?;
328            match source {
329                Some(&source) => {
330                    matched_sources[source] = true;
331                    match self.matched {
332                        Matched::Patch => {
333                            changed.push(row);
334                            sources.push(source as u32);
335                        }
336                        Matched::Delete => deleted.push(row),
337                        Matched::Ignore => {}
338                    }
339                }
340                None if self.by_source == BySource::Delete => deleted.push(row),
341                None => {}
342            }
343        }
344        let updates = match &self.updates {
345            Some(updates) => {
346                updates.changed_rows(batch, row_ids, changed, sources, &self.source)?
347            }
348            None => None,
349        };
350        Ok(BatchEdits {
351            updates,
352            deletions: row_id_rows(row_ids, deleted)?,
353        })
354    }
355
356    /// The source rows to append: those no table row matched, in source order, with the
357    /// table's schema.
358    fn inserted_rows(&self, matched_sources: &[bool]) -> Result<Option<RecordBatch>, Error> {
359        if self.not_matched == NotMatched::Ignore {
360            return Ok(None);
361        }
362        let unmatched = (0..self.source.num_rows() as u32)
363            .filter(|row| !matched_sources[*row as usize])
364            .collect::<Vec<_>>();
365        if unmatched.is_empty() {
366            return Ok(None);
367        }
368        let indices = UInt32Array::from(unmatched);
369        let columns =
370            self.table_schema
371                .fields()
372                .iter()
373                .map(|field| {
374                    let column = self.source.column_by_name(field.name()).ok_or_else(|| {
375                        Error::invalid(format!("source has no `{}`", field.name()))
376                    })?;
377                    take(column.as_ref(), &indices, None).map_err(Error::arrow)
378                })
379                .collect::<Result<Vec<_>, _>>()?;
380        RecordBatch::try_new(Arc::clone(&self.table_schema), columns)
381            .map(Some)
382            .map_err(Error::arrow)
383    }
384}
385
386impl Updates {
387    /// The columns a match of `source` rows writes, or `None` when it writes none.
388    fn new(
389        schema: &Schema,
390        source: &RecordBatch,
391        on: &[&str],
392        actions: &MergeActions,
393    ) -> Result<Option<Self>, Error> {
394        let source_schema = source.schema();
395        let names = match &actions.columns {
396            Some(names) => {
397                if let Some(key) = names.iter().find(|name| on.contains(&name.as_str())) {
398                    return Err(Error::invalid(format!(
399                        "a merge does not update its key `{key}`"
400                    )));
401                }
402                names.clone()
403            }
404            None => source_schema
405                .fields()
406                .iter()
407                .map(|field| field.name().clone())
408                .filter(|name| !on.contains(&name.as_str()))
409                .collect(),
410        };
411        if names.is_empty() {
412            return Ok(None);
413        }
414        let mut fields = vec![row_id_field()];
415        for name in &names {
416            fields.push(Arc::new(same_type(schema, &source_schema, name)?.clone()));
417        }
418        let source_columns = columns(source, &names)?;
419        let converter = converter(&source_columns)?;
420        let values = converter
421            .convert_columns(&source_columns)
422            .map_err(Error::arrow)?;
423        Ok(Some(Self {
424            schema: Arc::new(Schema::new(fields)),
425            converter,
426            values,
427        }))
428    }
429
430    /// The updated columns, after the row ID column.
431    fn updated_columns(&self) -> impl Iterator<Item = &String> {
432        self.schema.fields()[1..].iter().map(|field| field.name())
433    }
434
435    /// The row IDs of the `table_rows` of `batch` whose values differ from their matching
436    /// `source_rows`, with the source's values.
437    fn changed_rows(
438        &self,
439        batch: &RecordBatch,
440        row_ids: &ArrayRef,
441        table_rows: Vec<u32>,
442        source_rows: Vec<u32>,
443        source: &RecordBatch,
444    ) -> Result<Option<RecordBatch>, Error> {
445        if table_rows.is_empty() {
446            return Ok(None);
447        }
448        let names = self.updated_columns().cloned().collect::<Vec<_>>();
449        let current = self
450            .converter
451            .convert_columns(&columns(batch, &names)?)
452            .map_err(Error::arrow)?;
453        let (table_rows, source_rows): (Vec<u32>, Vec<u32>) = table_rows
454            .into_iter()
455            .zip(source_rows)
456            .filter(|(table, source)| {
457                current.row(*table as usize) != self.values.row(*source as usize)
458            })
459            .unzip();
460        if table_rows.is_empty() {
461            return Ok(None);
462        }
463        let (table_rows, source_rows) = (
464            UInt32Array::from(table_rows),
465            UInt32Array::from(source_rows),
466        );
467        let mut values = vec![take(row_ids.as_ref(), &table_rows, None).map_err(Error::arrow)?];
468        for column in columns(source, &names)? {
469            values.push(take(column.as_ref(), &source_rows, None).map_err(Error::arrow)?);
470        }
471        RecordBatch::try_new(Arc::clone(&self.schema), values)
472            .map(Some)
473            .map_err(Error::arrow)
474    }
475}
476
477/// One part of an edit whose rows a scan produces. Its write request starts with its first
478/// rows, so a part without rows sends none, and runs beside the scan.
479struct LazyPart {
480    edit: RowEdit,
481    schema: SchemaRef,
482    request: Option<StartedPart>,
483}
484
485type StartedPart = (
486    mpsc::Sender<Result<RecordBatch, ScandalError>>,
487    JoinHandle<Result<Vec<FragmentReceipt>, Error>>,
488);
489
490/// Batches a part's request may hold before the scan waits for it.
491const PART_BUFFER: usize = 2;
492
493impl LazyPart {
494    fn new(edit: RowEdit, schema: SchemaRef) -> Self {
495        Self {
496            edit,
497            schema,
498            request: None,
499        }
500    }
501
502    /// Send `rows` to the part's request, starting it with the first rows.
503    async fn send(&mut self, write: &mut TableWrite, rows: RecordBatch) -> Result<(), Error> {
504        let (sender, request) = match self.request.take() {
505            Some(started) => started,
506            None => {
507                let (sender, receiver) = mpsc::channel(PART_BUFFER);
508                let request = write.start_edit(
509                    self.edit,
510                    Arc::clone(&self.schema),
511                    receiver_stream(receiver),
512                )?;
513                (sender, spawn(request))
514            }
515        };
516        if sender.send(Ok(rows)).await.is_err() {
517            return Err(match joined(request).await {
518                Err(error) => error,
519                Ok(_) => Error::Protocol("a write request ended before reading its rows".into()),
520            });
521        }
522        self.request = Some((sender, request));
523        Ok(())
524    }
525
526    /// End the part's rows and return the receipts its request wrote.
527    async fn finish(self) -> Result<Vec<FragmentReceipt>, Error> {
528        let Some((sender, request)) = self.request else {
529            return Ok(Vec::new());
530        };
531        drop(sender);
532        joined(request).await
533    }
534}
535
536async fn joined(
537    request: JoinHandle<Result<Vec<FragmentReceipt>, Error>>,
538) -> Result<Vec<FragmentReceipt>, Error> {
539    request
540        .await
541        .map_err(|error| Error::Protocol(format!("a write request stopped: {error}")))?
542}
543
544fn receiver_stream(
545    receiver: mpsc::Receiver<Result<RecordBatch, ScandalError>>,
546) -> RecordBatchStream {
547    stream::unfold(receiver, |mut receiver| async move {
548        receiver.recv().await.map(|batch| (batch, receiver))
549    })
550    .boxed()
551}
552
553fn row_id_field() -> Arc<Field> {
554    Arc::new(Field::new(ROW_ID_COLUMN, DataType::UInt64, false))
555}
556
557fn row_id_schema() -> SchemaRef {
558    Arc::new(Schema::new(vec![row_id_field()]))
559}
560
561/// The row IDs of `rows`, as rows to delete, or `None` when there are none.
562fn row_id_rows(row_ids: &ArrayRef, rows: Vec<u32>) -> Result<Option<RecordBatch>, Error> {
563    if rows.is_empty() {
564        return Ok(None);
565    }
566    let ids = take(row_ids.as_ref(), &UInt32Array::from(rows), None).map_err(Error::arrow)?;
567    RecordBatch::try_new(row_id_schema(), vec![ids])
568        .map(Some)
569        .map_err(Error::arrow)
570}
571
572/// The columns `names` of `batch`, in that order.
573fn columns(batch: &RecordBatch, names: &[impl AsRef<str>]) -> Result<Vec<ArrayRef>, Error> {
574    names
575        .iter()
576        .map(|name| {
577            batch
578                .column_by_name(name.as_ref())
579                .cloned()
580                .ok_or_else(|| Error::invalid(format!("no column `{}`", name.as_ref())))
581        })
582        .collect()
583}
584
585/// The table's field `column`, which the source must hold with the same type.
586fn same_type<'a>(table: &'a Schema, source: &Schema, column: &str) -> Result<&'a Field, Error> {
587    let field = table
588        .field_with_name(column)
589        .map_err(|_| Error::invalid(format!("the table has no column `{column}`")))?;
590    let given = source
591        .field_with_name(column)
592        .map_err(|_| Error::invalid(format!("the merge source has no column `{column}`")))?;
593    if field.data_type() != given.data_type() {
594        return Err(Error::invalid(format!(
595            "the merge source's `{column}` holds {}, not the stored {}",
596            given.data_type(),
597            field.data_type()
598        )));
599    }
600    Ok(field)
601}
602
603/// Check that the source holds exactly the table's columns with their types, so its rows can
604/// be appended.
605fn check_insertable(table: &Schema, source: &Schema) -> Result<(), Error> {
606    for field in table.fields() {
607        same_type(table, source, field.name())?;
608    }
609    if let Some(extra) = source
610        .fields()
611        .iter()
612        .find(|field| table.field_with_name(field.name()).is_err())
613    {
614        return Err(Error::invalid(format!(
615            "inserting needs the table's columns only; the source also holds `{}`",
616            extra.name()
617        )));
618    }
619    Ok(())
620}
621
622fn converter(columns: &[ArrayRef]) -> Result<RowConverter, Error> {
623    RowConverter::new(
624        columns
625            .iter()
626            .map(|column| SortField::new(column.data_type().clone()))
627            .collect(),
628    )
629    .map_err(Error::arrow)
630}
631
632/// The source row of each non-null key; a key that two rows hold is an error.
633fn key_index(
634    converter: &RowConverter,
635    keys: &[ArrayRef],
636) -> Result<HashMap<Box<[u8]>, usize>, Error> {
637    let rows = converter.convert_columns(keys).map_err(Error::arrow)?;
638    let mut index = HashMap::with_capacity(rows.num_rows());
639    for row in 0..rows.num_rows() {
640        if !keys.iter().all(|key| key.is_valid(row)) {
641            continue;
642        }
643        if let Some(earlier) = index.insert(Box::from(rows.row(row).as_ref()), row) {
644            return Err(Error::invalid(format!(
645                "the merge source holds one key in rows {earlier} and {row}"
646            )));
647        }
648    }
649    Ok(index)
650}
651
652/// The least and greatest non-null values of an integer or string key, as literals.
653fn key_bounds(column: &dyn Array) -> Option<(VortexExpression, VortexExpression)> {
654    match column.data_type() {
655        DataType::Int8 => primitive_bounds::<Int8Type>(column),
656        DataType::Int16 => primitive_bounds::<Int16Type>(column),
657        DataType::Int32 => primitive_bounds::<Int32Type>(column),
658        DataType::Int64 => primitive_bounds::<Int64Type>(column),
659        DataType::UInt8 => primitive_bounds::<UInt8Type>(column),
660        DataType::UInt16 => primitive_bounds::<UInt16Type>(column),
661        DataType::UInt32 => primitive_bounds::<UInt32Type>(column),
662        DataType::UInt64 => primitive_bounds::<UInt64Type>(column),
663        DataType::Utf8 => string_bounds(column.as_string::<i32>().iter()),
664        DataType::LargeUtf8 => string_bounds(column.as_string::<i64>().iter()),
665        DataType::Utf8View => string_bounds(column.as_string_view().iter()),
666        _ => None,
667    }
668}
669
670fn primitive_bounds<T: ArrowPrimitiveType>(
671    column: &dyn Array,
672) -> Option<(VortexExpression, VortexExpression)>
673where
674    T::Native: Into<Scalar> + PartialOrd,
675{
676    let (min, max) = bounds(column.as_primitive::<T>().iter().flatten())?;
677    Some((lit(min), lit(max)))
678}
679
680fn string_bounds<'a>(
681    values: impl Iterator<Item = Option<&'a str>>,
682) -> Option<(VortexExpression, VortexExpression)> {
683    let (min, max) = bounds(values.flatten())?;
684    Some((lit(min), lit(max)))
685}
686
687fn bounds<T: PartialOrd + Copy>(values: impl Iterator<Item = T>) -> Option<(T, T)> {
688    values.fold(None, |bounds, value| match bounds {
689        None => Some((value, value)),
690        Some((min, max)) => Some((
691            if value < min { value } else { min },
692            if value > max { value } else { max },
693        )),
694    })
695}
696
697#[cfg(test)]
698mod tests {
699    use arrow_array::{Int64Array, StringArray, UInt64Array};
700
701    use super::*;
702
703    fn table_schema() -> Schema {
704        Schema::new(vec![
705            Field::new("id", DataType::Int64, true),
706            Field::new("label", DataType::Utf8, true),
707        ])
708    }
709
710    fn source(ids: Vec<Option<i64>>, labels: Vec<&str>) -> RecordBatch {
711        RecordBatch::try_new(
712            Arc::new(table_schema()),
713            vec![
714                Arc::new(Int64Array::from(ids)),
715                Arc::new(StringArray::from(labels)),
716            ],
717        )
718        .unwrap()
719    }
720
721    /// Table rows as a scan with row IDs returns them.
722    fn table_rows(ids: Vec<Option<i64>>, labels: Vec<&str>, row_ids: Vec<u64>) -> RecordBatch {
723        let schema = Schema::new(vec![
724            Field::new("id", DataType::Int64, true),
725            Field::new("label", DataType::Utf8, true),
726            Field::new(ROW_ID_COLUMN, DataType::UInt64, false),
727        ]);
728        RecordBatch::try_new(
729            Arc::new(schema),
730            vec![
731                Arc::new(Int64Array::from(ids)),
732                Arc::new(StringArray::from(labels)),
733                Arc::new(UInt64Array::from(row_ids)),
734            ],
735        )
736        .unwrap()
737    }
738
739    fn row_ids(rows: &RecordBatch) -> Vec<u64> {
740        rows.column_by_name(ROW_ID_COLUMN)
741            .unwrap()
742            .as_primitive::<UInt64Type>()
743            .values()
744            .to_vec()
745    }
746
747    #[test]
748    fn matches_patch_changed_rows_and_insert_unmatched_source_rows() {
749        let merge = Merge::new(
750            &table_schema(),
751            source(
752                vec![Some(1), Some(2), None, Some(4)],
753                vec!["a", "B", "n", "d"],
754            ),
755            &["id"],
756            &MergeActions::default(),
757        )
758        .unwrap();
759        let mut matched = vec![false; 4];
760        let edits = merge
761            .edit_batch(
762                &table_rows(
763                    vec![Some(1), Some(2), Some(2), None, Some(3)],
764                    vec!["a", "b", "x", "n", "c"],
765                    vec![10, 11, 12, 13, 14],
766                ),
767                &mut matched,
768            )
769            .unwrap();
770        // Row 10 already holds "a"; both rows keyed 2 take "B"; null keys match nothing.
771        let updates = edits.updates.unwrap();
772        assert_eq!(row_ids(&updates), [11, 12]);
773        assert_eq!(
774            updates
775                .column(1)
776                .as_string::<i32>()
777                .iter()
778                .collect::<Vec<_>>(),
779            [Some("B"), Some("B")]
780        );
781        assert!(edits.deletions.is_none());
782        assert_eq!(matched, [true, true, false, false]);
783        let inserted = merge.inserted_rows(&matched).unwrap().unwrap();
784        assert_eq!(
785            inserted
786                .column(1)
787                .as_string::<i32>()
788                .iter()
789                .collect::<Vec<_>>(),
790            [Some("n"), Some("d")]
791        );
792        assert_eq!(inserted.schema().as_ref(), &table_schema());
793    }
794
795    #[test]
796    fn deletes_follow_the_matched_and_by_source_actions() {
797        let actions = MergeActions {
798            matched: Matched::Delete,
799            columns: None,
800            not_matched: NotMatched::Ignore,
801            not_matched_by_source: BySource::Delete,
802        };
803        let merge = Merge::new(
804            &table_schema(),
805            source(vec![Some(1)], vec!["a"]),
806            &["id"],
807            &actions,
808        )
809        .unwrap();
810        assert!(merge.key_filter().unwrap().is_none());
811        let mut matched = vec![false];
812        let edits = merge
813            .edit_batch(
814                &table_rows(
815                    vec![Some(1), Some(2), None],
816                    vec!["a", "b", "c"],
817                    vec![7, 8, 9],
818                ),
819                &mut matched,
820            )
821            .unwrap();
822        assert!(edits.updates.is_none());
823        assert_eq!(row_ids(&edits.deletions.unwrap()), [7, 8, 9]);
824        assert!(merge.inserted_rows(&matched).unwrap().is_none());
825    }
826
827    #[test]
828    fn sources_with_duplicate_keys_or_other_types_are_refused() {
829        let actions = MergeActions::default();
830        let duplicate = source(vec![Some(1), Some(1)], vec!["a", "b"]);
831        assert!(Merge::new(&table_schema(), duplicate, &["id"], &actions).is_err());
832        let nulls = source(vec![None, None], vec!["a", "b"]);
833        assert!(Merge::new(&table_schema(), nulls, &["id"], &actions).is_ok());
834        let retyped = Schema::new(vec![
835            Field::new("id", DataType::Int32, true),
836            Field::new("label", DataType::Utf8, true),
837        ]);
838        assert!(
839            Merge::new(
840                &retyped,
841                source(vec![Some(1)], vec!["a"]),
842                &["id"],
843                &actions
844            )
845            .is_err()
846        );
847        let keyed_update = MergeActions {
848            columns: Some(vec!["id".to_owned()]),
849            ..MergeActions::default()
850        };
851        assert!(
852            Merge::new(
853                &table_schema(),
854                source(vec![Some(1)], vec!["a"]),
855                &["id"],
856                &keyed_update
857            )
858            .is_err()
859        );
860    }
861
862    #[test]
863    fn integer_and_string_keys_bound_the_scan() {
864        let merge = Merge::new(
865            &table_schema(),
866            source(vec![Some(4), None, Some(-2)], vec!["a", "b", "c"]),
867            &["id", "label"],
868            &MergeActions {
869                matched: Matched::Ignore,
870                ..MergeActions::default()
871            },
872        )
873        .unwrap();
874        assert!(merge.key_filter().unwrap().is_some());
875        assert_eq!(merge.scanned_columns(), ["id", "label"]);
876        let ids = Int64Array::from(vec![Some(4), None, Some(-2)]);
877        let (min, max) = key_bounds(&ids).unwrap();
878        assert_eq!((min, max), (lit(-2i64), lit(4i64)));
879        assert!(key_bounds(&Int64Array::from(vec![None::<i64>])).is_none());
880    }
881}