alopex_sql/catalog/
table.rs

1//! Table and column metadata definitions for the Alopex SQL catalog.
2//!
3//! This module defines [`TableMetadata`] and [`ColumnMetadata`] which store
4//! schema information for tables and their columns.
5
6use crate::ast::expr::Expr;
7use crate::catalog::persistent::{DataSourceFormat, TableType};
8use crate::planner::types::ResolvedType;
9use std::collections::HashMap;
10
11/// Storage layout for a table.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum StorageType {
14    /// Row-oriented storage (existing engine).
15    Row,
16    /// Columnar storage (v0.1.3+).
17    Columnar,
18}
19
20/// RowID materialization mode for columnar tables.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum RowIdMode {
23    /// RowID is not materialized or tracked.
24    None,
25    /// RowID is stored directly as a column.
26    Direct,
27}
28
29/// Compression codec used for stored data.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Compression {
32    None,
33    Lz4,
34    Zstd,
35}
36
37/// Configurable storage options for a table.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct StorageOptions {
40    pub storage_type: StorageType,
41    pub compression: Compression,
42    pub row_group_size: u32,
43    pub row_id_mode: RowIdMode,
44}
45
46impl Default for StorageOptions {
47    fn default() -> Self {
48        Self {
49            storage_type: StorageType::Row,
50            compression: Compression::Lz4,
51            row_group_size: 100_000,
52            row_id_mode: RowIdMode::Direct,
53        }
54    }
55}
56
57/// Metadata for a table in the catalog.
58///
59/// Contains the table ID, name, column definitions, and optional primary key constraint.
60///
61/// # Examples
62///
63/// ```
64/// use alopex_sql::catalog::{TableMetadata, ColumnMetadata};
65/// use alopex_sql::planner::types::ResolvedType;
66///
67/// let columns = vec![
68///     ColumnMetadata::new("id", ResolvedType::Integer)
69///         .with_primary_key(true)
70///         .with_not_null(true),
71///     ColumnMetadata::new("name", ResolvedType::Text)
72///         .with_not_null(true),
73/// ];
74///
75/// let table = TableMetadata::new("users", columns)
76///     .with_primary_key(vec!["id".to_string()]);
77///
78/// assert_eq!(table.name, "users");
79/// assert!(table.get_column("id").is_some());
80/// assert_eq!(table.column_names(), vec!["id", "name"]);
81/// ```
82#[derive(Debug, Clone)]
83pub struct TableMetadata {
84    /// Unique table ID assigned by the catalog.
85    pub table_id: u32,
86    /// Table name.
87    pub name: String,
88    /// Catalog name.
89    pub catalog_name: String,
90    /// Namespace name.
91    pub namespace_name: String,
92    /// Table type.
93    pub table_type: TableType,
94    /// Data source format.
95    pub data_source_format: DataSourceFormat,
96    /// Column definitions (order is preserved).
97    pub columns: Vec<ColumnMetadata>,
98    /// Primary key columns (supports composite keys).
99    pub primary_key: Option<Vec<String>>,
100    /// Storage configuration (row/columnar, compression, row group sizing).
101    pub storage_options: StorageOptions,
102    /// Storage location path.
103    pub storage_location: Option<String>,
104    /// Comment.
105    pub comment: Option<String>,
106    /// Custom properties.
107    pub properties: HashMap<String, String>,
108}
109
110impl TableMetadata {
111    /// Create a new table metadata with the given name and columns.
112    ///
113    /// The table_id defaults to 0; use `with_table_id()` to set it,
114    /// or it will be assigned by the Catalog when the table is created.
115    pub fn new(name: impl Into<String>, columns: Vec<ColumnMetadata>) -> Self {
116        Self {
117            table_id: 0,
118            name: name.into(),
119            catalog_name: "default".to_string(),
120            namespace_name: "default".to_string(),
121            table_type: TableType::Managed,
122            data_source_format: DataSourceFormat::default(),
123            columns,
124            primary_key: None,
125            storage_options: StorageOptions::default(),
126            storage_location: None,
127            comment: None,
128            properties: HashMap::new(),
129        }
130    }
131
132    /// Set the table ID.
133    pub fn with_table_id(mut self, table_id: u32) -> Self {
134        self.table_id = table_id;
135        self
136    }
137
138    /// Set the primary key columns.
139    pub fn with_primary_key(mut self, columns: Vec<String>) -> Self {
140        self.primary_key = Some(columns);
141        self
142    }
143
144    /// Get a column by name.
145    ///
146    /// Returns `None` if the column doesn't exist.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use alopex_sql::catalog::{TableMetadata, ColumnMetadata};
152    /// use alopex_sql::planner::types::ResolvedType;
153    ///
154    /// let table = TableMetadata::new("users", vec![
155    ///     ColumnMetadata::new("id", ResolvedType::Integer),
156    ///     ColumnMetadata::new("name", ResolvedType::Text),
157    /// ]);
158    ///
159    /// assert!(table.get_column("id").is_some());
160    /// assert!(table.get_column("unknown").is_none());
161    /// ```
162    pub fn get_column(&self, name: &str) -> Option<&ColumnMetadata> {
163        self.columns.iter().find(|c| c.name == name)
164    }
165
166    /// Get the index of a column by name.
167    ///
168    /// Returns `None` if the column doesn't exist.
169    pub fn get_column_index(&self, name: &str) -> Option<usize> {
170        self.columns.iter().position(|c| c.name == name)
171    }
172
173    /// Get a list of all column names in definition order.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use alopex_sql::catalog::{TableMetadata, ColumnMetadata};
179    /// use alopex_sql::planner::types::ResolvedType;
180    ///
181    /// let table = TableMetadata::new("users", vec![
182    ///     ColumnMetadata::new("id", ResolvedType::Integer),
183    ///     ColumnMetadata::new("name", ResolvedType::Text),
184    ///     ColumnMetadata::new("age", ResolvedType::Integer),
185    /// ]);
186    ///
187    /// assert_eq!(table.column_names(), vec!["id", "name", "age"]);
188    /// ```
189    pub fn column_names(&self) -> Vec<&str> {
190        self.columns.iter().map(|c| c.name.as_str()).collect()
191    }
192
193    /// Get the number of columns in the table.
194    pub fn column_count(&self) -> usize {
195        self.columns.len()
196    }
197}
198
199/// Metadata for a column in a table.
200///
201/// Contains the column name, data type, and constraint information.
202///
203/// # Examples
204///
205/// ```
206/// use alopex_sql::catalog::ColumnMetadata;
207/// use alopex_sql::planner::types::ResolvedType;
208///
209/// let column = ColumnMetadata::new("id", ResolvedType::Integer)
210///     .with_not_null(true)
211///     .with_primary_key(true);
212///
213/// assert_eq!(column.name, "id");
214/// assert_eq!(column.data_type, ResolvedType::Integer);
215/// assert!(column.not_null);
216/// assert!(column.primary_key);
217/// ```
218#[derive(Debug, Clone)]
219pub struct ColumnMetadata {
220    /// Column name.
221    pub name: String,
222    /// Column data type (normalized).
223    pub data_type: ResolvedType,
224    /// NOT NULL constraint.
225    pub not_null: bool,
226    /// PRIMARY KEY constraint.
227    pub primary_key: bool,
228    /// UNIQUE constraint.
229    pub unique: bool,
230    /// DEFAULT value expression.
231    pub default: Option<Expr>,
232}
233
234impl ColumnMetadata {
235    /// Create a new column metadata with the given name and data type.
236    ///
237    /// All constraints default to `false`, and `default` is `None`.
238    pub fn new(name: impl Into<String>, data_type: ResolvedType) -> Self {
239        Self {
240            name: name.into(),
241            data_type,
242            not_null: false,
243            primary_key: false,
244            unique: false,
245            default: None,
246        }
247    }
248
249    /// Set the NOT NULL constraint.
250    pub fn with_not_null(mut self, not_null: bool) -> Self {
251        self.not_null = not_null;
252        self
253    }
254
255    /// Set the PRIMARY KEY constraint.
256    pub fn with_primary_key(mut self, primary_key: bool) -> Self {
257        self.primary_key = primary_key;
258        self
259    }
260
261    /// Set the UNIQUE constraint.
262    pub fn with_unique(mut self, unique: bool) -> Self {
263        self.unique = unique;
264        self
265    }
266
267    /// Set the DEFAULT value.
268    pub fn with_default(mut self, default: Expr) -> Self {
269        self.default = Some(default);
270        self
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_table_metadata_new() {
280        let table = TableMetadata::new("users", vec![]);
281        assert_eq!(table.table_id, 0);
282        assert_eq!(table.name, "users");
283        assert!(table.columns.is_empty());
284        assert!(table.primary_key.is_none());
285    }
286
287    #[test]
288    fn test_table_metadata_with_table_id() {
289        let table = TableMetadata::new("users", vec![]).with_table_id(42);
290        assert_eq!(table.table_id, 42);
291        assert_eq!(table.name, "users");
292    }
293
294    #[test]
295    fn test_table_metadata_with_columns() {
296        let columns = vec![
297            ColumnMetadata::new("id", ResolvedType::Integer),
298            ColumnMetadata::new("name", ResolvedType::Text),
299        ];
300        let table = TableMetadata::new("users", columns);
301
302        assert_eq!(table.columns.len(), 2);
303        assert_eq!(table.columns[0].name, "id");
304        assert_eq!(table.columns[1].name, "name");
305    }
306
307    #[test]
308    fn test_table_metadata_with_primary_key() {
309        let table = TableMetadata::new("users", vec![])
310            .with_primary_key(vec!["id".to_string(), "tenant_id".to_string()]);
311
312        assert_eq!(
313            table.primary_key,
314            Some(vec!["id".to_string(), "tenant_id".to_string()])
315        );
316    }
317
318    #[test]
319    fn test_get_column() {
320        let columns = vec![
321            ColumnMetadata::new("id", ResolvedType::Integer),
322            ColumnMetadata::new("name", ResolvedType::Text),
323        ];
324        let table = TableMetadata::new("users", columns);
325
326        let id_col = table.get_column("id");
327        assert!(id_col.is_some());
328        assert_eq!(id_col.unwrap().name, "id");
329        assert_eq!(id_col.unwrap().data_type, ResolvedType::Integer);
330
331        let name_col = table.get_column("name");
332        assert!(name_col.is_some());
333        assert_eq!(name_col.unwrap().data_type, ResolvedType::Text);
334
335        assert!(table.get_column("unknown").is_none());
336    }
337
338    #[test]
339    fn test_get_column_index() {
340        let columns = vec![
341            ColumnMetadata::new("id", ResolvedType::Integer),
342            ColumnMetadata::new("name", ResolvedType::Text),
343            ColumnMetadata::new("age", ResolvedType::Integer),
344        ];
345        let table = TableMetadata::new("users", columns);
346
347        assert_eq!(table.get_column_index("id"), Some(0));
348        assert_eq!(table.get_column_index("name"), Some(1));
349        assert_eq!(table.get_column_index("age"), Some(2));
350        assert_eq!(table.get_column_index("unknown"), None);
351    }
352
353    #[test]
354    fn test_column_names() {
355        let columns = vec![
356            ColumnMetadata::new("id", ResolvedType::Integer),
357            ColumnMetadata::new("name", ResolvedType::Text),
358            ColumnMetadata::new("age", ResolvedType::Integer),
359        ];
360        let table = TableMetadata::new("users", columns);
361
362        assert_eq!(table.column_names(), vec!["id", "name", "age"]);
363    }
364
365    #[test]
366    fn test_column_count() {
367        let table = TableMetadata::new(
368            "users",
369            vec![
370                ColumnMetadata::new("id", ResolvedType::Integer),
371                ColumnMetadata::new("name", ResolvedType::Text),
372            ],
373        );
374        assert_eq!(table.column_count(), 2);
375    }
376
377    #[test]
378    fn test_column_metadata_new() {
379        let column = ColumnMetadata::new("id", ResolvedType::Integer);
380
381        assert_eq!(column.name, "id");
382        assert_eq!(column.data_type, ResolvedType::Integer);
383        assert!(!column.not_null);
384        assert!(!column.primary_key);
385        assert!(!column.unique);
386        assert!(column.default.is_none());
387    }
388
389    #[test]
390    fn test_column_metadata_constraints() {
391        let column = ColumnMetadata::new("id", ResolvedType::Integer)
392            .with_not_null(true)
393            .with_primary_key(true)
394            .with_unique(true);
395
396        assert!(column.not_null);
397        assert!(column.primary_key);
398        assert!(column.unique);
399    }
400
401    #[test]
402    fn test_storage_options_default() {
403        let options = StorageOptions::default();
404        assert_eq!(options.storage_type, StorageType::Row);
405        assert_eq!(options.compression, Compression::Lz4);
406        assert_eq!(options.row_group_size, 100_000);
407        assert_eq!(options.row_id_mode, RowIdMode::Direct);
408    }
409
410    #[test]
411    fn test_table_metadata_sets_default_storage_options() {
412        let table = TableMetadata::new("users", vec![]);
413        assert_eq!(table.storage_options, StorageOptions::default());
414    }
415}