Skip to main content

turso_sql/
schema.rs

1//! DDL statement builders for tables and indexes.
2//!
3//! SQLite has only four storage classes, so a schema cannot express "date"
4//! or "boolean" natively. This module owns the logical [`ColumnType`] that
5//! bridges that gap: it decides the declared type written in DDL — which is
6//! what `STRICT` tables enforce — and tells `turso-orm` how to decode the
7//! column back. Everything else here is a thin builder over the SQLite DDL
8//! grammar, including the Turso extensions (`ALTER COLUMN`, `USING fts`,
9//! `WITHOUT ROWID`) that are opt-in on the engine side.
10//!
11//! The module owns the DDL AST only. Rendering lives in `crate::writer`,
12//! which also inlines column defaults as literals because DDL cannot carry
13//! bound parameters.
14//!
15//! - [`Table`]: the entry point to `CREATE`, `ALTER` and `DROP TABLE`;
16//! - [`ColumnDef`], [`ColumnType`], [`TableConstraint`], [`ForeignKey`]:
17//!   the pieces of a table definition;
18//! - [`CreateTable`], [`AlterTable`], [`DropTable`], [`CreateIndex`],
19//!   [`DropIndex`]: the statements.
20
21use crate::expr::Expr;
22use crate::iden::{Ident, IntoIden};
23
24/// The entry point to the DDL builders.
25#[derive(Debug)]
26pub struct Table;
27
28impl Table {
29    /// Starts a `CREATE TABLE`.
30    pub fn create() -> CreateTable {
31        CreateTable::default()
32    }
33
34    /// Starts an `ALTER TABLE`.
35    pub fn alter() -> AlterTable {
36        AlterTable::default()
37    }
38
39    /// Starts a `DROP TABLE`.
40    pub fn drop() -> DropTable {
41        DropTable::default()
42    }
43}
44
45/// The logical column types.
46///
47/// SQLite has only four storage classes; the logical type decides the
48/// declared type written in DDL (which also drives `STRICT` tables) and how
49/// `turso-orm` decodes values. Every type maps to one of `INTEGER`, `REAL`,
50/// `TEXT` and `BLOB`.
51#[derive(Clone, Debug, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum ColumnType {
54    /// An `INTEGER`.
55    Integer,
56    /// An `INTEGER` holding `0` or `1`.
57    Boolean,
58    /// A `REAL`.
59    Real,
60    /// A `TEXT`.
61    Text,
62    /// A `TEXT` with an advisory maximum length that the engine does not
63    /// enforce.
64    String(Option<u32>),
65    /// A `BLOB`.
66    Blob,
67    /// A `TEXT` in `YYYY-MM-DD` form.
68    Date,
69    /// A `TEXT` in `HH:MM:SS.fff` form.
70    Time,
71    /// A `TEXT` in `YYYY-MM-DD HH:MM:SS.fff` form.
72    DateTime,
73    /// A `TEXT` in RFC 3339 form.
74    TimestampWithTimeZone,
75    /// A `TEXT` holding a hyphenated UUID.
76    Uuid,
77    /// A `TEXT` holding a JSON document.
78    Json,
79    /// A `TEXT` holding an exact decimal.
80    Decimal,
81    /// The `ANY` type, only meaningful in `STRICT` tables.
82    Any,
83    /// A custom declared type written verbatim.
84    Custom(String),
85}
86
87impl ColumnType {
88    /// The declared type written in DDL.
89    pub fn declared(&self) -> &str {
90        match self {
91            ColumnType::Integer | ColumnType::Boolean => "INTEGER",
92            ColumnType::Real => "REAL",
93            ColumnType::Text
94            | ColumnType::String(_)
95            | ColumnType::Date
96            | ColumnType::Time
97            | ColumnType::DateTime
98            | ColumnType::TimestampWithTimeZone
99            | ColumnType::Uuid
100            | ColumnType::Json
101            | ColumnType::Decimal => "TEXT",
102            ColumnType::Blob => "BLOB",
103            ColumnType::Any => "ANY",
104            ColumnType::Custom(s) => s,
105        }
106    }
107}
108
109/// The referential actions of a foreign key.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum ForeignKeyAction {
112    /// `NO ACTION`.
113    NoAction,
114    /// `RESTRICT`.
115    Restrict,
116    /// `CASCADE`.
117    Cascade,
118    /// `SET NULL`.
119    SetNull,
120    /// `SET DEFAULT`.
121    SetDefault,
122}
123
124impl ForeignKeyAction {
125    /// The action's SQL spelling.
126    pub(crate) fn sql(self) -> &'static str {
127        match self {
128            ForeignKeyAction::NoAction => "NO ACTION",
129            ForeignKeyAction::Restrict => "RESTRICT",
130            ForeignKeyAction::Cascade => "CASCADE",
131            ForeignKeyAction::SetNull => "SET NULL",
132            ForeignKeyAction::SetDefault => "SET DEFAULT",
133        }
134    }
135}
136
137/// A foreign-key constraint.
138#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct ForeignKey {
140    /// The optional constraint name.
141    pub name: Option<Ident>,
142    /// The local columns.
143    pub columns: Vec<Ident>,
144    /// The referenced table.
145    pub ref_table: Ident,
146    /// The referenced columns.
147    pub ref_columns: Vec<Ident>,
148    /// The `ON DELETE` action.
149    pub on_delete: Option<ForeignKeyAction>,
150    /// The `ON UPDATE` action.
151    pub on_update: Option<ForeignKeyAction>,
152}
153
154impl ForeignKey {
155    /// Builds `FOREIGN KEY (columns) REFERENCES table (ref_columns)`.
156    pub fn new<C: IntoIden, R: IntoIden>(
157        columns: impl IntoIterator<Item = C>,
158        ref_table: impl IntoIden,
159        ref_columns: impl IntoIterator<Item = R>,
160    ) -> Self {
161        Self {
162            name: None,
163            columns: columns.into_iter().map(IntoIden::into_iden).collect(),
164            ref_table: ref_table.into_iden(),
165            ref_columns: ref_columns.into_iter().map(IntoIden::into_iden).collect(),
166            on_delete: None,
167            on_update: None,
168        }
169    }
170
171    /// Sets the constraint name.
172    #[must_use]
173    pub fn name(mut self, name: impl IntoIden) -> Self {
174        self.name = Some(name.into_iden());
175        self
176    }
177
178    /// Sets `ON DELETE action`.
179    #[must_use]
180    pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
181        self.on_delete = Some(action);
182        self
183    }
184
185    /// Sets `ON UPDATE action`.
186    #[must_use]
187    pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
188        self.on_update = Some(action);
189        self
190    }
191}
192
193/// A column definition.
194#[derive(Clone, Debug, PartialEq)]
195#[allow(
196    clippy::struct_excessive_bools,
197    reason = "the flags mirror independent SQL column constraints"
198)]
199pub struct ColumnDef {
200    /// The column name.
201    pub name: Ident,
202    /// The logical type.
203    pub ty: ColumnType,
204    /// Whether `NOT NULL` is set.
205    pub not_null: bool,
206    /// Whether the column is a single-column `PRIMARY KEY`.
207    pub primary_key: bool,
208    /// Whether `AUTOINCREMENT` is set; requires an `INTEGER PRIMARY KEY`.
209    pub auto_increment: bool,
210    /// Whether `UNIQUE` is set.
211    pub unique: bool,
212    /// The `DEFAULT expr`, inlined as a literal when rendered.
213    pub default: Option<Expr>,
214    /// The `CHECK (expr)` constraint.
215    pub check: Option<Expr>,
216}
217
218impl ColumnDef {
219    /// A nullable column of the given type.
220    pub fn new(name: impl IntoIden, ty: ColumnType) -> Self {
221        Self {
222            name: name.into_iden(),
223            ty,
224            not_null: false,
225            primary_key: false,
226            auto_increment: false,
227            unique: false,
228            default: None,
229            check: None,
230        }
231    }
232
233    /// An `INTEGER` column.
234    pub fn integer(name: impl IntoIden) -> Self {
235        Self::new(name, ColumnType::Integer)
236    }
237
238    /// An `INTEGER` column holding a boolean.
239    pub fn boolean(name: impl IntoIden) -> Self {
240        Self::new(name, ColumnType::Boolean)
241    }
242
243    /// A `REAL` column.
244    pub fn real(name: impl IntoIden) -> Self {
245        Self::new(name, ColumnType::Real)
246    }
247
248    /// A `TEXT` column.
249    pub fn text(name: impl IntoIden) -> Self {
250        Self::new(name, ColumnType::Text)
251    }
252
253    /// A `TEXT` column with an advisory length.
254    pub fn string(name: impl IntoIden, len: Option<u32>) -> Self {
255        Self::new(name, ColumnType::String(len))
256    }
257
258    /// A `BLOB` column.
259    pub fn blob(name: impl IntoIden) -> Self {
260        Self::new(name, ColumnType::Blob)
261    }
262
263    /// A date column, stored as text.
264    pub fn date(name: impl IntoIden) -> Self {
265        Self::new(name, ColumnType::Date)
266    }
267
268    /// A naive timestamp column, stored as text.
269    pub fn date_time(name: impl IntoIden) -> Self {
270        Self::new(name, ColumnType::DateTime)
271    }
272
273    /// A zoned timestamp column, stored as RFC 3339 text.
274    pub fn timestamp_with_time_zone(name: impl IntoIden) -> Self {
275        Self::new(name, ColumnType::TimestampWithTimeZone)
276    }
277
278    /// A UUID column, stored as text.
279    pub fn uuid(name: impl IntoIden) -> Self {
280        Self::new(name, ColumnType::Uuid)
281    }
282
283    /// A JSON column, stored as text.
284    pub fn json(name: impl IntoIden) -> Self {
285        Self::new(name, ColumnType::Json)
286    }
287
288    /// A decimal column, stored as text.
289    pub fn decimal(name: impl IntoIden) -> Self {
290        Self::new(name, ColumnType::Decimal)
291    }
292
293    /// Sets `NOT NULL`.
294    #[must_use]
295    pub fn not_null(mut self) -> Self {
296        self.not_null = true;
297        self
298    }
299
300    /// Makes the column nullable, which is the default.
301    #[must_use]
302    pub fn null(mut self) -> Self {
303        self.not_null = false;
304        self
305    }
306
307    /// Sets `PRIMARY KEY`.
308    #[must_use]
309    pub fn primary_key(mut self) -> Self {
310        self.primary_key = true;
311        self
312    }
313
314    /// Sets `AUTOINCREMENT`.
315    #[must_use]
316    pub fn auto_increment(mut self) -> Self {
317        self.auto_increment = true;
318        self
319    }
320
321    /// Sets `UNIQUE`.
322    #[must_use]
323    pub fn unique_key(mut self) -> Self {
324        self.unique = true;
325        self
326    }
327
328    /// Sets `DEFAULT expr`.
329    #[must_use]
330    pub fn default(mut self, value: impl Into<Expr>) -> Self {
331        self.default = Some(value.into());
332        self
333    }
334
335    /// Sets `CHECK (expr)`.
336    #[must_use]
337    pub fn check(mut self, expr: Expr) -> Self {
338        self.check = Some(expr);
339        self
340    }
341}
342
343/// The table-level constraints.
344#[derive(Clone, Debug, PartialEq)]
345pub enum TableConstraint {
346    /// A composite `PRIMARY KEY (a, b)`.
347    PrimaryKey(Vec<Ident>),
348    /// A composite `UNIQUE (a, b)`.
349    Unique(Vec<Ident>),
350    /// A `CHECK (expr)` constraint.
351    Check(Expr),
352    /// A foreign key.
353    ForeignKey(ForeignKey),
354}
355
356/// A `CREATE TABLE` statement.
357#[derive(Clone, Debug, Default, PartialEq)]
358pub struct CreateTable {
359    /// The table name.
360    pub(crate) name: Option<Ident>,
361    /// Whether `IF NOT EXISTS` is set.
362    pub(crate) if_not_exists: bool,
363    /// The column definitions, in order.
364    pub(crate) columns: Vec<ColumnDef>,
365    /// The table-level constraints, rendered after the columns.
366    pub(crate) constraints: Vec<TableConstraint>,
367    /// Whether the table is `STRICT`.
368    pub(crate) strict: bool,
369    /// Whether the table is `WITHOUT ROWID`.
370    pub(crate) without_rowid: bool,
371}
372
373impl CreateTable {
374    /// Sets the table name.
375    #[must_use]
376    pub fn table(mut self, name: impl IntoIden) -> Self {
377        self.name = Some(name.into_iden());
378        self
379    }
380
381    /// Sets `IF NOT EXISTS`.
382    #[must_use]
383    pub fn if_not_exists(mut self) -> Self {
384        self.if_not_exists = true;
385        self
386    }
387
388    /// Adds a column.
389    #[must_use]
390    pub fn col(mut self, column: ColumnDef) -> Self {
391        self.columns.push(column);
392        self
393    }
394
395    /// Adds a composite primary key.
396    #[must_use]
397    pub fn primary_key<C: IntoIden>(mut self, columns: impl IntoIterator<Item = C>) -> Self {
398        self.constraints.push(TableConstraint::PrimaryKey(
399            columns.into_iter().map(IntoIden::into_iden).collect(),
400        ));
401        self
402    }
403
404    /// Adds a table-level unique constraint.
405    #[must_use]
406    pub fn unique<C: IntoIden>(mut self, columns: impl IntoIterator<Item = C>) -> Self {
407        self.constraints.push(TableConstraint::Unique(
408            columns.into_iter().map(IntoIden::into_iden).collect(),
409        ));
410        self
411    }
412
413    /// Adds a table-level check constraint.
414    #[must_use]
415    pub fn check(mut self, expr: Expr) -> Self {
416        self.constraints.push(TableConstraint::Check(expr));
417        self
418    }
419
420    /// Adds a foreign key.
421    #[must_use]
422    pub fn foreign_key(mut self, fk: ForeignKey) -> Self {
423        self.constraints.push(TableConstraint::ForeignKey(fk));
424        self
425    }
426
427    /// Makes the table `STRICT`, so the engine type-checks stored values
428    /// against the declared types.
429    #[must_use]
430    pub fn strict(mut self) -> Self {
431        self.strict = true;
432        self
433    }
434
435    /// Makes the table `WITHOUT ROWID` — experimental in Turso.
436    #[must_use]
437    pub fn without_rowid(mut self) -> Self {
438        self.without_rowid = true;
439        self
440    }
441
442    /// The columns defined so far.
443    pub fn columns(&self) -> &[ColumnDef] {
444        &self.columns
445    }
446
447    /// The table name, if set.
448    pub fn name(&self) -> Option<&Ident> {
449        self.name.as_ref()
450    }
451}
452
453/// The `ALTER TABLE` operations.
454#[derive(Clone, Debug, PartialEq)]
455pub(crate) enum AlterOp {
456    /// `ADD COLUMN`.
457    AddColumn(ColumnDef),
458    /// `DROP COLUMN`.
459    DropColumn(Ident),
460    /// `RENAME COLUMN a TO b`.
461    RenameColumn(Ident, Ident),
462    /// `RENAME TO`.
463    RenameTo(Ident),
464    /// The Turso extension that redefines a column in place.
465    AlterColumn(Ident, ColumnDef),
466}
467
468/// An `ALTER TABLE` statement.
469///
470/// SQLite accepts one operation per statement, so the builder keeps a single
471/// operation and the last setter wins.
472#[derive(Clone, Debug, Default, PartialEq)]
473pub struct AlterTable {
474    /// The table name.
475    pub(crate) name: Option<Ident>,
476    /// The single operation to apply.
477    pub(crate) op: Option<AlterOp>,
478}
479
480impl AlterTable {
481    /// Sets the table name.
482    #[must_use]
483    pub fn table(mut self, name: impl IntoIden) -> Self {
484        self.name = Some(name.into_iden());
485        self
486    }
487
488    /// Sets the operation to `ADD COLUMN`.
489    #[must_use]
490    pub fn add_column(mut self, column: ColumnDef) -> Self {
491        self.op = Some(AlterOp::AddColumn(column));
492        self
493    }
494
495    /// Sets the operation to `DROP COLUMN`.
496    #[must_use]
497    pub fn drop_column(mut self, column: impl IntoIden) -> Self {
498        self.op = Some(AlterOp::DropColumn(column.into_iden()));
499        self
500    }
501
502    /// Sets the operation to `RENAME COLUMN a TO b`.
503    #[must_use]
504    pub fn rename_column(mut self, from: impl IntoIden, to: impl IntoIden) -> Self {
505        self.op = Some(AlterOp::RenameColumn(from.into_iden(), to.into_iden()));
506        self
507    }
508
509    /// Sets the operation to `RENAME TO`.
510    #[must_use]
511    pub fn rename_to(mut self, to: impl IntoIden) -> Self {
512        self.op = Some(AlterOp::RenameTo(to.into_iden()));
513        self
514    }
515
516    /// Sets the operation to the Turso extension `ALTER COLUMN name TO <new
517    /// definition>`.
518    #[must_use]
519    pub fn alter_column(mut self, name: impl IntoIden, column: ColumnDef) -> Self {
520        self.op = Some(AlterOp::AlterColumn(name.into_iden(), column));
521        self
522    }
523}
524
525/// A `DROP TABLE` statement.
526#[derive(Clone, Debug, Default, PartialEq, Eq)]
527pub struct DropTable {
528    /// The table name.
529    pub(crate) name: Option<Ident>,
530    /// Whether `IF EXISTS` is set.
531    pub(crate) if_exists: bool,
532}
533
534impl DropTable {
535    /// Sets the table name.
536    #[must_use]
537    pub fn table(mut self, name: impl IntoIden) -> Self {
538        self.name = Some(name.into_iden());
539        self
540    }
541
542    /// Sets `IF EXISTS`.
543    #[must_use]
544    pub fn if_exists(mut self) -> Self {
545        self.if_exists = true;
546        self
547    }
548}
549
550/// A `CREATE INDEX` statement.
551#[derive(Clone, Debug, Default, PartialEq)]
552pub struct CreateIndex {
553    /// The index name.
554    pub(crate) name: Option<Ident>,
555    /// The indexed table.
556    pub(crate) table: Option<Ident>,
557    /// The indexed columns with their optional sort order.
558    pub(crate) columns: Vec<(Ident, Option<crate::Order>)>,
559    /// Whether the index is `UNIQUE`.
560    pub(crate) unique: bool,
561    /// Whether `IF NOT EXISTS` is set.
562    pub(crate) if_not_exists: bool,
563    /// The partial-index `WHERE` expression.
564    pub(crate) r#where: Option<Expr>,
565    /// The Turso `USING <method>` clause.
566    pub(crate) using: Option<&'static str>,
567}
568
569impl CreateIndex {
570    /// Starts a `CREATE INDEX`.
571    pub fn new() -> Self {
572        Self::default()
573    }
574
575    /// Sets the index name.
576    #[must_use]
577    pub fn name(mut self, name: impl IntoIden) -> Self {
578        self.name = Some(name.into_iden());
579        self
580    }
581
582    /// Sets the indexed table.
583    #[must_use]
584    pub fn table(mut self, table: impl IntoIden) -> Self {
585        self.table = Some(table.into_iden());
586        self
587    }
588
589    /// Adds an indexed column.
590    #[must_use]
591    pub fn col(mut self, column: impl IntoIden) -> Self {
592        self.columns.push((column.into_iden(), None));
593        self
594    }
595
596    /// Adds an indexed column with a sort order.
597    #[must_use]
598    pub fn col_order(mut self, column: impl IntoIden, order: crate::Order) -> Self {
599        self.columns.push((column.into_iden(), Some(order)));
600        self
601    }
602
603    /// Makes the index `UNIQUE`.
604    #[must_use]
605    pub fn unique(mut self) -> Self {
606        self.unique = true;
607        self
608    }
609
610    /// Sets `IF NOT EXISTS`.
611    #[must_use]
612    pub fn if_not_exists(mut self) -> Self {
613        self.if_not_exists = true;
614        self
615    }
616
617    /// Sets the partial-index `WHERE` expression.
618    #[must_use]
619    pub fn and_where(mut self, expr: Expr) -> Self {
620        self.r#where = Some(expr);
621        self
622    }
623
624    /// Sets the Turso extension `USING fts` / `USING <method>` —
625    /// experimental.
626    #[must_use]
627    pub fn using(mut self, method: &'static str) -> Self {
628        self.using = Some(method);
629        self
630    }
631}
632
633/// A `DROP INDEX` statement.
634#[derive(Clone, Debug, Default, PartialEq, Eq)]
635pub struct DropIndex {
636    /// The index name.
637    pub(crate) name: Option<Ident>,
638    /// Whether `IF EXISTS` is set.
639    pub(crate) if_exists: bool,
640}
641
642impl DropIndex {
643    /// Starts a `DROP INDEX`.
644    pub fn new() -> Self {
645        Self::default()
646    }
647
648    /// Sets the index name.
649    #[must_use]
650    pub fn name(mut self, name: impl IntoIden) -> Self {
651        self.name = Some(name.into_iden());
652        self
653    }
654
655    /// Sets `IF EXISTS`.
656    #[must_use]
657    pub fn if_exists(mut self) -> Self {
658        self.if_exists = true;
659        self
660    }
661}