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/// A table/column/index name, double-quoted for SQL. Every identifier the
8/// ORM emits goes through this, so names that collide with SQL keywords
9/// (`order`, `group`, `index`, ...) just work.
10pub fn quote_ident(name: &str) -> String {
11    format!("\"{}\"", name.replace('"', "\"\""))
12}
13
14/// One column definition line. `auto_pk` says whether this table uses the
15/// conventional `id: i64` surrogate key (rendered inline as
16/// `INTEGER PRIMARY KEY AUTOINCREMENT`); composite keys are rendered as a
17/// table constraint by [`create_table_sql`] instead.
18pub fn column_sql(c: &ColumnDef, auto_pk: bool) -> String {
19    let mut parts = vec![quote_ident(&c.name), c.ty.sql().to_string()];
20    if c.primary_key && auto_pk {
21        parts.push("PRIMARY KEY AUTOINCREMENT".into());
22    }
23    if !c.nullable {
24        parts.push("NOT NULL".into());
25    }
26    if c.unique {
27        parts.push("UNIQUE".into());
28    }
29    if let Some(d) = &c.default {
30        parts.push(format!("DEFAULT {}", d.sql()));
31    }
32    if let Some(values) = &c.check_in
33        && !values.is_empty()
34    {
35        let list = values
36            .iter()
37            .map(|v| format!("'{}'", v.replace('\'', "''")))
38            .collect::<Vec<_>>()
39            .join(", ");
40        parts.push(format!("CHECK ({} IN ({list}))", quote_ident(&c.name)));
41    }
42    parts.join(" ")
43}
44
45pub fn create_table_sql(t: &TableDef) -> String {
46    let auto = t.auto_id();
47    let mut lines: Vec<String> = t.columns.iter().map(|c| column_sql(c, auto)).collect();
48
49    if !auto {
50        let pk: Vec<String> = t
51            .primary_key()
52            .iter()
53            .map(|c| quote_ident(&c.name))
54            .collect();
55        if !pk.is_empty() {
56            lines.push(format!("PRIMARY KEY ({})", pk.join(", ")));
57        }
58    }
59    for c in &t.columns {
60        if let Some(fk) = &c.references {
61            let mut line = format!(
62                "FOREIGN KEY ({}) REFERENCES {}({})",
63                quote_ident(&c.name),
64                quote_ident(&fk.table),
65                quote_ident(&fk.column)
66            );
67            if let Some(od) = fk.on_delete {
68                line.push_str(&format!(" ON DELETE {}", od.sql()));
69            }
70            lines.push(line);
71        }
72    }
73
74    format!(
75        "CREATE TABLE {} (\n    {}\n);",
76        quote_ident(&t.name),
77        lines.join(",\n    ")
78    )
79}
80
81pub fn index_name(table: &str, column: &str) -> String {
82    format!("idx_{table}_{column}")
83}
84
85/// `CREATE INDEX` statements for every `#[orm(index)]` column.
86pub fn index_sqls(t: &TableDef) -> Vec<String> {
87    t.columns
88        .iter()
89        .filter(|c| c.index)
90        .map(|c| {
91            format!(
92                "CREATE INDEX {} ON {} ({});",
93                quote_ident(&index_name(&t.name, &c.name)),
94                quote_ident(&t.name),
95                quote_ident(&c.name)
96            )
97        })
98        .collect()
99}
100
101pub fn composite_index_name(table: &str, columns: &[String]) -> String {
102    format!("idx_{table}_{}", columns.join("_"))
103}
104
105/// `CREATE [UNIQUE] INDEX` statements for every struct-level `#[orm(unique(...))]`
106/// / `#[orm(index(...))]` — composite constraints, enforced as indexes (not
107/// inline table constraints) so they can be added/dropped without a rebuild.
108pub fn composite_index_sqls(t: &TableDef) -> Vec<String> {
109    let mut out: Vec<String> = t
110        .composite_uniques
111        .iter()
112        .map(|cols| composite_index_sql(&t.name, cols, true))
113        .collect();
114    out.extend(
115        t.composite_indexes
116            .iter()
117            .map(|cols| composite_index_sql(&t.name, cols, false)),
118    );
119    out
120}
121
122fn composite_index_sql(table: &str, columns: &[String], unique: bool) -> String {
123    format!(
124        "CREATE {}INDEX {} ON {} ({});",
125        if unique { "UNIQUE " } else { "" },
126        quote_ident(&composite_index_name(table, columns)),
127        quote_ident(table),
128        columns
129            .iter()
130            .map(|c| quote_ident(c))
131            .collect::<Vec<_>>()
132            .join(", "),
133    )
134}