Skip to main content

laddu_data/
columns.rs

1//! Exact, immutable row-data columns independent of expression scalars.
2
3use std::{fmt, str::FromStr, sync::Arc};
4
5use crate::{LadduDataError, LadduDataResult};
6
7macro_rules! integer_columns {
8    ($(($variant:ident, $ty:ty, $name:literal, $alias:literal)),* $(,)?) => {
9        /// Exact storage dtype for non-expression row data.
10        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
11        pub enum ColumnDType {
12            $(#[doc = concat!("An exact `", $name, "` column.")]
13            $variant,)*
14        }
15
16        impl ColumnDType {
17            /// Returns the canonical NumPy-style dtype name.
18            pub const fn name(self) -> &'static str {
19                match self { $(Self::$variant => $name,)* }
20            }
21
22            /// Returns storage bytes per row.
23            pub const fn width(self) -> usize {
24                match self { $(Self::$variant => std::mem::size_of::<$ty>(),)* }
25            }
26        }
27
28        impl FromStr for ColumnDType {
29            type Err = LadduDataError;
30            fn from_str(value: &str) -> LadduDataResult<Self> {
31                match value {
32                    $($name | $alias => Ok(Self::$variant),)*
33                    _ => Err(LadduDataError::Schema(format!("unsupported column dtype: {value}"))),
34                }
35            }
36        }
37
38        /// One exact row-data value.
39        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
40        pub enum ColumnValue {
41            $(#[doc = concat!("An exact `", $name, "` value.")]
42            $variant($ty),)*
43        }
44
45        impl ColumnValue {
46            /// Returns the value's exact dtype.
47            pub const fn dtype(self) -> ColumnDType {
48                match self { $(Self::$variant(_) => ColumnDType::$variant,)* }
49            }
50        }
51
52        /// An immutable exact row-data column.
53        ///
54        /// Unlike floating-point expression scalars, these values never pass
55        /// through floating-point storage or runtime precision conversion.
56        #[derive(Clone, Debug, PartialEq, Eq)]
57        pub enum Column {
58            $(#[doc = concat!("An immutable `", $name, "` buffer.")]
59            $variant(Arc<[$ty]>),)*
60        }
61
62        impl Column {
63            /// Returns the exact storage dtype.
64            pub fn dtype(&self) -> ColumnDType {
65                match self { $(Self::$variant(_) => ColumnDType::$variant,)* }
66            }
67
68            /// Returns the number of values.
69            pub fn len(&self) -> usize {
70                match self { $(Self::$variant(values) => values.len(),)* }
71            }
72
73            /// Returns one exact value.
74            ///
75            /// # Panics
76            /// Panics when `row` is outside the column.
77            pub fn at(&self, row: usize) -> ColumnValue {
78                match self { $(Self::$variant(values) => ColumnValue::$variant(values[row]),)* }
79            }
80
81            /// Appends exact little-endian values to a transport buffer.
82            pub fn append_le_bytes(&self, output: &mut Vec<u8>) {
83                match self { $(Self::$variant(values) => {
84                    for value in values.iter() { output.extend_from_slice(&value.to_le_bytes()); }
85                },)* }
86            }
87
88            /// Decodes a buffer of exact little-endian values.
89            ///
90            /// # Errors
91            /// Returns an error if the byte count is not a multiple of the dtype width.
92            pub fn from_le_bytes(dtype: ColumnDType, bytes: &[u8]) -> LadduDataResult<Self> {
93                if !bytes.len().is_multiple_of(dtype.width()) {
94                    return Err(LadduDataError::Schema("invalid integer column byte count".into()));
95                }
96                Ok(match dtype { $(ColumnDType::$variant =>
97                    Self::$variant(bytes.as_chunks::<{ std::mem::size_of::<$ty>() }>().0.iter()
98                        .map(|bytes| <$ty>::from_le_bytes(*bytes)).collect()),)* })
99            }
100
101            /// Copies selected rows in the requested order.
102            ///
103            /// # Panics
104            /// Panics when a requested row is outside the column.
105            pub fn select(&self, rows: &[usize]) -> Self {
106                match self { $(Self::$variant(values) => Self::$variant(rows.iter().map(|&row| values[row]).collect()),)* }
107            }
108
109            /// Copies a half-open range of values.
110            ///
111            /// # Panics
112            /// Panics when the range is invalid.
113            pub fn slice(&self, start: usize, end: usize) -> Self {
114                match self { $(Self::$variant(values) => Self::$variant(Arc::from(&values[start..end])),)* }
115            }
116
117        }
118
119        pub(crate) enum ColumnBuffer {
120            $($variant(Vec<$ty>),)*
121        }
122
123        impl ColumnBuffer {
124            pub(crate) fn new(dtype: ColumnDType, capacity: usize) -> Self {
125                match dtype { $(ColumnDType::$variant => Self::$variant(Vec::with_capacity(capacity)),)* }
126            }
127
128            pub(crate) fn push(&mut self, value: ColumnValue) -> LadduDataResult<()> {
129                match (self, value) {
130                    $((Self::$variant(values), ColumnValue::$variant(value)) => values.push(value),)*
131                    _ => return Err(LadduDataError::Schema("typed row value dtype does not match schema".into())),
132                }
133                Ok(())
134            }
135
136            pub(crate) fn extend(&mut self, column: &Column) -> LadduDataResult<()> {
137                match (self, column) {
138                    $((Self::$variant(values), Column::$variant(column)) => values.extend_from_slice(column),)*
139                    _ => return Err(LadduDataError::Schema("typed column dtype does not match schema".into())),
140                }
141                Ok(())
142            }
143
144            pub(crate) fn finish(self) -> Column {
145                match self { $(Self::$variant(values) => Column::$variant(values.into()),)* }
146            }
147        }
148    };
149}
150
151integer_columns!(
152    (I8, i8, "int8", "i8"),
153    (U8, u8, "uint8", "u8"),
154    (I16, i16, "int16", "i16"),
155    (U16, u16, "uint16", "u16"),
156    (I32, i32, "int32", "i32"),
157    (U32, u32, "uint32", "u32"),
158    (I64, i64, "int64", "i64"),
159    (U64, u64, "uint64", "u64"),
160);
161
162impl fmt::Display for ColumnDType {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter.write_str(self.name())
165    }
166}
167
168impl Column {
169    /// Returns whether the column contains no values.
170    pub fn is_empty(&self) -> bool {
171        self.len() == 0
172    }
173
174    /// Creates a correctly typed empty column.
175    pub fn empty(dtype: ColumnDType) -> Self {
176        ColumnBuffer::new(dtype, 0).finish()
177    }
178
179    /// Concatenates columns of one exact dtype.
180    ///
181    /// # Errors
182    /// Returns an error if any column has a different dtype.
183    pub fn concat(dtype: ColumnDType, columns: &[&Self]) -> LadduDataResult<Self> {
184        let mut output = ColumnBuffer::new(dtype, columns.iter().map(|column| column.len()).sum());
185        for column in columns {
186            output.extend(column)?;
187        }
188        Ok(output.finish())
189    }
190}