oxide_sql_core/migrations/
column_builder.rs

1//! Type-safe column definition builder.
2//!
3//! Provides a fluent API for defining columns in migrations with compile-time
4//! validation of constraints.
5
6use crate::ast::DataType;
7
8/// A reference to a foreign key in another table.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ForeignKeyRef {
11    /// The referenced table name.
12    pub table: String,
13    /// The referenced column name.
14    pub column: String,
15    /// Action on delete.
16    pub on_delete: Option<ForeignKeyAction>,
17    /// Action on update.
18    pub on_update: Option<ForeignKeyAction>,
19}
20
21/// Foreign key referential action.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ForeignKeyAction {
24    /// No action.
25    NoAction,
26    /// Restrict deletion/update.
27    Restrict,
28    /// Cascade the operation.
29    Cascade,
30    /// Set to NULL.
31    SetNull,
32    /// Set to default value.
33    SetDefault,
34}
35
36impl ForeignKeyAction {
37    /// Returns the SQL representation of the action.
38    #[must_use]
39    pub fn as_sql(self) -> &'static str {
40        match self {
41            Self::NoAction => "NO ACTION",
42            Self::Restrict => "RESTRICT",
43            Self::Cascade => "CASCADE",
44            Self::SetNull => "SET NULL",
45            Self::SetDefault => "SET DEFAULT",
46        }
47    }
48}
49
50/// Default value for a column.
51#[derive(Debug, Clone, PartialEq)]
52pub enum DefaultValue {
53    /// NULL default.
54    Null,
55    /// Boolean default.
56    Boolean(bool),
57    /// Integer default.
58    Integer(i64),
59    /// Float default.
60    Float(f64),
61    /// String default.
62    String(String),
63    /// Raw SQL expression (e.g., CURRENT_TIMESTAMP).
64    Expression(String),
65}
66
67impl DefaultValue {
68    /// Returns the SQL representation of the default value.
69    #[must_use]
70    pub fn to_sql(&self) -> String {
71        match self {
72            Self::Null => String::from("NULL"),
73            Self::Boolean(b) => {
74                if *b {
75                    String::from("TRUE")
76                } else {
77                    String::from("FALSE")
78                }
79            }
80            Self::Integer(i) => i.to_string(),
81            Self::Float(f) => f.to_string(),
82            Self::String(s) => format!("'{}'", s.replace('\'', "''")),
83            Self::Expression(expr) => expr.clone(),
84        }
85    }
86}
87
88/// A complete column definition for migrations.
89#[derive(Debug, Clone, PartialEq)]
90pub struct ColumnDefinition {
91    /// Column name.
92    pub name: String,
93    /// Data type.
94    pub data_type: DataType,
95    /// Whether the column is nullable.
96    pub nullable: bool,
97    /// Default value.
98    pub default: Option<DefaultValue>,
99    /// Whether this is a primary key.
100    pub primary_key: bool,
101    /// Whether this column is unique.
102    pub unique: bool,
103    /// Whether this column auto-increments.
104    pub autoincrement: bool,
105    /// Foreign key reference, if any.
106    pub references: Option<ForeignKeyRef>,
107    /// Check constraint expression, if any.
108    pub check: Option<String>,
109    /// Collation for string columns.
110    pub collation: Option<String>,
111}
112
113impl ColumnDefinition {
114    /// Creates a new column definition.
115    #[must_use]
116    pub fn new(name: impl Into<String>, data_type: DataType) -> Self {
117        Self {
118            name: name.into(),
119            data_type,
120            nullable: true,
121            default: None,
122            primary_key: false,
123            unique: false,
124            autoincrement: false,
125            references: None,
126            check: None,
127            collation: None,
128        }
129    }
130}
131
132/// Type-safe column definition builder.
133///
134/// Provides a fluent API for building column definitions with all constraints.
135#[derive(Debug, Clone)]
136pub struct ColumnBuilder {
137    name: String,
138    data_type: DataType,
139    nullable: bool,
140    default: Option<DefaultValue>,
141    primary_key: bool,
142    unique: bool,
143    autoincrement: bool,
144    references: Option<ForeignKeyRef>,
145    check: Option<String>,
146    collation: Option<String>,
147}
148
149impl ColumnBuilder {
150    /// Creates a new column builder with name and type.
151    #[must_use]
152    pub fn new(name: impl Into<String>, data_type: DataType) -> Self {
153        Self {
154            name: name.into(),
155            data_type,
156            nullable: true,
157            default: None,
158            primary_key: false,
159            unique: false,
160            autoincrement: false,
161            references: None,
162            check: None,
163            collation: None,
164        }
165    }
166
167    /// Marks the column as NOT NULL.
168    #[must_use]
169    pub fn not_null(mut self) -> Self {
170        self.nullable = false;
171        self
172    }
173
174    /// Marks the column as nullable (default).
175    #[must_use]
176    pub fn nullable(mut self) -> Self {
177        self.nullable = true;
178        self
179    }
180
181    /// Marks the column as PRIMARY KEY.
182    #[must_use]
183    pub fn primary_key(mut self) -> Self {
184        self.primary_key = true;
185        self.nullable = false; // Primary keys are implicitly NOT NULL
186        self
187    }
188
189    /// Marks the column as UNIQUE.
190    #[must_use]
191    pub fn unique(mut self) -> Self {
192        self.unique = true;
193        self
194    }
195
196    /// Marks the column as AUTOINCREMENT.
197    #[must_use]
198    pub fn autoincrement(mut self) -> Self {
199        self.autoincrement = true;
200        self
201    }
202
203    /// Sets a boolean default value.
204    #[must_use]
205    pub fn default_bool(mut self, value: bool) -> Self {
206        self.default = Some(DefaultValue::Boolean(value));
207        self
208    }
209
210    /// Sets an integer default value.
211    #[must_use]
212    pub fn default_int(mut self, value: i64) -> Self {
213        self.default = Some(DefaultValue::Integer(value));
214        self
215    }
216
217    /// Sets a float default value.
218    #[must_use]
219    pub fn default_float(mut self, value: f64) -> Self {
220        self.default = Some(DefaultValue::Float(value));
221        self
222    }
223
224    /// Sets a string default value.
225    #[must_use]
226    pub fn default_str(mut self, value: impl Into<String>) -> Self {
227        self.default = Some(DefaultValue::String(value.into()));
228        self
229    }
230
231    /// Sets a NULL default value.
232    #[must_use]
233    pub fn default_null(mut self) -> Self {
234        self.default = Some(DefaultValue::Null);
235        self
236    }
237
238    /// Sets a raw SQL expression as default (e.g., CURRENT_TIMESTAMP).
239    #[must_use]
240    pub fn default_expr(mut self, expr: impl Into<String>) -> Self {
241        self.default = Some(DefaultValue::Expression(expr.into()));
242        self
243    }
244
245    /// Sets a foreign key reference.
246    #[must_use]
247    pub fn references(mut self, table: impl Into<String>, column: impl Into<String>) -> Self {
248        self.references = Some(ForeignKeyRef {
249            table: table.into(),
250            column: column.into(),
251            on_delete: None,
252            on_update: None,
253        });
254        self
255    }
256
257    /// Sets a foreign key reference with ON DELETE action.
258    #[must_use]
259    pub fn references_on_delete(
260        mut self,
261        table: impl Into<String>,
262        column: impl Into<String>,
263        on_delete: ForeignKeyAction,
264    ) -> Self {
265        self.references = Some(ForeignKeyRef {
266            table: table.into(),
267            column: column.into(),
268            on_delete: Some(on_delete),
269            on_update: None,
270        });
271        self
272    }
273
274    /// Sets a foreign key reference with full options.
275    #[must_use]
276    pub fn references_full(
277        mut self,
278        table: impl Into<String>,
279        column: impl Into<String>,
280        on_delete: Option<ForeignKeyAction>,
281        on_update: Option<ForeignKeyAction>,
282    ) -> Self {
283        self.references = Some(ForeignKeyRef {
284            table: table.into(),
285            column: column.into(),
286            on_delete,
287            on_update,
288        });
289        self
290    }
291
292    /// Adds a CHECK constraint.
293    #[must_use]
294    pub fn check(mut self, expr: impl Into<String>) -> Self {
295        self.check = Some(expr.into());
296        self
297    }
298
299    /// Sets the collation for string columns.
300    #[must_use]
301    pub fn collation(mut self, collation: impl Into<String>) -> Self {
302        self.collation = Some(collation.into());
303        self
304    }
305
306    /// Builds the column definition.
307    #[must_use]
308    pub fn build(self) -> ColumnDefinition {
309        ColumnDefinition {
310            name: self.name,
311            data_type: self.data_type,
312            nullable: self.nullable,
313            default: self.default,
314            primary_key: self.primary_key,
315            unique: self.unique,
316            autoincrement: self.autoincrement,
317            references: self.references,
318            check: self.check,
319            collation: self.collation,
320        }
321    }
322}
323
324// =============================================================================
325// Shorthand Functions for Common Types
326// =============================================================================
327
328/// Creates an INTEGER column builder.
329#[must_use]
330pub fn integer(name: impl Into<String>) -> ColumnBuilder {
331    ColumnBuilder::new(name, DataType::Integer)
332}
333
334/// Creates a SMALLINT column builder.
335#[must_use]
336pub fn smallint(name: impl Into<String>) -> ColumnBuilder {
337    ColumnBuilder::new(name, DataType::Smallint)
338}
339
340/// Creates a BIGINT column builder.
341#[must_use]
342pub fn bigint(name: impl Into<String>) -> ColumnBuilder {
343    ColumnBuilder::new(name, DataType::Bigint)
344}
345
346/// Creates a REAL column builder.
347#[must_use]
348pub fn real(name: impl Into<String>) -> ColumnBuilder {
349    ColumnBuilder::new(name, DataType::Real)
350}
351
352/// Creates a DOUBLE column builder.
353#[must_use]
354pub fn double(name: impl Into<String>) -> ColumnBuilder {
355    ColumnBuilder::new(name, DataType::Double)
356}
357
358/// Creates a DECIMAL column builder.
359#[must_use]
360pub fn decimal(name: impl Into<String>, precision: u16, scale: u16) -> ColumnBuilder {
361    ColumnBuilder::new(
362        name,
363        DataType::Decimal {
364            precision: Some(precision),
365            scale: Some(scale),
366        },
367    )
368}
369
370/// Creates a NUMERIC column builder.
371#[must_use]
372pub fn numeric(name: impl Into<String>, precision: u16, scale: u16) -> ColumnBuilder {
373    ColumnBuilder::new(
374        name,
375        DataType::Numeric {
376            precision: Some(precision),
377            scale: Some(scale),
378        },
379    )
380}
381
382/// Creates a CHAR column builder.
383#[must_use]
384pub fn char(name: impl Into<String>, len: u32) -> ColumnBuilder {
385    ColumnBuilder::new(name, DataType::Char(Some(len)))
386}
387
388/// Creates a VARCHAR column builder.
389#[must_use]
390pub fn varchar(name: impl Into<String>, len: u32) -> ColumnBuilder {
391    ColumnBuilder::new(name, DataType::Varchar(Some(len)))
392}
393
394/// Creates a TEXT column builder.
395#[must_use]
396pub fn text(name: impl Into<String>) -> ColumnBuilder {
397    ColumnBuilder::new(name, DataType::Text)
398}
399
400/// Creates a BLOB column builder.
401#[must_use]
402pub fn blob(name: impl Into<String>) -> ColumnBuilder {
403    ColumnBuilder::new(name, DataType::Blob)
404}
405
406/// Creates a BINARY column builder.
407#[must_use]
408pub fn binary(name: impl Into<String>, len: u32) -> ColumnBuilder {
409    ColumnBuilder::new(name, DataType::Binary(Some(len)))
410}
411
412/// Creates a VARBINARY column builder.
413#[must_use]
414pub fn varbinary(name: impl Into<String>, len: u32) -> ColumnBuilder {
415    ColumnBuilder::new(name, DataType::Varbinary(Some(len)))
416}
417
418/// Creates a DATE column builder.
419#[must_use]
420pub fn date(name: impl Into<String>) -> ColumnBuilder {
421    ColumnBuilder::new(name, DataType::Date)
422}
423
424/// Creates a TIME column builder.
425#[must_use]
426pub fn time(name: impl Into<String>) -> ColumnBuilder {
427    ColumnBuilder::new(name, DataType::Time)
428}
429
430/// Creates a TIMESTAMP column builder.
431#[must_use]
432pub fn timestamp(name: impl Into<String>) -> ColumnBuilder {
433    ColumnBuilder::new(name, DataType::Timestamp)
434}
435
436/// Creates a DATETIME column builder.
437#[must_use]
438pub fn datetime(name: impl Into<String>) -> ColumnBuilder {
439    ColumnBuilder::new(name, DataType::Datetime)
440}
441
442/// Creates a BOOLEAN column builder.
443#[must_use]
444pub fn boolean(name: impl Into<String>) -> ColumnBuilder {
445    ColumnBuilder::new(name, DataType::Boolean)
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_basic_column() {
454        let col = integer("id").build();
455        assert_eq!(col.name, "id");
456        assert_eq!(col.data_type, DataType::Integer);
457        assert!(col.nullable);
458        assert!(!col.primary_key);
459    }
460
461    #[test]
462    fn test_primary_key_column() {
463        let col = bigint("id").primary_key().autoincrement().build();
464        assert_eq!(col.name, "id");
465        assert!(col.primary_key);
466        assert!(col.autoincrement);
467        assert!(!col.nullable); // Primary key implies NOT NULL
468    }
469
470    #[test]
471    fn test_varchar_column() {
472        let col = varchar("username", 255).not_null().unique().build();
473        assert_eq!(col.data_type, DataType::Varchar(Some(255)));
474        assert!(!col.nullable);
475        assert!(col.unique);
476    }
477
478    #[test]
479    fn test_column_with_default() {
480        let col = boolean("active").not_null().default_bool(true).build();
481        assert_eq!(col.default, Some(DefaultValue::Boolean(true)));
482
483        let col = integer("count").default_int(0).build();
484        assert_eq!(col.default, Some(DefaultValue::Integer(0)));
485
486        let col = timestamp("created_at")
487            .not_null()
488            .default_expr("CURRENT_TIMESTAMP")
489            .build();
490        assert_eq!(
491            col.default,
492            Some(DefaultValue::Expression("CURRENT_TIMESTAMP".to_string()))
493        );
494    }
495
496    #[test]
497    fn test_foreign_key_column() {
498        let col = bigint("user_id")
499            .not_null()
500            .references_on_delete("users", "id", ForeignKeyAction::Cascade)
501            .build();
502
503        assert!(col.references.is_some());
504        let fk = col.references.unwrap();
505        assert_eq!(fk.table, "users");
506        assert_eq!(fk.column, "id");
507        assert_eq!(fk.on_delete, Some(ForeignKeyAction::Cascade));
508    }
509
510    #[test]
511    fn test_column_with_check() {
512        let col = integer("age").not_null().check("age >= 0").build();
513        assert_eq!(col.check, Some("age >= 0".to_string()));
514    }
515
516    #[test]
517    fn test_default_value_to_sql() {
518        assert_eq!(DefaultValue::Null.to_sql(), "NULL");
519        assert_eq!(DefaultValue::Boolean(true).to_sql(), "TRUE");
520        assert_eq!(DefaultValue::Boolean(false).to_sql(), "FALSE");
521        assert_eq!(DefaultValue::Integer(42).to_sql(), "42");
522        assert_eq!(DefaultValue::Float(3.15).to_sql(), "3.15");
523        assert_eq!(DefaultValue::String("hello".into()).to_sql(), "'hello'");
524        assert_eq!(DefaultValue::String("it's".into()).to_sql(), "'it''s'"); // Escaped
525        assert_eq!(
526            DefaultValue::Expression("CURRENT_TIMESTAMP".into()).to_sql(),
527            "CURRENT_TIMESTAMP"
528        );
529    }
530}