proof_of_sql/base/commitment/
table_commitment.rs

1use super::{
2    committable_column::CommittableColumn, AppendColumnCommitmentsError, ColumnCommitments,
3    ColumnCommitmentsMismatch, Commitment, DuplicateIdents,
4};
5use crate::base::{
6    database::{ColumnField, CommitmentAccessor, OwnedTable, TableRef},
7    scalar::Scalar,
8};
9use alloc::vec::Vec;
10use core::ops::Range;
11use serde::{Deserialize, Serialize};
12use snafu::Snafu;
13use sqlparser::ast::Ident;
14
15/// Cannot create a [`TableCommitment`] with a negative range.
16#[derive(Debug, Snafu)]
17#[snafu(display("cannot create a TableCommitment with a negative range"))]
18pub struct NegativeRange;
19
20/// Cannot create a [`TableCommitment`] from columns of mixed length.
21#[derive(Debug, Snafu)]
22#[snafu(display("cannot create a TableCommitment from columns of mixed length"))]
23pub struct MixedLengthColumns;
24
25/// Errors that can occur when trying to create or extend a [`TableCommitment`] from columns.
26#[derive(Debug, Snafu)]
27pub enum TableCommitmentFromColumnsError {
28    /// Cannot construct [`TableCommitment`] from columns of mixed length.
29    #[snafu(transparent)]
30    MixedLengthColumns {
31        /// The underlying source error
32        source: MixedLengthColumns,
33    },
34    /// Cannot construct [`TableCommitment`] from columns with duplicate idents.
35    #[snafu(transparent)]
36    DuplicateIdents {
37        /// The underlying source error
38        source: DuplicateIdents,
39    },
40}
41
42/// Errors that can occur when attempting to append rows to a [`TableCommitment`].
43#[derive(Debug, Snafu)]
44pub enum AppendTableCommitmentError {
45    /// Cannot append columns of mixed length to existing [`TableCommitment`].
46    #[snafu(transparent)]
47    MixedLengthColumns {
48        /// The underlying source error
49        source: MixedLengthColumns,
50    },
51    /// Encountered error when appending internal [`ColumnCommitments`].
52    #[snafu(transparent)]
53    AppendColumnCommitments {
54        /// The underlying source error
55        source: AppendColumnCommitmentsError,
56    },
57}
58
59/// Errors that can occur when performing arithmetic on [`TableCommitment`]s.
60#[derive(Debug, Snafu)]
61pub enum TableCommitmentArithmeticError {
62    /// Cannot perform arithmetic on columns with mismatched metadata.
63    #[snafu(transparent)]
64    ColumnMismatch {
65        /// The underlying source error
66        source: ColumnCommitmentsMismatch,
67    },
68    /// Cannot perform [`TableCommitment`] arithmetic that would result in a negative range.
69    #[snafu(transparent)]
70    NegativeRange {
71        /// The underlying source error
72        source: NegativeRange,
73    },
74    /// Cannot perform arithmetic for noncontiguous table commitments.
75    #[snafu(display(
76        "cannot perform table commitment arithmetic for noncontiguous table commitments"
77    ))]
78    NonContiguous,
79}
80
81/// Commitment for an entire table, with column and table metadata.
82///
83/// Unlike [`ColumnCommitments`], all columns in this commitment must have the same length.
84#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct TableCommitment<C>
86where
87    C: Commitment,
88{
89    range: Range<usize>,
90    column_commitments: ColumnCommitments<C>,
91}
92
93impl<C: Commitment> TableCommitment<C> {
94    /// Create a new [`TableCommitment`] for a table from a commitment accessor.
95    #[expect(
96        clippy::missing_panics_doc,
97        reason = "The assertion ensures that from_accessor should not create columns with a negative range"
98    )]
99    pub fn from_accessor_with_max_bounds(
100        table_ref: &TableRef,
101        columns: &[ColumnField],
102        accessor: &impl CommitmentAccessor<C>,
103    ) -> Self {
104        let length = accessor.get_length(table_ref);
105        let offset = accessor.get_offset(table_ref);
106        Self::try_new(
107            ColumnCommitments::from_accessor_with_max_bounds(table_ref, columns, accessor),
108            offset..offset + length,
109        )
110        .expect("from_accessor should not create columns with a negative range")
111    }
112
113    #[cfg(test)]
114    pub(super) fn column_commitments_mut(&mut self) -> &mut ColumnCommitments<C> {
115        &mut self.column_commitments
116    }
117
118    /// Construct a new [`TableCommitment`].
119    ///
120    /// Will error if the range is "negative", i.e. if its end < start.
121    pub fn try_new(
122        column_commitments: ColumnCommitments<C>,
123        range: Range<usize>,
124    ) -> Result<Self, NegativeRange> {
125        if range.start <= range.end {
126            Ok(TableCommitment {
127                range,
128                column_commitments,
129            })
130        } else {
131            Err(NegativeRange)
132        }
133    }
134
135    /// Returns a reference to this type's internal [`ColumnCommitments`].
136    #[must_use]
137    pub fn column_commitments(&self) -> &ColumnCommitments<C> {
138        &self.column_commitments
139    }
140
141    /// Returns a reference to the range of rows this type commits to.
142    #[must_use]
143    pub fn range(&self) -> &Range<usize> {
144        &self.range
145    }
146
147    /// Returns the number of columns in the committed table.
148    #[must_use]
149    pub fn num_columns(&self) -> usize {
150        self.column_commitments.len()
151    }
152
153    /// Returns the number of rows that have been committed to.
154    #[must_use]
155    pub fn num_rows(&self) -> usize {
156        self.range.len()
157    }
158
159    /// Returns a [`TableCommitment`] to the provided columns with the given row offset.
160    ///
161    /// Provided columns must have the same length and no duplicate idents.
162    pub fn try_from_columns_with_offset<'a, COL>(
163        columns: impl IntoIterator<Item = (&'a Ident, COL)>,
164        offset: usize,
165        setup: &C::PublicSetup<'_>,
166    ) -> Result<TableCommitment<C>, TableCommitmentFromColumnsError>
167    where
168        COL: Into<CommittableColumn<'a>>,
169    {
170        let (identifiers, committable_columns): (Vec<&Ident>, Vec<CommittableColumn>) = columns
171            .into_iter()
172            .map(|(identifier, column)| (identifier, column.into()))
173            .unzip();
174
175        let num_rows = num_rows_of_columns(&committable_columns)?;
176
177        let column_commitments = ColumnCommitments::try_from_columns_with_offset(
178            identifiers.into_iter().zip(committable_columns.into_iter()),
179            offset,
180            setup,
181        )?;
182
183        Ok(TableCommitment {
184            column_commitments,
185            range: offset..offset + num_rows,
186        })
187    }
188
189    /// Returns a [`TableCommitment`] to the provided table with the given row offset.
190    #[expect(
191        clippy::missing_panics_doc,
192        reason = "since OwnedTables cannot have columns of mixed length or duplicate idents"
193    )]
194    pub fn from_owned_table_with_offset<S>(
195        owned_table: &OwnedTable<S>,
196        offset: usize,
197        setup: &C::PublicSetup<'_>,
198    ) -> TableCommitment<C>
199    where
200        S: Scalar,
201    {
202        Self::try_from_columns_with_offset(owned_table.inner_table(), offset, setup)
203            .expect("OwnedTables cannot have columns of mixed length or duplicate idents")
204    }
205
206    /// Append rows of data from the provided columns to the existing [`TableCommitment`].
207    ///
208    /// The row offset is assumed to be the end of the [`TableCommitment`]'s current range.
209    ///
210    /// Will error on a variety of mismatches, or if the provided columns have mixed length.
211    pub fn try_append_rows<'a, COL>(
212        &mut self,
213        columns: impl IntoIterator<Item = (&'a Ident, COL)>,
214        setup: &C::PublicSetup<'_>,
215    ) -> Result<(), AppendTableCommitmentError>
216    where
217        COL: Into<CommittableColumn<'a>>,
218    {
219        let (identifiers, committable_columns): (Vec<&Ident>, Vec<CommittableColumn>) = columns
220            .into_iter()
221            .map(|(identifier, column)| (identifier, column.into()))
222            .unzip();
223
224        let num_rows = num_rows_of_columns(&committable_columns)?;
225
226        self.column_commitments.try_append_rows_with_offset(
227            identifiers.into_iter().zip(committable_columns.into_iter()),
228            self.range.end,
229            setup,
230        )?;
231        self.range.end += num_rows;
232
233        Ok(())
234    }
235
236    /// Append data of the provided table to the exiting [`TableCommitment`].
237    ///
238    /// Will error on a variety of mismatches.
239    /// See [`ColumnCommitmentsMismatch`] for an enumeration of these errors.
240    /// # Panics
241    /// Panics if `owned_table` has duplicate idents.
242    /// Panics if `owned_table` contains columns of mixed length.
243    pub fn append_owned_table<S>(
244        &mut self,
245        owned_table: &OwnedTable<S>,
246        setup: &C::PublicSetup<'_>,
247    ) -> Result<(), ColumnCommitmentsMismatch>
248    where
249        S: Scalar,
250    {
251        self.try_append_rows(owned_table.inner_table(), setup)
252            .map_err(|e| match e {
253                AppendTableCommitmentError::AppendColumnCommitments { source: e } => match e {
254                    AppendColumnCommitmentsError::Mismatch { source: e } => e,
255                    AppendColumnCommitmentsError::DuplicateIdents { .. } => {
256                        panic!("OwnedTables cannot have duplicate idents");
257                    }
258                },
259                AppendTableCommitmentError::MixedLengthColumns { .. } => {
260                    panic!("OwnedTables cannot have columns of mixed length");
261                }
262            })
263    }
264
265    /// Add new columns to this [`TableCommitment`].
266    ///
267    /// Columns must have the same length as the current commitment and no duplicate idents.
268    pub fn try_extend_columns<'a, COL>(
269        &mut self,
270        columns: impl IntoIterator<Item = (&'a Ident, COL)>,
271        setup: &C::PublicSetup<'_>,
272    ) -> Result<(), TableCommitmentFromColumnsError>
273    where
274        COL: Into<CommittableColumn<'a>>,
275    {
276        let num_rows = self.range.len();
277
278        let (identifiers, committable_columns): (Vec<&Ident>, Vec<CommittableColumn>) = columns
279            .into_iter()
280            .map(|(identifier, column)| (identifier, column.into()))
281            .unzip();
282
283        let num_rows_of_new_columns = num_rows_of_columns(&committable_columns)?;
284        if num_rows_of_new_columns != num_rows {
285            Err(MixedLengthColumns)?;
286        }
287
288        self.column_commitments.try_extend_columns_with_offset(
289            identifiers.into_iter().zip(committable_columns.into_iter()),
290            self.range.start,
291            setup,
292        )?;
293
294        Ok(())
295    }
296
297    /// Add two [`TableCommitment`]s together.
298    ///
299    /// `self` must end where `other` begins, or vice versa.
300    /// Otherwise, [`TableCommitmentArithmeticError::NonContiguous`] is returned.
301    ///
302    /// This will also error on a variety of mismatches.
303    /// See [`ColumnCommitmentsMismatch`] for an enumeration of these errors.
304    pub fn try_add(self, other: Self) -> Result<Self, TableCommitmentArithmeticError>
305    where
306        Self: Sized,
307    {
308        let range = if self.range.end == other.range.start {
309            self.range.start..other.range.end
310        } else if other.range.end == self.range.start {
311            other.range.start..self.range.end
312        } else {
313            return Err(TableCommitmentArithmeticError::NonContiguous);
314        };
315
316        let column_commitments = self.column_commitments.try_add(other.column_commitments)?;
317
318        Ok(TableCommitment {
319            range,
320            column_commitments,
321        })
322    }
323
324    /// Subtract two [`TableCommitment`]s.
325    ///
326    /// `self` and `other` must begin at the same row number or end at the same row number.
327    /// Otherwise, [`TableCommitmentArithmeticError::NonContiguous`] is returned.
328    ///
329    /// Furthermore, `other`'s range must be smaller or equal to `self`'s.
330    /// Otherwise, [`TableCommitmentArithmeticError::NegativeRange`] is returned.
331    ///
332    /// This will also error on a variety of mismatches.
333    /// See [`ColumnCommitmentsMismatch`] for an enumeration of these errors.
334    pub fn try_sub(self, other: Self) -> Result<Self, TableCommitmentArithmeticError>
335    where
336        Self: Sized,
337    {
338        if self.range.len() < other.range.len() {
339            Err(NegativeRange)?;
340        }
341
342        let range = if self.range.start == other.range.start {
343            other.range.end..self.range.end
344        } else if self.range.end == other.range.end {
345            self.range.start..other.range.start
346        } else {
347            return Err(TableCommitmentArithmeticError::NonContiguous);
348        };
349
350        let column_commitments = self.column_commitments.try_sub(other.column_commitments)?;
351
352        Ok(TableCommitment {
353            range,
354            column_commitments,
355        })
356    }
357}
358
359/// Return the number of rows for the provided columns, erroring if they have mixed length.
360fn num_rows_of_columns<'a>(
361    committable_columns: impl IntoIterator<Item = &'a CommittableColumn<'a>>,
362) -> Result<usize, MixedLengthColumns> {
363    let mut committable_columns_iter = committable_columns.into_iter().peekable();
364    let num_rows = committable_columns_iter
365        .peek()
366        .map_or(0, |committable_column| committable_column.len());
367
368    for committable_column in committable_columns_iter {
369        if committable_column.len() != num_rows {
370            return Err(MixedLengthColumns);
371        }
372    }
373
374    Ok(num_rows)
375}
376
377#[cfg(all(test, feature = "arrow", feature = "blitzar"))]
378mod tests {
379    use super::*;
380    use crate::base::{
381        commitment::naive_commitment::NaiveCommitment,
382        database::{owned_table_utility::*, Column, OwnedColumn},
383        map::IndexMap,
384        scalar::test_scalar::TestScalar,
385    };
386    use arrow::{
387        array::{Int64Array, StringArray},
388        datatypes::{DataType, Field, Schema},
389        record_batch::RecordBatch,
390    };
391    use std::sync::Arc;
392
393    #[test]
394    #[expect(clippy::reversed_empty_ranges)]
395    fn we_cannot_construct_table_commitment_with_negative_range() {
396        let try_new_result =
397            TableCommitment::<NaiveCommitment>::try_new(ColumnCommitments::default(), 1..0);
398
399        assert!(matches!(try_new_result, Err(NegativeRange)));
400    }
401
402    #[test]
403    fn we_can_construct_table_commitment_from_columns_and_identifiers() {
404        // no-columns case
405        let mut empty_columns_iter: IndexMap<Ident, OwnedColumn<TestScalar>> = IndexMap::default();
406        let empty_table_commitment =
407            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
408                &empty_columns_iter,
409                0,
410                &(),
411            )
412            .unwrap();
413        assert_eq!(
414            empty_table_commitment.column_commitments(),
415            &ColumnCommitments::try_from_columns_with_offset(&empty_columns_iter, 0, &()).unwrap()
416        );
417        assert_eq!(empty_table_commitment.range(), &(0..0));
418        assert_eq!(empty_table_commitment.num_columns(), 0);
419        assert_eq!(empty_table_commitment.num_rows(), 0);
420
421        // no-rows case
422        empty_columns_iter.insert("column_a".into(), OwnedColumn::BigInt(vec![]));
423        let empty_table_commitment =
424            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
425                &empty_columns_iter,
426                1,
427                &(),
428            )
429            .unwrap();
430        assert_eq!(
431            empty_table_commitment.column_commitments(),
432            &ColumnCommitments::try_from_columns_with_offset(&empty_columns_iter, 1, &()).unwrap()
433        );
434        assert_eq!(empty_table_commitment.range(), &(1..1));
435        assert_eq!(empty_table_commitment.num_columns(), 1);
436        assert_eq!(empty_table_commitment.num_rows(), 0);
437
438        // nonempty case
439        let owned_table = owned_table::<TestScalar>([
440            bigint("bigint_id", [1, 5, -5, 0]),
441            // "int128_column" => [100i128, 200, 300, 400], TODO: enable this column once blitzar
442            // supports it
443            varchar("varchar_id", ["Lorem", "ipsum", "dolor", "sit"]),
444            scalar("scalar_id", [1000, 2000, -1000, 0]),
445        ]);
446        let table_commitment = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
447            owned_table.inner_table(),
448            2,
449            &(),
450        )
451        .unwrap();
452        assert_eq!(
453            table_commitment.column_commitments(),
454            &ColumnCommitments::try_from_columns_with_offset(owned_table.inner_table(), 2, &())
455                .unwrap()
456        );
457        assert_eq!(table_commitment.range(), &(2..6));
458        assert_eq!(table_commitment.num_columns(), 3);
459        assert_eq!(table_commitment.num_rows(), 4);
460
461        // matches from_owned_table constructor
462        let table_commitment_from_owned_table =
463            TableCommitment::from_owned_table_with_offset(&owned_table, 2, &());
464        assert_eq!(table_commitment_from_owned_table, table_commitment);
465    }
466
467    #[test]
468    fn we_cannot_construct_table_commitment_from_duplicate_identifiers() {
469        let duplicate_identifier_a = "duplicate_identifier_a".into();
470        let duplicate_identifier_b = "duplicate_identifier_b".into();
471        let unique_identifier = "unique_identifier".into();
472
473        let empty_column = OwnedColumn::<TestScalar>::BigInt(vec![]);
474
475        let from_columns_result = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
476            [
477                (&duplicate_identifier_a, &empty_column),
478                (&unique_identifier, &empty_column),
479                (&duplicate_identifier_a, &empty_column),
480            ],
481            0,
482            &(),
483        );
484        assert!(matches!(
485            from_columns_result,
486            Err(TableCommitmentFromColumnsError::DuplicateIdents { .. })
487        ));
488
489        let mut table_commitment =
490            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
491                [
492                    (&duplicate_identifier_a, &empty_column),
493                    (&unique_identifier, &empty_column),
494                ],
495                0,
496                &(),
497            )
498            .unwrap();
499        let column_commitments = table_commitment.column_commitments().clone();
500
501        let extend_columns_result =
502            table_commitment.try_extend_columns([(&duplicate_identifier_a, &empty_column)], &());
503        assert!(matches!(
504            extend_columns_result,
505            Err(TableCommitmentFromColumnsError::DuplicateIdents { .. })
506        ));
507
508        let extend_columns_result = table_commitment.try_extend_columns(
509            [
510                (&duplicate_identifier_b, &empty_column),
511                (&duplicate_identifier_b, &empty_column),
512            ],
513            &(),
514        );
515        assert!(matches!(
516            extend_columns_result,
517            Err(TableCommitmentFromColumnsError::DuplicateIdents { .. })
518        ));
519
520        // make sure the commitment wasn't mutated
521        assert_eq!(table_commitment.num_columns(), 2);
522        assert_eq!(table_commitment.column_commitments(), &column_commitments);
523    }
524
525    #[test]
526    fn we_cannot_construct_table_commitment_from_columns_of_mixed_length() {
527        let column_id_a = "column_a".into();
528        let column_id_b = "column_b".into();
529        let column_id_c = "column_c".into();
530
531        let one_row_column = OwnedColumn::<TestScalar>::BigInt(vec![1]);
532        let two_row_column = OwnedColumn::<TestScalar>::BigInt(vec![1, 2]);
533
534        let from_columns_result = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
535            [
536                (&column_id_a, &one_row_column),
537                (&column_id_b, &two_row_column),
538            ],
539            0,
540            &(),
541        );
542        assert!(matches!(
543            from_columns_result,
544            Err(TableCommitmentFromColumnsError::MixedLengthColumns { .. })
545        ));
546
547        let mut table_commitment =
548            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
549                [(&column_id_a, &one_row_column)],
550                0,
551                &(),
552            )
553            .unwrap();
554        let column_commitments = table_commitment.column_commitments().clone();
555
556        let extend_columns_result =
557            table_commitment.try_extend_columns([(&column_id_b, &two_row_column)], &());
558        assert!(matches!(
559            extend_columns_result,
560            Err(TableCommitmentFromColumnsError::MixedLengthColumns { .. })
561        ));
562
563        let extend_columns_result = table_commitment.try_extend_columns(
564            [
565                (&column_id_b, &one_row_column),
566                (&column_id_c, &two_row_column),
567            ],
568            &(),
569        );
570        assert!(matches!(
571            extend_columns_result,
572            Err(TableCommitmentFromColumnsError::MixedLengthColumns { .. })
573        ));
574
575        // make sure the commitment wasn't mutated
576        assert_eq!(table_commitment.num_columns(), 1);
577        assert_eq!(table_commitment.column_commitments(), &column_commitments);
578    }
579
580    #[test]
581    fn we_can_append_rows_to_table_commitment() {
582        let bigint_id: Ident = "bigint_column".into();
583        let bigint_data = [1i64, 5, -5, 0, 10];
584
585        let varchar_id: Ident = "varchar_column".into();
586        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
587
588        let scalar_id: Ident = "scalar_column".into();
589        let scalar_data = [1000, 2000, 3000, -1000, 0];
590
591        let initial_columns: OwnedTable<TestScalar> = owned_table([
592            bigint(bigint_id.value.as_str(), bigint_data[..2].to_vec()),
593            varchar(varchar_id.value.as_str(), varchar_data[..2].to_vec()),
594            scalar(scalar_id.value.as_str(), scalar_data[..2].to_vec()),
595        ]);
596
597        let mut table_commitment =
598            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
599                initial_columns.inner_table(),
600                0,
601                &(),
602            )
603            .unwrap();
604        let mut table_commitment_clone = table_commitment.clone();
605
606        let append_columns: OwnedTable<TestScalar> = owned_table([
607            bigint(bigint_id.value.as_str(), bigint_data[2..].to_vec()),
608            varchar(varchar_id.value.as_str(), varchar_data[2..].to_vec()),
609            scalar(scalar_id.value.as_str(), scalar_data[2..].to_vec()),
610        ]);
611
612        table_commitment
613            .try_append_rows(append_columns.inner_table(), &())
614            .unwrap();
615
616        let total_columns: OwnedTable<TestScalar> = owned_table([
617            bigint(bigint_id.value.as_str(), bigint_data),
618            varchar(varchar_id.value.as_str(), varchar_data),
619            scalar(scalar_id.value.as_str(), scalar_data),
620        ]);
621
622        let expected_table_commitment =
623            TableCommitment::try_from_columns_with_offset(total_columns.inner_table(), 0, &())
624                .unwrap();
625
626        assert_eq!(table_commitment, expected_table_commitment);
627
628        // matches append_owned_table result
629        table_commitment_clone
630            .append_owned_table(&append_columns, &())
631            .unwrap();
632        assert_eq!(table_commitment, table_commitment_clone);
633    }
634
635    #[test]
636    fn we_cannot_append_mismatched_columns_to_table_commitment() {
637        let base_table: OwnedTable<TestScalar> = owned_table([
638            bigint("column_a", [1, 2, 3, 4]),
639            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
640        ]);
641        let mut table_commitment =
642            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
643                base_table.inner_table(),
644                0,
645                &(),
646            )
647            .unwrap();
648        let column_commitments = table_commitment.column_commitments().clone();
649
650        let table_diff_type: OwnedTable<TestScalar> = owned_table([
651            varchar("column_a", ["5", "6", "7", "8"]),
652            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
653        ]);
654        assert!(matches!(
655            table_commitment.try_append_rows(table_diff_type.inner_table(), &()),
656            Err(AppendTableCommitmentError::AppendColumnCommitments {
657                source: AppendColumnCommitmentsError::Mismatch {
658                    source: ColumnCommitmentsMismatch::ColumnCommitmentMetadata { .. }
659                }
660            })
661        ));
662
663        // make sure the commitment wasn't mutated
664        assert_eq!(table_commitment.num_rows(), 4);
665        assert_eq!(table_commitment.column_commitments(), &column_commitments);
666    }
667
668    #[test]
669    fn we_cannot_append_columns_with_duplicate_identifiers_to_table_commitment() {
670        let column_id_a = "column_a".into();
671        let column_id_b = "column_b".into();
672
673        let column_data = OwnedColumn::<TestScalar>::BigInt(vec![1, 2, 3]);
674
675        let mut table_commitment =
676            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
677                [(&column_id_a, &column_data), (&column_id_b, &column_data)],
678                0,
679                &(),
680            )
681            .unwrap();
682        let column_commitments = table_commitment.column_commitments().clone();
683
684        let append_column_result = table_commitment.try_append_rows(
685            [
686                (&column_id_a, &column_data),
687                (&column_id_b, &column_data),
688                (&column_id_a, &column_data),
689            ],
690            &(),
691        );
692        assert!(matches!(
693            append_column_result,
694            Err(AppendTableCommitmentError::AppendColumnCommitments {
695                source: AppendColumnCommitmentsError::DuplicateIdents { .. }
696            })
697        ));
698
699        // make sure the commitment wasn't mutated
700        assert_eq!(table_commitment.num_rows(), 3);
701        assert_eq!(table_commitment.column_commitments(), &column_commitments);
702    }
703
704    #[expect(clippy::similar_names)]
705    #[test]
706    fn we_cannot_append_columns_of_mixed_length_to_table_commitment() {
707        let column_id_a: Ident = "column_a".into();
708        let column_id_b: Ident = "column_b".into();
709        let base_table: OwnedTable<TestScalar> = owned_table([
710            bigint(column_id_a.value.as_str(), [1, 2, 3, 4]),
711            varchar(
712                column_id_b.value.as_str(),
713                ["Lorem", "ipsum", "dolor", "sit"],
714            ),
715        ]);
716
717        let mut table_commitment =
718            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
719                base_table.inner_table(),
720                0,
721                &(),
722            )
723            .unwrap();
724        let column_commitments = table_commitment.column_commitments().clone();
725
726        let column_a_append_data = OwnedColumn::<TestScalar>::BigInt(vec![5, 6, 7]);
727        let column_b_append_data =
728            OwnedColumn::VarChar(["amet", "consectetur"].map(String::from).to_vec());
729
730        let append_result = table_commitment.try_append_rows(
731            [
732                (&column_id_a, &column_a_append_data),
733                (&column_id_b, &column_b_append_data),
734            ],
735            &(),
736        );
737        assert!(matches!(
738            append_result,
739            Err(AppendTableCommitmentError::MixedLengthColumns { .. })
740        ));
741
742        // make sure the commitment wasn't mutated
743        assert_eq!(table_commitment.num_rows(), 4);
744        assert_eq!(table_commitment.column_commitments(), &column_commitments);
745    }
746
747    #[test]
748    fn we_can_extend_columns_to_table_commitment() {
749        let bigint_id: Ident = "bigint_column".into();
750        let bigint_data = [1i64, 5, -5, 0, 10];
751
752        let varchar_id: Ident = "varchar_column".into();
753        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
754
755        let scalar_id: Ident = "scalar_column".into();
756        let scalar_data = [1000, 2000, 3000, -1000, 0];
757
758        let initial_columns: OwnedTable<TestScalar> = owned_table([
759            bigint(bigint_id.value.as_str(), bigint_data),
760            varchar(varchar_id.value.as_str(), varchar_data),
761        ]);
762        let mut table_commitment =
763            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
764                initial_columns.inner_table(),
765                2,
766                &(),
767            )
768            .unwrap();
769
770        let new_columns =
771            owned_table::<TestScalar>([scalar(scalar_id.value.as_str(), scalar_data)]);
772        table_commitment
773            .try_extend_columns(new_columns.inner_table(), &())
774            .unwrap();
775
776        let expected_columns = owned_table::<TestScalar>([
777            bigint(bigint_id.value.as_str(), bigint_data),
778            varchar(varchar_id.value.as_str(), varchar_data),
779            scalar(scalar_id.value.as_str(), scalar_data),
780        ]);
781        let expected_table_commitment =
782            TableCommitment::try_from_columns_with_offset(expected_columns.inner_table(), 2, &())
783                .unwrap();
784
785        assert_eq!(table_commitment, expected_table_commitment);
786    }
787
788    #[test]
789    fn we_can_add_table_commitments() {
790        let bigint_id: Ident = "bigint_column".into();
791        let bigint_data = [1i64, 5, -5, 0, 10];
792
793        let varchar_id: Ident = "varchar_column".into();
794        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
795
796        let scalar_id: Ident = "scalar_column".into();
797        let scalar_data = [1000, 2000, 3000, -1000, 0];
798
799        let columns_a: OwnedTable<TestScalar> = owned_table([
800            bigint(bigint_id.value.as_str(), bigint_data[..2].to_vec()),
801            varchar(varchar_id.value.as_str(), varchar_data[..2].to_vec()),
802            scalar(scalar_id.value.as_str(), scalar_data[..2].to_vec()),
803        ]);
804
805        let table_commitment_a = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
806            columns_a.inner_table(),
807            0,
808            &(),
809        )
810        .unwrap();
811
812        let columns_b: OwnedTable<TestScalar> = owned_table([
813            bigint(bigint_id.value.as_str(), bigint_data[2..].to_vec()),
814            varchar(varchar_id.value.as_str(), varchar_data[2..].to_vec()),
815            scalar(scalar_id.value.as_str(), scalar_data[2..].to_vec()),
816        ]);
817        let table_commitment_b =
818            TableCommitment::try_from_columns_with_offset(columns_b.inner_table(), 2, &()).unwrap();
819
820        let columns_sum: OwnedTable<TestScalar> = owned_table([
821            bigint(bigint_id.value.as_str(), bigint_data),
822            varchar(varchar_id.value.as_str(), varchar_data),
823            scalar(scalar_id.value.as_str(), scalar_data),
824        ]);
825        let table_commitment_sum =
826            TableCommitment::try_from_columns_with_offset(columns_sum.inner_table(), 0, &())
827                .unwrap();
828
829        assert_eq!(
830            table_commitment_a
831                .clone()
832                .try_add(table_commitment_b.clone())
833                .unwrap(),
834            table_commitment_sum
835        );
836        // commutativity
837        assert_eq!(
838            table_commitment_b.try_add(table_commitment_a).unwrap(),
839            table_commitment_sum
840        );
841    }
842
843    #[test]
844    fn we_cannot_add_mismatched_table_commitments() {
845        let base_table: OwnedTable<TestScalar> = owned_table([
846            bigint("column_a", [1, 2, 3, 4]),
847            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
848        ]);
849        let table_commitment = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
850            base_table.inner_table(),
851            0,
852            &(),
853        )
854        .unwrap();
855
856        let table_diff_type: OwnedTable<TestScalar> = owned_table([
857            varchar("column_a", ["5", "6", "7", "8"]),
858            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
859        ]);
860        let table_commitment_diff_type =
861            TableCommitment::try_from_columns_with_offset(table_diff_type.inner_table(), 4, &())
862                .unwrap();
863        assert!(matches!(
864            table_commitment.try_add(table_commitment_diff_type),
865            Err(TableCommitmentArithmeticError::ColumnMismatch { .. })
866        ));
867    }
868
869    #[test]
870    fn we_cannot_add_noncontiguous_table_commitments() {
871        let base_table: OwnedTable<TestScalar> = owned_table([
872            bigint("column_a", [1, 2, 3, 4]),
873            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
874        ]);
875        let table_commitment = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
876            base_table.inner_table(),
877            5,
878            &(),
879        )
880        .unwrap();
881
882        let high_disjoint_table_commitment =
883            TableCommitment::try_from_columns_with_offset(base_table.inner_table(), 10, &())
884                .unwrap();
885        assert!(matches!(
886            table_commitment
887                .clone()
888                .try_add(high_disjoint_table_commitment),
889            Err(TableCommitmentArithmeticError::NonContiguous)
890        ));
891
892        let high_overlapping_table_commitment =
893            TableCommitment::try_from_columns_with_offset(base_table.inner_table(), 7, &())
894                .unwrap();
895        assert!(matches!(
896            table_commitment
897                .clone()
898                .try_add(high_overlapping_table_commitment),
899            Err(TableCommitmentArithmeticError::NonContiguous)
900        ));
901
902        let equal_range_table_commitment =
903            TableCommitment::try_from_columns_with_offset(base_table.inner_table(), 5, &())
904                .unwrap();
905        assert!(matches!(
906            table_commitment
907                .clone()
908                .try_add(equal_range_table_commitment),
909            Err(TableCommitmentArithmeticError::NonContiguous)
910        ));
911
912        let low_overlapping_table_commitment =
913            TableCommitment::try_from_columns_with_offset(base_table.inner_table(), 3, &())
914                .unwrap();
915        assert!(matches!(
916            table_commitment
917                .clone()
918                .try_add(low_overlapping_table_commitment),
919            Err(TableCommitmentArithmeticError::NonContiguous)
920        ));
921
922        let low_disjoint_table_commitment =
923            TableCommitment::try_from_columns_with_offset(base_table.inner_table(), 0, &())
924                .unwrap();
925        assert!(matches!(
926            table_commitment
927                .clone()
928                .try_add(low_disjoint_table_commitment),
929            Err(TableCommitmentArithmeticError::NonContiguous)
930        ));
931    }
932
933    #[test]
934    fn we_can_sub_table_commitments() {
935        let bigint_id: Ident = "bigint_column".into();
936        let bigint_data = [1i64, 5, -5, 0, 10];
937
938        let varchar_id: Ident = "varchar_column".into();
939        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
940
941        let scalar_id: Ident = "scalar_column".into();
942        let scalar_data = [1000, 2000, 3000, -1000, 0];
943
944        let columns_low: OwnedTable<TestScalar> = owned_table([
945            bigint(bigint_id.value.as_str(), bigint_data[..2].to_vec()),
946            varchar(varchar_id.value.as_str(), varchar_data[..2].to_vec()),
947            scalar(scalar_id.value.as_str(), scalar_data[..2].to_vec()),
948        ]);
949        let table_commitment_low =
950            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
951                columns_low.inner_table(),
952                0,
953                &(),
954            )
955            .unwrap();
956
957        let columns_high: OwnedTable<TestScalar> = owned_table([
958            bigint(bigint_id.value.as_str(), bigint_data[2..].to_vec()),
959            varchar(varchar_id.value.as_str(), varchar_data[2..].to_vec()),
960            scalar(scalar_id.value.as_str(), scalar_data[2..].to_vec()),
961        ]);
962        let table_commitment_high =
963            TableCommitment::try_from_columns_with_offset(columns_high.inner_table(), 2, &())
964                .unwrap();
965
966        let columns_all: OwnedTable<TestScalar> = owned_table([
967            bigint(bigint_id.value.as_str(), bigint_data),
968            varchar(varchar_id.value.as_str(), varchar_data),
969            scalar(scalar_id.value.as_str(), scalar_data),
970        ]);
971        let table_commitment_all =
972            TableCommitment::try_from_columns_with_offset(columns_all.inner_table(), 0, &())
973                .unwrap();
974
975        // case where we subtract the low commitment off the total to get the high commitment
976        let high_difference = table_commitment_all
977            .clone()
978            .try_sub(table_commitment_low.clone())
979            .unwrap();
980        assert_eq!(
981            high_difference.column_commitments().commitments(),
982            table_commitment_high.column_commitments().commitments()
983        );
984        assert_eq!(high_difference.range(), table_commitment_high.range());
985
986        // case where we subtract the high commitment off the total to get the low commitment
987        let low_difference = table_commitment_all.try_sub(table_commitment_high).unwrap();
988        assert_eq!(
989            low_difference.column_commitments().commitments(),
990            table_commitment_low.column_commitments().commitments()
991        );
992        assert_eq!(low_difference.range(), table_commitment_low.range());
993
994        // subtraction for column metadata is tested more thoroughly at a lower level
995    }
996
997    #[test]
998    fn we_cannot_sub_mismatched_table_commitments() {
999        let base_table: OwnedTable<TestScalar> = owned_table([
1000            bigint("column_a", [1, 2, 3, 4]),
1001            varchar("column_b", ["Lorem", "ipsum", "dolor", "sit"]),
1002        ]);
1003        let table_commitment = TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
1004            base_table.inner_table(),
1005            0,
1006            &(),
1007        )
1008        .unwrap();
1009
1010        let table_diff_type: OwnedTable<TestScalar> = owned_table([
1011            varchar("column_a", ["1", "2"]),
1012            varchar("column_b", ["Lorem", "ipsum"]),
1013        ]);
1014        let table_commitment_diff_type =
1015            TableCommitment::try_from_columns_with_offset(table_diff_type.inner_table(), 0, &())
1016                .unwrap();
1017        assert!(matches!(
1018            table_commitment.try_sub(table_commitment_diff_type),
1019            Err(TableCommitmentArithmeticError::ColumnMismatch { .. })
1020        ));
1021    }
1022
1023    #[test]
1024    fn we_cannot_sub_noncontiguous_table_commitments() {
1025        let bigint_id: Ident = "bigint_column".into();
1026        let bigint_data = [1i64, 5, -5, 0, 10];
1027
1028        let varchar_id: Ident = "varchar_column".into();
1029        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
1030
1031        let scalar_id: Ident = "scalar_column".into();
1032        let scalar_data = [1000, 2000, 3000, -1000, 0];
1033
1034        let columns_minuend: OwnedTable<TestScalar> = owned_table([
1035            bigint(bigint_id.value.as_str(), bigint_data[..].to_vec()),
1036            varchar(varchar_id.value.as_str(), varchar_data[..].to_vec()),
1037            scalar(scalar_id.value.as_str(), scalar_data[..].to_vec()),
1038        ]);
1039
1040        let columns_subtrahend: OwnedTable<TestScalar> = owned_table([
1041            bigint(bigint_id.value.as_str(), bigint_data[..2].to_vec()),
1042            varchar(varchar_id.value.as_str(), varchar_data[..2].to_vec()),
1043            scalar(scalar_id.value.as_str(), scalar_data[..2].to_vec()),
1044        ]);
1045
1046        let minuend_table_commitment =
1047            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
1048                columns_minuend.inner_table(),
1049                4,
1050                &(),
1051            )
1052            .unwrap();
1053
1054        let high_contiguous_table_commitment =
1055            TableCommitment::try_from_columns_with_offset(columns_subtrahend.inner_table(), 9, &())
1056                .unwrap();
1057        assert!(matches!(
1058            minuend_table_commitment
1059                .clone()
1060                .try_sub(high_contiguous_table_commitment),
1061            Err(TableCommitmentArithmeticError::NonContiguous)
1062        ));
1063
1064        let high_overlapping_table_commitment =
1065            TableCommitment::try_from_columns_with_offset(columns_subtrahend.inner_table(), 6, &())
1066                .unwrap();
1067        assert!(matches!(
1068            minuend_table_commitment
1069                .clone()
1070                .try_sub(high_overlapping_table_commitment),
1071            Err(TableCommitmentArithmeticError::NonContiguous)
1072        ));
1073
1074        let low_overlapping_table_commitment =
1075            TableCommitment::try_from_columns_with_offset(columns_subtrahend.inner_table(), 3, &())
1076                .unwrap();
1077        assert!(matches!(
1078            minuend_table_commitment
1079                .clone()
1080                .try_sub(low_overlapping_table_commitment),
1081            Err(TableCommitmentArithmeticError::NonContiguous)
1082        ));
1083
1084        let low_contiguous_table_commitment =
1085            TableCommitment::try_from_columns_with_offset(columns_subtrahend.inner_table(), 2, &())
1086                .unwrap();
1087        assert!(matches!(
1088            minuend_table_commitment
1089                .clone()
1090                .try_sub(low_contiguous_table_commitment),
1091            Err(TableCommitmentArithmeticError::NonContiguous)
1092        ));
1093    }
1094
1095    #[test]
1096    fn we_cannot_sub_commitments_with_negative_difference() {
1097        let bigint_id: Ident = "bigint_column".into();
1098        let bigint_data = [1i64, 5, -5, 0, 10];
1099
1100        let varchar_id: Ident = "varchar_column".into();
1101        let varchar_data = ["Lorem", "ipsum", "dolor", "sit", "amet"];
1102
1103        let scalar_id: Ident = "scalar_column".into();
1104        let scalar_data = [1000, 2000, 3000, -1000, 0];
1105
1106        let columns_low: OwnedTable<TestScalar> = owned_table([
1107            bigint(bigint_id.value.as_str(), bigint_data[..2].to_vec()),
1108            varchar(varchar_id.value.as_str(), varchar_data[..2].to_vec()),
1109            scalar(scalar_id.value.as_str(), scalar_data[..2].to_vec()),
1110        ]);
1111        let table_commitment_low =
1112            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(
1113                columns_low.inner_table(),
1114                0,
1115                &(),
1116            )
1117            .unwrap();
1118
1119        let columns_high: OwnedTable<TestScalar> = owned_table([
1120            bigint(bigint_id.value.as_str(), bigint_data[2..].to_vec()),
1121            varchar(varchar_id.value.as_str(), varchar_data[2..].to_vec()),
1122            scalar(scalar_id.value.as_str(), scalar_data[2..].to_vec()),
1123        ]);
1124        let table_commitment_high =
1125            TableCommitment::try_from_columns_with_offset(columns_high.inner_table(), 2, &())
1126                .unwrap();
1127
1128        let columns_all: OwnedTable<TestScalar> = owned_table([
1129            bigint(bigint_id.value.as_str(), bigint_data),
1130            varchar(varchar_id.value.as_str(), varchar_data),
1131            scalar(scalar_id.value.as_str(), scalar_data),
1132        ]);
1133        let table_commitment_all =
1134            TableCommitment::try_from_columns_with_offset(columns_all.inner_table(), 0, &())
1135                .unwrap();
1136
1137        // try to subtract the total commitment off the low to get the "negative" high commitment
1138        let try_negative_high_difference_result =
1139            table_commitment_low.try_sub(table_commitment_all.clone());
1140        assert!(matches!(
1141            try_negative_high_difference_result,
1142            Err(TableCommitmentArithmeticError::NegativeRange { .. })
1143        ));
1144
1145        // try to subtract the total commitment off the high to get the "negative" low commitment
1146        let try_negative_low_difference_result =
1147            table_commitment_high.try_sub(table_commitment_all);
1148        assert!(matches!(
1149            try_negative_low_difference_result,
1150            Err(TableCommitmentArithmeticError::NegativeRange { .. })
1151        ));
1152    }
1153
1154    #[test]
1155    fn we_can_create_and_append_table_commitments_with_record_batches() {
1156        let schema = Arc::new(Schema::new(vec![
1157            Field::new("a", DataType::Int64, false),
1158            Field::new("b", DataType::Utf8, false),
1159        ]));
1160
1161        let batch = RecordBatch::try_new(
1162            schema,
1163            vec![
1164                Arc::new(Int64Array::from(vec![1, 2, 3])),
1165                Arc::new(StringArray::from(vec!["1", "2", "3"])),
1166            ],
1167        )
1168        .unwrap();
1169
1170        let b_scals = ["1".into(), "2".into(), "3".into()];
1171
1172        let columns = [
1173            (&"a".into(), &Column::<TestScalar>::BigInt(&[1, 2, 3])),
1174            (
1175                &"b".into(),
1176                &Column::<TestScalar>::VarChar((&["1", "2", "3"], &b_scals)),
1177            ),
1178        ];
1179
1180        let mut expected_commitment =
1181            TableCommitment::<NaiveCommitment>::try_from_columns_with_offset(columns, 0, &())
1182                .unwrap();
1183
1184        let mut commitment =
1185            TableCommitment::<NaiveCommitment>::try_from_record_batch(&batch, &()).unwrap();
1186
1187        assert_eq!(commitment, expected_commitment);
1188        let schema2 = Arc::new(Schema::new(vec![
1189            Field::new("a", DataType::Int64, false),
1190            Field::new("b", DataType::Utf8, false),
1191        ]));
1192
1193        let batch2 = RecordBatch::try_new(
1194            schema2,
1195            vec![
1196                Arc::new(Int64Array::from(vec![4, 5, 6])),
1197                Arc::new(StringArray::from(vec!["4", "5", "6"])),
1198            ],
1199        )
1200        .unwrap();
1201
1202        let b_scals2 = ["4".into(), "5".into(), "6".into()];
1203
1204        let columns2 = [
1205            (&"a".into(), &Column::<TestScalar>::BigInt(&[4, 5, 6])),
1206            (
1207                &"b".into(),
1208                &Column::<TestScalar>::VarChar((&["4", "5", "6"], &b_scals2)),
1209            ),
1210        ];
1211
1212        expected_commitment.try_append_rows(columns2, &()).unwrap();
1213        commitment.try_append_record_batch(&batch2, &()).unwrap();
1214
1215        assert_eq!(commitment, expected_commitment);
1216    }
1217}