Skip to main content

fse_schema/
sql.rs

1//! SQLite DDL generation from the schema model. Kept dialect-shaped (every
2//! statement goes through this module) so a second backend is a new module,
3//! not a rewrite.
4
5use crate::model::{ColumnDef, TableDef};
6
7/// One column definition line. `auto_pk` says whether this table uses the
8/// conventional `id: i64` surrogate key (rendered inline as
9/// `INTEGER PRIMARY KEY AUTOINCREMENT`); composite keys are rendered as a
10/// table constraint by [`create_table_sql`] instead.
11pub fn column_sql(c: &ColumnDef, auto_pk: bool) -> String {
12    let mut parts = vec![c.name.clone(), c.ty.sql().to_string()];
13    if c.primary_key && auto_pk {
14        parts.push("PRIMARY KEY AUTOINCREMENT".into());
15    }
16    if !c.nullable {
17        parts.push("NOT NULL".into());
18    }
19    if c.unique {
20        parts.push("UNIQUE".into());
21    }
22    if let Some(d) = &c.default {
23        parts.push(format!("DEFAULT {}", d.sql()));
24    }
25    if let Some(values) = &c.check_in
26        && !values.is_empty()
27    {
28        let list = values
29            .iter()
30            .map(|v| format!("'{}'", v.replace('\'', "''")))
31            .collect::<Vec<_>>()
32            .join(", ");
33        parts.push(format!("CHECK ({} IN ({list}))", c.name));
34    }
35    parts.join(" ")
36}
37
38pub fn create_table_sql(t: &TableDef) -> String {
39    let auto = t.auto_id();
40    let mut lines: Vec<String> = t.columns.iter().map(|c| column_sql(c, auto)).collect();
41
42    if !auto {
43        let pk: Vec<String> = t.primary_key().iter().map(|c| c.name.clone()).collect();
44        if !pk.is_empty() {
45            lines.push(format!("PRIMARY KEY ({})", pk.join(", ")));
46        }
47    }
48    for c in &t.columns {
49        if let Some(fk) = &c.references {
50            let mut line = format!("FOREIGN KEY ({}) REFERENCES {}({})", c.name, fk.table, fk.column);
51            if let Some(od) = fk.on_delete {
52                line.push_str(&format!(" ON DELETE {}", od.sql()));
53            }
54            lines.push(line);
55        }
56    }
57
58    format!("CREATE TABLE {} (\n    {}\n);", t.name, lines.join(",\n    "))
59}
60
61pub fn index_name(table: &str, column: &str) -> String {
62    format!("idx_{table}_{column}")
63}
64
65/// `CREATE INDEX` statements for every `#[orm(index)]` column.
66pub fn index_sqls(t: &TableDef) -> Vec<String> {
67    t.columns
68        .iter()
69        .filter(|c| c.index)
70        .map(|c| {
71            format!(
72                "CREATE INDEX {} ON {} ({});",
73                index_name(&t.name, &c.name),
74                t.name,
75                c.name
76            )
77        })
78        .collect()
79}
80
81pub fn composite_index_name(table: &str, columns: &[String]) -> String {
82    format!("idx_{table}_{}", columns.join("_"))
83}
84
85/// `CREATE [UNIQUE] INDEX` statements for every struct-level `#[orm(unique(...))]`
86/// / `#[orm(index(...))]` — composite constraints, enforced as indexes (not
87/// inline table constraints) so they can be added/dropped without a rebuild.
88pub fn composite_index_sqls(t: &TableDef) -> Vec<String> {
89    let mut out: Vec<String> = t
90        .composite_uniques
91        .iter()
92        .map(|cols| composite_index_sql(&t.name, cols, true))
93        .collect();
94    out.extend(t.composite_indexes.iter().map(|cols| composite_index_sql(&t.name, cols, false)));
95    out
96}
97
98fn composite_index_sql(table: &str, columns: &[String], unique: bool) -> String {
99    format!(
100        "CREATE {}INDEX {} ON {table} ({});",
101        if unique { "UNIQUE " } else { "" },
102        composite_index_name(table, columns),
103        columns.join(", "),
104    )
105}