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}