Skip to main content

drizzle_types/sqlite/ddl/
column.rs

1//! `SQLite` columns: [`ColumnDef`] (const) and [`Column`] (runtime).
2
3use crate::alloc_prelude::*;
4
5#[cfg(feature = "serde")]
6use crate::serde_helpers::{cow_from_string, cow_option_from_string};
7
8// =============================================================================
9// Generated Column Types
10// =============================================================================
11
12/// Whether a generated column is `STORED` or `VIRTUAL`.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
16pub enum GeneratedType {
17    /// Stored generated column
18    #[default]
19    Stored,
20    /// Virtual generated column
21    Virtual,
22}
23
24/// The `GENERATED ALWAYS AS (...)` part of a [`ColumnDef`].
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct GeneratedDef {
27    /// SQL expression for generation
28    pub expression: &'static str,
29    /// Generation type: stored or virtual
30    pub gen_type: GeneratedType,
31}
32
33impl GeneratedDef {
34    /// Creates a `STORED` generated column from a SQL expression.
35    #[must_use]
36    pub const fn stored(expression: &'static str) -> Self {
37        Self {
38            expression,
39            gen_type: GeneratedType::Stored,
40        }
41    }
42
43    /// Creates a `VIRTUAL` generated column from a SQL expression.
44    #[must_use]
45    pub const fn virtual_col(expression: &'static str) -> Self {
46        Self {
47            expression,
48            gen_type: GeneratedType::Virtual,
49        }
50    }
51
52    /// Converts to the runtime [`Generated`].
53    #[must_use]
54    pub const fn into_generated(self) -> Generated {
55        Generated {
56            expression: Cow::Borrowed(self.expression),
57            gen_type: self.gen_type,
58        }
59    }
60}
61
62/// The `GENERATED ALWAYS AS (...)` part of a [`Column`]. With `serde`, the
63/// fields are named `as` and `type`.
64#[derive(Clone, Debug, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
67pub struct Generated {
68    /// SQL expression for generation
69    #[cfg_attr(
70        feature = "serde",
71        serde(rename = "as", deserialize_with = "cow_from_string")
72    )]
73    pub expression: Cow<'static, str>,
74    /// Generation type: stored or virtual
75    #[cfg_attr(feature = "serde", serde(rename = "type"))]
76    pub gen_type: GeneratedType,
77}
78
79// =============================================================================
80// Const-friendly Definition Type
81// =============================================================================
82
83/// How a [`ColumnDef`] is a primary key: plain `PRIMARY KEY` or
84/// `PRIMARY KEY AUTOINCREMENT`. A `None` in the `primary_key` field means it
85/// is not one.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87pub enum PrimaryKeyKind {
88    /// Plain `PRIMARY KEY`
89    Plain,
90    /// `PRIMARY KEY AUTOINCREMENT`
91    Autoincrement,
92}
93
94/// A column definition that can be built in a `const`.
95///
96/// Builder methods set one property each; [`into_column`](Self::into_column)
97/// converts to the runtime [`Column`].
98///
99/// # Examples
100///
101/// ```
102/// use drizzle_types::sqlite::ddl::ColumnDef;
103///
104/// const ID: ColumnDef = ColumnDef::new("users", "id", "INTEGER")
105///     .primary_key()
106///     .autoincrement();
107///
108/// const COLUMNS: &[ColumnDef] = &[
109///     ColumnDef::new("users", "id", "INTEGER").primary_key().autoincrement(),
110///     ColumnDef::new("users", "name", "TEXT").not_null(),
111///     ColumnDef::new("users", "email", "TEXT"),
112/// ];
113/// ```
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub struct ColumnDef {
116    /// Parent table name
117    pub table: &'static str,
118    /// Column name
119    pub name: &'static str,
120    /// SQL type, such as `"INTEGER"` or `"TEXT"`
121    pub sql_type: &'static str,
122    /// `NOT NULL`
123    pub not_null: bool,
124    /// Primary key form; `None` if not a primary key
125    pub primary_key: Option<PrimaryKeyKind>,
126    /// `UNIQUE`
127    pub unique: bool,
128    /// `DEFAULT` value as SQL, such as `"0"` or `"'active'"`
129    pub default: Option<&'static str>,
130    /// Generated column configuration
131    pub generated: Option<GeneratedDef>,
132    /// Collation name (`BINARY`, `NOCASE`, `RTRIM`, or a custom registered collation).
133    /// `None` means the default collation (`BINARY`) and no `COLLATE` clause is emitted.
134    pub collate: Option<&'static str>,
135}
136
137impl ColumnDef {
138    /// Creates a nullable column with no constraints.
139    #[must_use]
140    pub const fn new(table: &'static str, name: &'static str, sql_type: &'static str) -> Self {
141        Self {
142            table,
143            name,
144            sql_type,
145            not_null: false,
146            primary_key: None,
147            unique: false,
148            default: None,
149            generated: None,
150            collate: None,
151        }
152    }
153
154    /// Adds `NOT NULL`.
155    #[must_use]
156    pub const fn not_null(self) -> Self {
157        Self {
158            not_null: true,
159            ..self
160        }
161    }
162
163    /// Makes the column `PRIMARY KEY AUTOINCREMENT` (and `NOT NULL`).
164    #[must_use]
165    pub const fn autoincrement(self) -> Self {
166        Self {
167            primary_key: Some(PrimaryKeyKind::Autoincrement),
168            not_null: true,
169            ..self
170        }
171    }
172
173    /// Makes the column `PRIMARY KEY` (and `NOT NULL`), keeping
174    /// `AUTOINCREMENT` if already set.
175    #[must_use]
176    pub const fn primary_key(self) -> Self {
177        let primary_key = match self.primary_key {
178            Some(kind) => Some(kind),
179            None => Some(PrimaryKeyKind::Plain),
180        };
181        Self {
182            primary_key,
183            not_null: true,
184            ..self
185        }
186    }
187
188    /// Same as [`primary_key`](Self::primary_key()).
189    #[must_use]
190    pub const fn primary(self) -> Self {
191        self.primary_key()
192    }
193
194    /// Adds `UNIQUE`.
195    #[must_use]
196    pub const fn unique(self) -> Self {
197        Self {
198            unique: true,
199            ..self
200        }
201    }
202
203    /// Sets the `DEFAULT` value, written as SQL.
204    #[must_use]
205    pub const fn default_value(self, value: &'static str) -> Self {
206        Self {
207            default: Some(value),
208            ..self
209        }
210    }
211
212    /// Makes the column `GENERATED ALWAYS AS (expression) STORED`.
213    #[must_use]
214    pub const fn generated_stored(self, expression: &'static str) -> Self {
215        Self {
216            generated: Some(GeneratedDef::stored(expression)),
217            ..self
218        }
219    }
220
221    /// Makes the column `GENERATED ALWAYS AS (expression) VIRTUAL`.
222    #[must_use]
223    pub const fn generated_virtual(self, expression: &'static str) -> Self {
224        Self {
225            generated: Some(GeneratedDef::virtual_col(expression)),
226            ..self
227        }
228    }
229
230    /// Sets the column's `COLLATE` sequence.
231    ///
232    /// `name` should be a built-in collation (`BINARY`, `NOCASE`, `RTRIM`) or
233    /// one registered on the connection with `sqlite3_create_collation`.
234    #[must_use]
235    pub const fn collate(self, name: &'static str) -> Self {
236        Self {
237            collate: Some(name),
238            ..self
239        }
240    }
241
242    /// Converts to the runtime [`Column`].
243    #[must_use]
244    pub const fn into_column(self) -> Column {
245        Column {
246            table: Cow::Borrowed(self.table),
247            name: Cow::Borrowed(self.name),
248            sql_type: Cow::Borrowed(self.sql_type),
249            not_null: self.not_null,
250            autoincrement: match self.primary_key {
251                Some(PrimaryKeyKind::Autoincrement) => Some(true),
252                _ => None,
253            },
254            primary_key: if self.primary_key.is_some() {
255                Some(true)
256            } else {
257                None
258            },
259            unique: if self.unique { Some(true) } else { None },
260            default: match self.default {
261                Some(s) => Some(Cow::Borrowed(s)),
262                None => None,
263            },
264            generated: match self.generated {
265                Some(g) => Some(g.into_generated()),
266                None => None,
267            },
268            collate: match self.collate {
269                Some(s) => Some(Cow::Borrowed(s)),
270                None => None,
271            },
272            ordinal_position: None,
273        }
274    }
275}
276
277impl Default for ColumnDef {
278    fn default() -> Self {
279        Self::new("", "", "")
280    }
281}
282
283// =============================================================================
284// Runtime Type for Serde
285// =============================================================================
286
287/// A table column, as stored in migration snapshots.
288///
289/// The boolean constraints are `Option<bool>` to match the snapshot format;
290/// `None` and `Some(false)` both mean unset.
291#[derive(Clone, Debug, PartialEq, Eq)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
293#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
294pub struct Column {
295    /// Parent table name
296    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
297    pub table: Cow<'static, str>,
298
299    /// Column name
300    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
301    pub name: Cow<'static, str>,
302
303    /// SQL type, such as `"INTEGER"` (serialized as `type`)
304    #[cfg_attr(
305        feature = "serde",
306        serde(rename = "type", deserialize_with = "cow_from_string")
307    )]
308    pub sql_type: Cow<'static, str>,
309
310    /// Is this column NOT NULL?
311    #[cfg_attr(feature = "serde", serde(default))]
312    pub not_null: bool,
313
314    /// Is this column AUTOINCREMENT?
315    #[cfg_attr(feature = "serde", serde(default))]
316    pub autoincrement: Option<bool>,
317
318    /// Is this column a PRIMARY KEY?
319    #[cfg_attr(
320        feature = "serde",
321        serde(default, skip_serializing_if = "Option::is_none")
322    )]
323    pub primary_key: Option<bool>,
324
325    /// Is this column UNIQUE?
326    #[cfg_attr(
327        feature = "serde",
328        serde(default, skip_serializing_if = "Option::is_none")
329    )]
330    pub unique: Option<bool>,
331
332    /// `DEFAULT` value as SQL
333    #[cfg_attr(
334        feature = "serde",
335        serde(default, deserialize_with = "cow_option_from_string")
336    )]
337    pub default: Option<Cow<'static, str>>,
338
339    /// Generated column configuration
340    #[cfg_attr(feature = "serde", serde(default))]
341    pub generated: Option<Generated>,
342
343    /// Collation sequence (`BINARY`, `NOCASE`, `RTRIM`, or custom). `None` means
344    /// the default `BINARY` collation and no `COLLATE` clause is emitted.
345    #[cfg_attr(
346        feature = "serde",
347        serde(default, deserialize_with = "cow_option_from_string")
348    )]
349    pub collate: Option<Cow<'static, str>>,
350
351    /// Ordinal position within the table (cid, 0-based).
352    ///
353    /// This is primarily populated by introspection and used for stable codegen ordering.
354    #[cfg_attr(
355        feature = "serde",
356        serde(default, skip_serializing_if = "Option::is_none")
357    )]
358    pub ordinal_position: Option<i32>,
359}
360
361impl Column {
362    /// Creates a nullable column with no constraints.
363    #[must_use]
364    pub fn new(
365        table: impl Into<Cow<'static, str>>,
366        name: impl Into<Cow<'static, str>>,
367        sql_type: impl Into<Cow<'static, str>>,
368    ) -> Self {
369        Self {
370            table: table.into(),
371            name: name.into(),
372            sql_type: sql_type.into(),
373            not_null: false,
374            autoincrement: None,
375            primary_key: None,
376            unique: None,
377            default: None,
378            generated: None,
379            collate: None,
380            ordinal_position: None,
381        }
382    }
383
384    /// Adds `NOT NULL`.
385    #[must_use]
386    pub const fn not_null(mut self) -> Self {
387        self.not_null = true;
388        self
389    }
390
391    /// Marks the column `AUTOINCREMENT`. Unlike [`ColumnDef::autoincrement`],
392    /// this does not set `primary_key` or `not_null`.
393    #[must_use]
394    pub const fn autoincrement(mut self) -> Self {
395        self.autoincrement = Some(true);
396        self
397    }
398
399    /// Sets the `DEFAULT` value, written as SQL.
400    #[must_use]
401    pub fn default_value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
402        self.default = Some(value.into());
403        self
404    }
405
406    /// Returns the column name.
407    #[inline]
408    #[must_use]
409    pub fn name(&self) -> &str {
410        &self.name
411    }
412
413    /// Returns the table name.
414    #[inline]
415    #[must_use]
416    pub fn table(&self) -> &str {
417        &self.table
418    }
419
420    /// Returns the SQL type.
421    #[inline]
422    #[must_use]
423    pub fn sql_type(&self) -> &str {
424        &self.sql_type
425    }
426
427    /// Returns `true` if `primary_key` is `Some(true)`.
428    #[inline]
429    #[must_use]
430    pub const fn is_primary_key(&self) -> bool {
431        matches!(self.primary_key, Some(true))
432    }
433
434    /// Returns `true` if `autoincrement` is `Some(true)`.
435    #[inline]
436    #[must_use]
437    pub const fn is_autoincrement(&self) -> bool {
438        matches!(self.autoincrement, Some(true))
439    }
440
441    /// Returns `true` if `unique` is `Some(true)`.
442    #[inline]
443    #[must_use]
444    pub const fn is_unique(&self) -> bool {
445        matches!(self.unique, Some(true))
446    }
447}
448
449impl Default for Column {
450    fn default() -> Self {
451        Self::new("", "", "")
452    }
453}
454
455impl From<ColumnDef> for Column {
456    fn from(def: ColumnDef) -> Self {
457        let mut col = def.into_column();
458        // Handle generated conversion at runtime
459        if let Some(generated_def) = def.generated {
460            col.generated = Some(generated_def.into_generated());
461        }
462        col
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_const_column_def() {
472        const COL_DEF: ColumnDef = ColumnDef::new("users", "id", "INTEGER")
473            .primary_key()
474            .autoincrement();
475
476        assert_eq!(COL_DEF.name, "id");
477        assert_eq!(COL_DEF.table, "users");
478        assert_eq!(COL_DEF.sql_type, "INTEGER");
479        const {
480            assert!(COL_DEF.not_null);
481        }
482        const {
483            assert!(COL_DEF.primary_key.is_some());
484        }
485        const {
486            assert!(matches!(
487                COL_DEF.primary_key,
488                Some(PrimaryKeyKind::Autoincrement)
489            ));
490        }
491
492        let col: Column = COL_DEF.into_column();
493
494        assert_eq!(col.name, Cow::Borrowed("id"));
495        assert_eq!(col.table, Cow::Borrowed("users"));
496        assert_eq!(col.sql_type, Cow::Borrowed("INTEGER"));
497        assert!(col.not_null);
498        // assert!(COL.primary_key);
499        // assert!(COL.autoincrement);
500    }
501
502    #[test]
503    fn test_const_columns_array() {
504        const COLUMNS: &[ColumnDef] = &[
505            ColumnDef::new("users", "id", "INTEGER")
506                .primary_key()
507                .autoincrement(),
508            ColumnDef::new("users", "name", "TEXT").not_null(),
509            ColumnDef::new("users", "email", "TEXT"),
510        ];
511
512        assert_eq!(COLUMNS.len(), 3);
513        assert_eq!(COLUMNS[0].name, "id");
514        assert_eq!(COLUMNS[1].name, "name");
515        assert_eq!(COLUMNS[2].name, "email");
516        assert!(COLUMNS[1].not_null);
517        assert!(!COLUMNS[2].not_null);
518    }
519
520    #[test]
521    fn test_generated_column() {
522        const GEN_COL: ColumnDef = ColumnDef::new("users", "full_name", "TEXT")
523            .generated_stored("first_name || ' ' || last_name");
524
525        assert!(GEN_COL.generated.is_some());
526        assert_eq!(GEN_COL.generated.unwrap().gen_type, GeneratedType::Stored);
527    }
528
529    #[cfg(feature = "serde")]
530    #[test]
531    fn test_serde_roundtrip() {
532        let col = Column::new("users", "id", "INTEGER");
533        let json = serde_json::to_string(&col).unwrap();
534        let parsed: Column = serde_json::from_str(&json).unwrap();
535        assert_eq!(parsed.name(), "id");
536    }
537}