Skip to main content

turso_orm/entity/
schema.rs

1//! Schema generation from entities, modeled by [`Schema`].
2//!
3//! The generator reads the static column, key and relation definitions of an
4//! entity and produces `turso_sql` DDL builders; it never executes anything,
5//! so callers decide whether to run the statements directly or hand them to
6//! a migration. Column defaults are inlined as literals by the SQL layer
7//! because SQLite does not bind parameters in DDL.
8//!
9//! A single-column primary key is declared inline on the column so that an
10//! integral key becomes a true `INTEGER PRIMARY KEY` row id alias; composite
11//! keys fall back to a table-level `PRIMARY KEY (...)` clause.
12
13use turso_sql::{ColumnDef as SqlColumnDef, CreateIndex, CreateTable, Table};
14
15use super::base_entity::EntityTrait;
16use super::column::ColumnTrait;
17use super::iden::{IdenStatic, Iterable};
18use super::primary_key::{PrimaryKeyToColumn, PrimaryKeyTrait};
19use super::relation::RelationTrait;
20
21/// A generator of DDL statements from entity definitions.
22#[derive(Debug, Default)]
23pub struct Schema {
24    /// Whether generated tables carry the `STRICT` table option.
25    strict: bool,
26}
27
28impl Schema {
29    /// A schema generator with the default, non-strict settings.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Sets whether generated tables are declared `STRICT`.
35    #[must_use]
36    pub fn strict(mut self, strict: bool) -> Self {
37        self.strict = strict;
38        self
39    }
40
41    /// Builds the `CREATE TABLE` statement for an entity, including the
42    /// foreign keys implied by its `belongs_to` relations.
43    pub fn create_table_from_entity<E: EntityTrait>(&self, _: E) -> CreateTable {
44        let mut create = Table::create().table(E::TABLE_NAME);
45        let pks: Vec<E::PrimaryKey> = E::PrimaryKey::iter().collect();
46        let single_pk = pks.len() == 1;
47        for column in E::Column::iter() {
48            let def = column.def();
49            let mut col = SqlColumnDef::new(column.as_str(), def.ty);
50            if !def.nullable {
51                col = col.not_null();
52            }
53            if def.unique {
54                col = col.unique_key();
55            }
56            if let Some(d) = def.default {
57                col = col.default(d);
58            }
59            // Only a single-column key is declared inline; that is what makes
60            // an integral key an `INTEGER PRIMARY KEY` row id alias.
61            let is_pk = E::PrimaryKey::from_column(column).is_some();
62            if is_pk && single_pk {
63                col = col.primary_key();
64                if E::PrimaryKey::auto_increment() {
65                    col = col.auto_increment();
66                }
67            }
68            create = create.col(col);
69        }
70        if !single_pk && !pks.is_empty() {
71            create = create.primary_key(pks.iter().map(IdenStatic::as_str));
72        }
73        for relation in E::Relation::iter() {
74            if let Some(fk) = relation.def().foreign_key() {
75                create = create.foreign_key(fk);
76            }
77        }
78        if self.strict {
79            create = create.strict();
80        }
81        create
82    }
83
84    /// Builds one `CREATE INDEX IF NOT EXISTS` statement per column marked `indexed`.
85    ///
86    /// Index names follow `idx-<table>-<column>` so that they are predictable
87    /// for migrations that need to drop them later.
88    pub fn create_index_from_entity<E: EntityTrait>(&self, _: E) -> Vec<CreateIndex> {
89        E::Column::iter()
90            .filter(|c| c.def().indexed)
91            .map(|c| {
92                CreateIndex::new()
93                    .name(format!("idx-{}-{}", E::TABLE_NAME, c.as_str()))
94                    .table(E::TABLE_NAME)
95                    .col(c.as_str())
96                    .if_not_exists()
97            })
98            .collect()
99    }
100}