// Table and index DDL, rendered once for every caller. A model, a migration
// operation and the reflection snapshot all have to agree on how a table is spelled
// for a dialect, so the spelling lives here and everything else delegates.
///|
/// `CREATE TABLE` for a column list, rendered for `dialect`. A single primary key
/// stays inline; several become one table-level `PRIMARY KEY (a, b)` constraint,
/// since SQL allows only one inline key per table. MySQL gets the `ENGINE=InnoDB`
/// suffix it needs to honour foreign keys.
pub fn create_table_sql(
name : String,
columns : Array[Column],
dialect : @query.Dialect,
if_not_exists~ : Bool = false,
) -> String {
let head = if if_not_exists {
"CREATE TABLE IF NOT EXISTS "
} else {
"CREATE TABLE "
}
let pks = columns.filter(fn(c) { c.primary_key })
let composite = pks.length() > 1
let defs : Array[String] = []
for c in columns {
defs.push(c.ddl_for(dialect, inline_pk=!composite))
}
if composite {
defs.push("PRIMARY KEY (" + pks.map(fn(c) { c.name }).join(", ") + ")")
}
let tail = if dialect is Mysql { " ENGINE=InnoDB" } else { "" }
head + name + " (" + defs.join(", ") + ")" + tail
}
///|
/// `DROP TABLE`, with the idempotent form when `if_exists`.
pub fn drop_table_sql(name : String, if_exists~ : Bool = false) -> String {
"DROP TABLE " + (if if_exists { "IF EXISTS " } else { "" }) + name
}
///|
/// The statement that adds one column. SQLite, PostgreSQL and MySQL share the
/// spelling, so the dialect is not consulted.
pub fn add_column_sql(
table : String,
column : Column,
dialect : @query.Dialect,
) -> String {
"ALTER TABLE " +
table +
" ADD COLUMN " +
column.ddl_for(dialect, inline_pk=false)
}
///|
/// The statement that drops one column.
pub fn drop_column_sql(table : String, name : String) -> String {
"ALTER TABLE " + table + " DROP COLUMN " + name
}
///|
/// `CREATE INDEX`. `if_not_exists` is emitted where the dialect accepts it; MySQL
/// has no such clause for an index, so it is dropped there rather than producing a
/// statement the server rejects.
pub fn create_index_sql(
name : String,
table : String,
columns : Array[String],
unique~ : Bool = false,
if_not_exists~ : Bool = false,
dialect~ : @query.Dialect = Sqlite,
) -> String {
"CREATE " +
(if unique { "UNIQUE " } else { "" }) +
"INDEX " +
(if if_not_exists && !(dialect is Mysql) { "IF NOT EXISTS " } else { "" }) +
name +
" ON " +
table +
" (" +
columns.join(", ") +
")"
}
///|
/// `DROP INDEX`. SQLite and PostgreSQL name an index on its own; MySQL spells it
/// `DROP INDEX name ON table`, so there the table is part of the statement and leaving
/// it out raises rather than emitting SQL the server would reject.
pub fn drop_index_sql(
name : String,
table : String?,
if_exists~ : Bool = false,
dialect~ : @query.Dialect = Sqlite,
) -> String raise @moondb.DbError {
let head = if if_exists { "DROP INDEX IF EXISTS " } else { "DROP INDEX " }
match (dialect, table) {
(Mysql, Some(t)) =>
"DROP INDEX " +
(if if_exists { "IF EXISTS " } else { "" }) +
name +
" ON " +
t
(Mysql, None) =>
raise @moondb.QueryError(
"MySQL drops an index with DROP INDEX name ON table: name the table " +
name +
" sits on",
)
_ => head + name
}
}
///|
/// `ALTER TABLE … RENAME TO …`. All three dialects share it.
pub fn rename_table_sql(from : String, to : String) -> String {
"ALTER TABLE " + from + " RENAME TO " + to
}
///|
/// `ALTER TABLE … RENAME COLUMN a TO b` (SQLite 3.25+, MySQL 8.0, PostgreSQL 8).
pub fn rename_column_sql(table : String, from : String, to : String) -> String {
"ALTER TABLE " + table + " RENAME COLUMN " + from + " TO " + to
}