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