Skip to main content

proof_of_sql/base/database/
owned_table.rs

1use super::{ColumnField, OwnedColumn, Table};
2use crate::base::{
3    database::ColumnCoercionError, map::IndexMap, polynomial::compute_evaluation_vector,
4    scalar::Scalar,
5};
6use alloc::{vec, vec::Vec};
7use itertools::{EitherOrBoth, Itertools};
8use serde::{Deserialize, Serialize};
9use snafu::Snafu;
10use sqlparser::ast::Ident;
11
12/// An error that occurs when working with tables.
13#[derive(Snafu, Debug, PartialEq, Eq)]
14pub enum OwnedTableError {
15    /// The columns have different lengths.
16    #[snafu(display("Columns have different lengths"))]
17    ColumnLengthMismatch,
18}
19
20/// Errors that can occur when coercing a table.
21#[derive(Snafu, Debug, PartialEq, Eq)]
22pub(crate) enum TableCoercionError {
23    #[snafu(transparent)]
24    ColumnCoercionError { source: ColumnCoercionError },
25    /// Name mismatch between column and field.
26    #[snafu(display("Name mismatch between column and field"))]
27    NameMismatch,
28    /// Column count mismatch.
29    #[snafu(display("Column count mismatch"))]
30    ColumnCountMismatch,
31}
32
33/// A table of data, with schema included. This is simply a map from `Ident` to `OwnedColumn`,
34/// where columns order matters.
35/// This is primarily used as an internal result that is used before
36/// converting to the final result in either Arrow format or JSON.
37/// This is the analog of an arrow [`RecordBatch`](arrow::record_batch::RecordBatch).
38#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
39pub struct OwnedTable<S: Scalar> {
40    table: IndexMap<Ident, OwnedColumn<S>>,
41}
42impl<S: Scalar> OwnedTable<S> {
43    /// Creates a new [`OwnedTable`].
44    pub fn try_new(table: IndexMap<Ident, OwnedColumn<S>>) -> Result<Self, OwnedTableError> {
45        if table.is_empty() {
46            return Ok(Self { table });
47        }
48        let num_rows = table[0].len();
49        if table.values().any(|column| column.len() != num_rows) {
50            Err(OwnedTableError::ColumnLengthMismatch)
51        } else {
52            Ok(Self { table })
53        }
54    }
55    /// Creates a new [`OwnedTable`].
56    pub fn try_from_iter<T: IntoIterator<Item = (Ident, OwnedColumn<S>)>>(
57        iter: T,
58    ) -> Result<Self, OwnedTableError> {
59        Self::try_new(IndexMap::from_iter(iter))
60    }
61
62    #[expect(
63        clippy::missing_panics_doc,
64        reason = "Mapping from one table to another should not result in column mismatch"
65    )]
66    /// Attempts to coerce the columns of the table to match the provided fields.
67    ///
68    /// # Arguments
69    ///
70    /// * `fields` - An iterator of `ColumnField` items that specify the desired schema.
71    ///
72    /// # Errors
73    ///
74    /// Returns a `TableCoercionError` if:
75    /// * The number of columns in the table does not match the number of fields.
76    /// * The name of a column does not match the name of the corresponding field.
77    /// * A column cannot be coerced to the type specified by the corresponding field.
78    pub(crate) fn try_coerce_with_fields<T: IntoIterator<Item = ColumnField>>(
79        self,
80        fields: T,
81    ) -> Result<Self, TableCoercionError> {
82        self.into_inner()
83            .into_iter()
84            .zip_longest(fields)
85            .map(|p| match p {
86                EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => {
87                    Err(TableCoercionError::ColumnCountMismatch)
88                }
89                EitherOrBoth::Both((name, column), field) if name == field.name() => Ok((
90                    name,
91                    column.try_coerce_scalar_to_numeric(field.data_type())?,
92                )),
93                EitherOrBoth::Both(_, _) => Err(TableCoercionError::NameMismatch),
94            })
95            .process_results(|iter| {
96                Self::try_from_iter(iter).expect("Columns should have the same length")
97            })
98    }
99
100    /// Number of columns in the table.
101    #[must_use]
102    pub fn num_columns(&self) -> usize {
103        self.table.len()
104    }
105    /// Number of rows in the table.
106    #[must_use]
107    pub fn num_rows(&self) -> usize {
108        if self.table.is_empty() {
109            0
110        } else {
111            self.table[0].len()
112        }
113    }
114    /// Whether the table has no columns.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.table.is_empty()
118    }
119    /// Returns the columns of this table as an `IndexMap`
120    #[must_use]
121    pub fn into_inner(self) -> IndexMap<Ident, OwnedColumn<S>> {
122        self.table
123    }
124    /// Returns a reference to the columns of this table as an `IndexMap`
125    #[must_use]
126    pub fn inner_table(&self) -> &IndexMap<Ident, OwnedColumn<S>> {
127        &self.table
128    }
129    /// Returns the columns of this table as an Iterator
130    pub fn column_names(&self) -> impl Iterator<Item = &Ident> {
131        self.table.keys()
132    }
133    /// Returns the column with the given position.
134    #[must_use]
135    pub fn column_by_index(&self, index: usize) -> Option<&OwnedColumn<S>> {
136        self.table.get_index(index).map(|(_, v)| v)
137    }
138
139    pub(crate) fn mle_evaluations(&self, evaluation_point: &[S]) -> Vec<S> {
140        let mut evaluation_vector = vec![S::ZERO; self.num_rows()];
141        compute_evaluation_vector(&mut evaluation_vector, evaluation_point);
142        self.table
143            .values()
144            .map(|column| column.inner_product(&evaluation_vector))
145            .collect()
146    }
147}
148
149impl<S: Scalar> IntoIterator for OwnedTable<S> {
150    type Item = (Ident, OwnedColumn<S>);
151
152    type IntoIter = indexmap::map::IntoIter<Ident, OwnedColumn<S>>;
153
154    fn into_iter(self) -> Self::IntoIter {
155        self.table.into_iter()
156    }
157}
158
159impl<S: Scalar> TryFrom<IndexMap<Ident, OwnedColumn<S>>> for OwnedTable<S> {
160    type Error = OwnedTableError;
161
162    fn try_from(value: IndexMap<Ident, OwnedColumn<S>>) -> Result<Self, Self::Error> {
163        Self::try_new(value)
164    }
165}
166
167// Note: we modify the default PartialEq for IndexMap to also check for column ordering.
168// This is to align with the behaviour of a `RecordBatch`.
169impl<S: Scalar> PartialEq for OwnedTable<S> {
170    fn eq(&self, other: &Self) -> bool {
171        self.table == other.table
172            && self
173                .table
174                .keys()
175                .zip(other.table.keys())
176                .all(|(a, b)| a == b)
177    }
178}
179
180#[cfg(test)]
181impl<S: Scalar> core::ops::Index<&str> for OwnedTable<S> {
182    type Output = OwnedColumn<S>;
183    fn index(&self, index: &str) -> &Self::Output {
184        self.table.get(&Ident::new(index)).unwrap()
185    }
186}
187
188impl<'a, S: Scalar> From<&Table<'a, S>> for OwnedTable<S> {
189    fn from(value: &Table<'a, S>) -> Self {
190        OwnedTable::try_from_iter(
191            value
192                .inner_table()
193                .iter()
194                .map(|(name, column)| (name.clone(), OwnedColumn::from(column))),
195        )
196        .expect("Tables should not have columns with differing lengths")
197    }
198}
199
200impl<'a, S: Scalar> From<Table<'a, S>> for OwnedTable<S> {
201    fn from(value: Table<'a, S>) -> Self {
202        OwnedTable::try_from_iter(
203            value
204                .into_inner()
205                .into_iter()
206                .map(|(name, column)| (name, OwnedColumn::from(&column))),
207        )
208        .expect("Tables should not have columns with differing lengths")
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::OwnedTable;
215    use crate::base::{
216        database::{
217            owned_table_utility::*, table_utility::*, ColumnCoercionError, OwnedColumn, Table,
218            TableCoercionError, TableOptions,
219        },
220        map::indexmap,
221        posql_time::{PoSQLTimeUnit, PoSQLTimeZone},
222        scalar::test_scalar::TestScalar,
223        IndexMap,
224    };
225    use bumpalo::Bump;
226
227    #[test]
228    fn test_conversion_from_table_to_owned_table() {
229        let alloc = Bump::new();
230
231        let borrowed_table = table::<TestScalar>([
232            borrowed_bigint(
233                "bigint",
234                [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX],
235                &alloc,
236            ),
237            borrowed_int128(
238                "decimal",
239                [0_i128, 1, 2, 3, 4, 5, 6, i128::MIN, i128::MAX],
240                &alloc,
241            ),
242            borrowed_varchar(
243                "varchar",
244                ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
245                &alloc,
246            ),
247            borrowed_scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8], &alloc),
248            borrowed_boolean(
249                "boolean",
250                [true, false, true, false, true, false, true, false, true],
251                &alloc,
252            ),
253            borrowed_timestamptz(
254                "time_stamp",
255                PoSQLTimeUnit::Second,
256                PoSQLTimeZone::utc(),
257                [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX],
258                &alloc,
259            ),
260        ]);
261
262        let expected_table = owned_table::<TestScalar>([
263            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
264            int128("decimal", [0_i128, 1, 2, 3, 4, 5, 6, i128::MIN, i128::MAX]),
265            varchar("varchar", ["0", "1", "2", "3", "4", "5", "6", "7", "8"]),
266            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8]),
267            boolean(
268                "boolean",
269                [true, false, true, false, true, false, true, false, true],
270            ),
271            timestamptz(
272                "time_stamp",
273                PoSQLTimeUnit::Second,
274                PoSQLTimeZone::utc(),
275                [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX],
276            ),
277        ]);
278
279        assert_eq!(OwnedTable::from(&borrowed_table), expected_table);
280        assert_eq!(OwnedTable::from(borrowed_table), expected_table);
281    }
282
283    #[test]
284    fn test_empty_and_no_columns_tables() {
285        let alloc = Bump::new();
286        // Test with no rows
287        let empty_table = table::<TestScalar>([borrowed_bigint("bigint", [0; 0], &alloc)]);
288        let expected_empty_table = owned_table::<TestScalar>([bigint("bigint", [0; 0])]);
289        assert_eq!(OwnedTable::from(&empty_table), expected_empty_table);
290        assert_eq!(OwnedTable::from(empty_table), expected_empty_table);
291
292        // Test with no columns
293        let no_columns_table_no_rows =
294            Table::try_new_with_options(indexmap! {}, TableOptions::new(Some(0))).unwrap();
295        let no_columns_table_two_rows =
296            Table::try_new_with_options(indexmap! {}, TableOptions::new(Some(2))).unwrap();
297        let expected_no_columns_table = owned_table::<TestScalar>([]);
298        assert_eq!(
299            OwnedTable::from(&no_columns_table_no_rows),
300            expected_no_columns_table
301        );
302        assert_eq!(
303            OwnedTable::from(no_columns_table_no_rows),
304            expected_no_columns_table
305        );
306        assert_eq!(
307            OwnedTable::from(&no_columns_table_two_rows),
308            expected_no_columns_table
309        );
310        assert_eq!(
311            OwnedTable::from(no_columns_table_two_rows),
312            expected_no_columns_table
313        );
314    }
315
316    #[test]
317    fn test_try_coerce_with_fields() {
318        use crate::base::database::{ColumnField, ColumnType};
319
320        let table = owned_table::<TestScalar>([
321            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
322            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8]),
323        ]);
324
325        let fields = vec![
326            ColumnField::new("bigint".into(), ColumnType::BigInt),
327            ColumnField::new("scalar".into(), ColumnType::Int),
328        ];
329
330        let coerced_table = table.clone().try_coerce_with_fields(fields).unwrap();
331
332        let expected_table = owned_table::<TestScalar>([
333            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
334            int("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8]),
335        ]);
336
337        assert_eq!(coerced_table, expected_table);
338    }
339
340    #[test]
341    fn test_try_coerce_with_fields_name_mismatch() {
342        use crate::base::database::{ColumnField, ColumnType};
343
344        let table = owned_table::<TestScalar>([
345            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
346            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8]),
347        ]);
348
349        let fields = vec![
350            ColumnField::new("bigint".into(), ColumnType::BigInt),
351            ColumnField::new("mismatch".into(), ColumnType::Int),
352        ];
353
354        let result = table.clone().try_coerce_with_fields(fields);
355
356        assert!(matches!(result, Err(TableCoercionError::NameMismatch)));
357    }
358
359    #[test]
360    fn test_try_coerce_with_fields_column_count_mismatch() {
361        use crate::base::database::{ColumnField, ColumnType};
362
363        let table = owned_table::<TestScalar>([
364            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
365            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, 8]),
366        ]);
367
368        let fields = vec![ColumnField::new("bigint".into(), ColumnType::BigInt)];
369
370        let result = table.clone().try_coerce_with_fields(fields);
371
372        assert!(matches!(
373            result,
374            Err(TableCoercionError::ColumnCountMismatch)
375        ));
376    }
377
378    #[test]
379    fn test_try_coerce_with_fields_overflow() {
380        use crate::base::database::{ColumnField, ColumnType};
381
382        let table = owned_table::<TestScalar>([
383            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
384            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, i64::MAX]),
385        ]);
386
387        let fields = vec![
388            ColumnField::new("bigint".into(), ColumnType::BigInt),
389            ColumnField::new("scalar".into(), ColumnType::TinyInt),
390        ];
391
392        let result = table.clone().try_coerce_with_fields(fields);
393
394        assert!(matches!(
395            result,
396            Err(TableCoercionError::ColumnCoercionError {
397                source: ColumnCoercionError::Overflow
398            })
399        ));
400    }
401
402    #[test]
403    fn test_try_from() {
404        let index_map = [
405            bigint("bigint", [0_i64, 1, 2, 3, 4, 5, 6, i64::MIN, i64::MAX]),
406            scalar("scalar", [0, 1, 2, 3, 4, 5, 6, 7, i64::MAX]),
407        ]
408        .into_iter()
409        .collect::<IndexMap<_, OwnedColumn<TestScalar>>>();
410        let table = OwnedTable::try_from(index_map.clone()).unwrap();
411
412        let iter_value: IndexMap<_, _> = table.into_iter().collect();
413        assert_eq!(iter_value, index_map);
414    }
415}