use super::diff::Action;
use super::ir::{
Column, ColumnFk, Generated, GeneratedKind, Index, Schema, Table, TableForeignKey,
};
pub fn emit_action(action: &Action) -> Option<String> {
Some(match action {
Action::CreateTable(t) => emit_table(t),
Action::DropTable(name) => format!("DROP TABLE {};", quote_ident(name)),
Action::AddColumn { table, column } => {
format!(
"ALTER TABLE {} ADD COLUMN {};",
quote_ident(table),
emit_column(column)
)
}
Action::DropColumn { table, column } => {
format!(
"ALTER TABLE {} DROP COLUMN {};",
quote_ident(table),
quote_ident(column)
)
}
Action::AlterColumn { table, after, .. } => {
format!(
"ALTER TABLE {} ALTER COLUMN {} TO {};",
quote_ident(table),
quote_ident(&after.name),
emit_column(after)
)
}
Action::CreateIndex { table, index } => emit_index(table, index),
Action::DropIndex { name } => format!("DROP INDEX {};", quote_ident(name)),
Action::NeedsRebuild { .. } => return None,
})
}
pub fn emit_schema(schema: &Schema) -> Vec<String> {
let mut out = Vec::with_capacity(schema.tables.len() * 2);
for t in &schema.tables {
out.push(emit_table(t));
}
for t in &schema.tables {
for idx in &t.indexes {
out.push(emit_index(&t.name, idx));
}
}
out
}
pub fn emit_table(t: &Table) -> String {
let mut s = String::new();
s.push_str("CREATE TABLE ");
s.push_str("e_ident(&t.name));
s.push_str(" (\n");
let mut parts: Vec<String> = Vec::new();
for col in &t.columns {
parts.push(format!(" {}", emit_column(col)));
}
if let Some(pk) = &t.primary_key {
parts.push(format!(
" PRIMARY KEY ({})",
pk.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ")
));
}
for fk in &t.foreign_keys {
parts.push(format!(" {}", emit_table_fk(fk)));
}
for chk in &t.checks {
parts.push(format!(
" CONSTRAINT {} CHECK ({})",
quote_ident(&chk.name),
chk.expr
));
}
s.push_str(&parts.join(",\n"));
s.push_str("\n)");
let mut suffixes: Vec<&str> = Vec::new();
if t.without_rowid {
suffixes.push("WITHOUT ROWID");
}
if t.strict {
suffixes.push("STRICT");
}
if !suffixes.is_empty() {
s.push(' ');
s.push_str(&suffixes.join(", "));
}
s.push(';');
s
}
fn emit_column(col: &Column) -> String {
let mut s = String::new();
s.push_str("e_ident(&col.name));
s.push(' ');
s.push_str(col.ty.sql());
if col.primary_key {
s.push_str(" PRIMARY KEY");
if col.auto_increment {
s.push_str(" AUTOINCREMENT");
}
}
if !col.nullable && !col.primary_key {
s.push_str(" NOT NULL");
}
if col.unique && !col.primary_key {
s.push_str(" UNIQUE");
}
if let Some(d) = &col.default {
s.push_str(" DEFAULT ");
s.push_str(d);
}
if let Some(c) = &col.check {
s.push_str(" CHECK (");
s.push_str(c);
s.push(')');
}
if let Some(g) = &col.generated {
emit_generated(&mut s, g);
}
if let Some(fk) = &col.references {
emit_column_fk(&mut s, fk);
}
s
}
fn emit_generated(s: &mut String, g: &Generated) {
s.push_str(" GENERATED ALWAYS AS (");
s.push_str(&g.expr);
s.push_str(") ");
s.push_str(match g.kind {
GeneratedKind::Stored => "STORED",
GeneratedKind::Virtual => "VIRTUAL",
});
}
fn emit_column_fk(s: &mut String, fk: &ColumnFk) {
s.push_str(" REFERENCES ");
s.push_str("e_ident(&fk.table));
s.push_str(" (");
s.push_str("e_ident(&fk.column));
s.push(')');
if let Some(a) = fk.on_delete {
s.push_str(" ON DELETE ");
s.push_str(a.sql());
}
if let Some(a) = fk.on_update {
s.push_str(" ON UPDATE ");
s.push_str(a.sql());
}
}
fn emit_table_fk(fk: &TableForeignKey) -> String {
let mut s = String::new();
s.push_str("FOREIGN KEY (");
s.push_str(
&fk.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", "),
);
s.push_str(") REFERENCES ");
s.push_str("e_ident(&fk.references_table));
s.push_str(" (");
s.push_str(
&fk.references_columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", "),
);
s.push(')');
if let Some(a) = fk.on_delete {
s.push_str(" ON DELETE ");
s.push_str(a.sql());
}
if let Some(a) = fk.on_update {
s.push_str(" ON UPDATE ");
s.push_str(a.sql());
}
s
}
pub fn emit_index(table_name: &str, idx: &Index) -> String {
let mut s = String::new();
s.push_str("CREATE ");
if idx.unique {
s.push_str("UNIQUE ");
}
s.push_str("INDEX ");
let derived;
let name: &str = if let Some(n) = &idx.name {
n.as_str()
} else {
derived = format!(
"{}_{}_idx",
table_name,
idx.columns
.iter()
.map(|c| sanitize_for_ident(c))
.collect::<Vec<_>>()
.join("_")
);
derived.as_str()
};
s.push_str("e_ident(name));
s.push_str(" ON ");
s.push_str("e_ident(table_name));
s.push_str(" (");
s.push_str(&idx.columns.join(", "));
s.push_str(");");
s
}
fn quote_ident(name: &str) -> String {
if needs_quoting(name) {
format!("\"{}\"", name.replace('"', "\"\""))
} else {
name.to_string()
}
}
fn needs_quoting(name: &str) -> bool {
if name.is_empty() {
return true;
}
let bad_char = !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_');
let starts_digit = name.chars().next().is_some_and(|c| c.is_ascii_digit());
bad_char || starts_digit || is_reserved(name)
}
fn is_reserved(name: &str) -> bool {
const RESERVED: &[&str] = &[
"abort", "action", "add", "after", "all", "alter", "analyze", "and", "as", "asc",
"attach", "autoincrement", "before", "begin", "between", "by", "cascade", "case",
"cast", "check", "collate", "column", "commit", "conflict", "constraint", "create",
"cross", "current", "database", "default", "deferrable", "deferred", "delete", "desc",
"detach", "distinct", "drop", "each", "else", "end", "escape", "except", "exclusive",
"exists", "explain", "fail", "for", "foreign", "from", "full", "glob", "group",
"having", "if", "ignore", "immediate", "in", "index", "indexed", "initially",
"inner", "insert", "instead", "intersect", "into", "is", "isnull", "join", "key",
"left", "like", "limit", "match", "natural", "no", "not", "notnull", "null", "of",
"offset", "on", "or", "order", "outer", "plan", "pragma", "primary", "query",
"raise", "references", "regexp", "reindex", "release", "rename", "replace",
"restrict", "right", "rollback", "row", "savepoint", "select", "set", "table",
"temp", "temporary", "then", "to", "transaction", "trigger", "union", "unique",
"update", "using", "vacuum", "values", "view", "virtual", "when", "where",
"with", "without",
];
RESERVED.iter().any(|r| r.eq_ignore_ascii_case(name))
}
fn sanitize_for_ident(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut last_underscore = false;
for c in s.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
last_underscore = false;
} else if !last_underscore && !out.is_empty() {
out.push('_');
last_underscore = true;
}
}
while out.ends_with('_') {
out.pop();
}
out
}