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#[derive(Debug, Snafu)]
17#[snafu(display("cannot create a TableCommitment with a negative range"))]
18pub struct NegativeRange;
19
20#[derive(Debug, Snafu)]
22#[snafu(display("cannot create a TableCommitment from columns of mixed length"))]
23pub struct MixedLengthColumns;
24
25#[derive(Debug, Snafu)]
27pub enum TableCommitmentFromColumnsError {
28 #[snafu(transparent)]
30 MixedLengthColumns {
31 source: MixedLengthColumns,
33 },
34 #[snafu(transparent)]
36 DuplicateIdents {
37 source: DuplicateIdents,
39 },
40}
41
42#[derive(Debug, Snafu)]
44pub enum AppendTableCommitmentError {
45 #[snafu(transparent)]
47 MixedLengthColumns {
48 source: MixedLengthColumns,
50 },
51 #[snafu(transparent)]
53 AppendColumnCommitments {
54 source: AppendColumnCommitmentsError,
56 },
57}
58
59#[derive(Debug, Snafu)]
61pub enum TableCommitmentArithmeticError {
62 #[snafu(transparent)]
64 ColumnMismatch {
65 source: ColumnCommitmentsMismatch,
67 },
68 #[snafu(transparent)]
70 NegativeRange {
71 source: NegativeRange,
73 },
74 #[snafu(display(
76 "cannot perform table commitment arithmetic for noncontiguous table commitments"
77 ))]
78 NonContiguous,
79}
80
81#[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 #[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 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 #[must_use]
137 pub fn column_commitments(&self) -> &ColumnCommitments<C> {
138 &self.column_commitments
139 }
140
141 #[must_use]
143 pub fn range(&self) -> &Range<usize> {
144 &self.range
145 }
146
147 #[must_use]
149 pub fn num_columns(&self) -> usize {
150 self.column_commitments.len()
151 }
152
153 #[must_use]
155 pub fn num_rows(&self) -> usize {
156 self.range.len()
157 }
158
159 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 #[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 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 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 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 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 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
359fn 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 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 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 let owned_table = owned_table::<TestScalar>([
440 bigint("bigint_id", [1, 5, -5, 0]),
441 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 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 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 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 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 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 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 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 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 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 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 }
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 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 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}