Skip to main content

drizzle_types/sqlite/ddl/
column.rs

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