// A schema operation as data. A revision lists the operations that apply it, and the
// runner renders them for whichever dialect it is talking to — which is what makes
// one revision apply on SQLite, PostgreSQL and MySQL, and what lets the same list be
// rendered to SQL text for an offline run.
//
// SQLAlchemy spells this namespace `op.create_table(...)`; here an operation is a
// value returned by `create_table(...)`, so it can also be compared, printed, and
// stored before anything renders it.
///|
/// One schema change. `AlterColumn` carries only what changes, so a rename is a
/// rename rather than a full column redefinition; `RebuildTable` is SQLite's batch
/// alter, where the column set is supplied whole because the engine cannot alter a
/// column in place.
pub(all) enum Op {
CreateTable(
name~ : String,
columns~ : Array[@schema.Column],
if_not_exists~ : Bool
)
DropTable(name~ : String, if_exists~ : Bool)
AddColumn(table~ : String, column~ : @schema.Column)
DropColumn(table~ : String, name~ : String)
RenameTable(from~ : String, to~ : String)
RenameColumn(table~ : String, from~ : String, to~ : String)
AlterColumn(
table~ : String,
name~ : String,
new_name~ : String?,
col_type~ : @schema.ColumnType?,
nullable~ : Bool?,
default~ : String?,
drop_default~ : Bool
)
AddForeignKey(
table~ : String,
name~ : String,
columns~ : Array[String],
ref_table~ : String,
ref_columns~ : Array[String],
on_delete~ : String?,
on_update~ : String?
)
DropForeignKey(table~ : String, name~ : String)
CreateIndex(
name~ : String,
table~ : String,
columns~ : Array[String],
unique~ : Bool,
if_not_exists~ : Bool
)
DropIndex(name~ : String, table~ : String?, if_exists~ : Bool)
RebuildTable(
name~ : String,
columns~ : Array[@schema.Column],
copy~ : Array[String]
)
Execute(sql~ : String)
}
///|
/// Create a table from its column list.
pub fn create_table(
name : String,
columns : Array[@schema.Column],
if_not_exists~ : Bool = false,
) -> Op {
CreateTable(name~, columns~, if_not_exists~)
}
///|
/// Drop a table, optionally tolerating its absence.
pub fn drop_table(name : String, if_exists~ : Bool = false) -> Op {
DropTable(name~, if_exists~)
}
///|
/// Add one column.
pub fn add_column(table : String, column : @schema.Column) -> Op {
AddColumn(table~, column~)
}
///|
/// Drop one column.
pub fn drop_column(table : String, name : String) -> Op {
DropColumn(table~, name~)
}
///|
/// Rename a table.
pub fn rename_table(from : String, to : String) -> Op {
RenameTable(from~, to~)
}
///|
/// Rename a column. Every dialect here spells this `RENAME COLUMN`, so it is the one
/// column change that does not need a table rebuild on SQLite.
pub fn rename_column(table : String, from : String, to : String) -> Op {
RenameColumn(table~, from~, to~)
}
///|
/// Change a column: its name, its type, its nullability, or its default. Only the
/// parts given are rendered. SQLite cannot change a type or a nullability in place,
/// so that combination raises and points at `rebuild_table`.
pub fn alter_column(
table : String,
name : String,
new_name? : String,
col_type? : @schema.ColumnType,
nullable? : Bool,
default? : String,
drop_default~ : Bool = false,
) -> Op {
AlterColumn(
table~,
name~,
new_name~,
col_type~,
nullable~,
default~,
drop_default~,
)
}
///|
/// Add a foreign-key constraint to an existing table. SQLite cannot add one without
/// rebuilding the table, so it raises there.
pub fn add_foreign_key(
table : String,
name : String,
columns : Array[String],
ref_table : String,
ref_columns : Array[String],
on_delete? : String,
on_update? : String,
) -> Op {
AddForeignKey(
table~,
name~,
columns~,
ref_table~,
ref_columns~,
on_delete~,
on_update~,
)
}
///|
/// Drop a foreign-key constraint by name.
pub fn drop_foreign_key(table : String, name : String) -> Op {
DropForeignKey(table~, name~)
}
///|
/// Create an index.
pub fn create_index(
name : String,
table : String,
columns : Array[String],
unique~ : Bool = false,
if_not_exists~ : Bool = false,
) -> Op {
CreateIndex(name~, table~, columns~, unique~, if_not_exists~)
}
///|
/// Drop an index. `table` matters on MySQL, which names an index as `table.index`.
pub fn drop_index(
name : String,
table? : String,
if_exists~ : Bool = false,
) -> Op {
DropIndex(name~, table~, if_exists~)
}
///|
/// SQLite's batch alter: build the table again from `columns`, carrying over the
/// listed `copy` columns, and swap it in. Indexes on the old table are dropped with
/// it, so pair this with the `create_index` operations that re-establish them.
pub fn rebuild_table(
name : String,
columns : Array[@schema.Column],
copy : Array[String],
) -> Op {
RebuildTable(name~, columns~, copy~)
}
///|
/// Run SQL verbatim. For the changes the operation set does not model — a trigger, an
/// engine-specific clause — and for data migrations.
pub fn execute(sql : String) -> Op {
Execute(sql~)
}
///|
/// The name of the table an operation touches, for ordering and reporting.
pub fn Op::table(self : Op) -> String {
match self {
CreateTable(name~, ..) => name
DropTable(name~, ..) => name
AddColumn(table~, ..) => table
DropColumn(table~, ..) => table
RenameTable(from~, ..) => from
RenameColumn(table~, ..) => table
AlterColumn(table~, ..) => table
AddForeignKey(table~, ..) => table
DropForeignKey(table~, ..) => table
CreateIndex(table~, ..) => table
DropIndex(..) => ""
RebuildTable(name~, ..) => name
Execute(..) => ""
}
}
///|
/// The name of the temporary table `RebuildTable` builds before swapping it in.
fn batch_table(name : String) -> String {
"moonorm_batch_" + name
}
///|
/// The statements this operation is, rendered for `dialect`. Most are one statement;
/// `AlterColumn` is several when more than one property changes, and `RebuildTable`
/// is the four-step swap SQLite needs.
///
/// Raises rather than emitting something the engine would reject: SQLite cannot add
/// or drop a foreign key or change a column's type in place, and `rebuild_table` is
/// the wrong tool for PostgreSQL and MySQL, which alter in place. The error names the
/// engine and points at the operation that does work.
pub fn Op::to_sql(
self : Op,
dialect : @query.Dialect,
) -> Array[String] raise @moondb.DbError {
match self {
CreateTable(name~, columns~, if_not_exists~) =>
[@schema.create_table_sql(name, columns, dialect, if_not_exists~)]
DropTable(name~, if_exists~) => [@schema.drop_table_sql(name, if_exists~)]
AddColumn(table~, column~) =>
[@schema.add_column_sql(table, column, dialect)]
DropColumn(table~, name~) => [@schema.drop_column_sql(table, name)]
RenameTable(from~, to~) => [@schema.rename_table_sql(from, to)]
RenameColumn(table~, from~, to~) =>
[@schema.rename_column_sql(table, from, to)]
AlterColumn(
table~,
name~,
new_name~,
col_type~,
nullable~,
default~,
drop_default~
) =>
alter_column_sql(
table, name, new_name, col_type, nullable, default, drop_default, dialect,
)
AddForeignKey(
table~,
name~,
columns~,
ref_table~,
ref_columns~,
on_delete~,
on_update~
) => {
if dialect is Sqlite {
raise @moondb.QueryError(
"SQLite cannot add a foreign key to an existing table: rebuild " +
table +
" with the constraint declared on the column",
)
}
let mut s = "ALTER TABLE " +
table +
" ADD CONSTRAINT " +
name +
" FOREIGN KEY (" +
columns.join(", ") +
") REFERENCES " +
ref_table +
" (" +
ref_columns.join(", ") +
")"
match on_delete {
Some(act) => s = s + " ON DELETE " + act
None => ()
}
match on_update {
Some(act) => s = s + " ON UPDATE " + act
None => ()
}
[s]
}
DropForeignKey(table~, name~) =>
match dialect {
Sqlite =>
raise @moondb.QueryError(
"SQLite cannot drop a foreign key in place: rebuild " + table,
)
Postgres => ["ALTER TABLE " + table + " DROP CONSTRAINT " + name]
Mysql => ["ALTER TABLE " + table + " DROP FOREIGN KEY " + name]
}
CreateIndex(name~, table~, columns~, unique~, if_not_exists~) =>
[
@schema.create_index_sql(
name,
table,
columns,
unique~,
if_not_exists~,
dialect~,
),
]
DropIndex(name~, table~, if_exists~) =>
[@schema.drop_index_sql(name, table, if_exists~, dialect~)]
RebuildTable(name~, columns~, copy~) => {
if !(dialect is Sqlite) {
raise @moondb.QueryError(
"rebuild_table is SQLite's batch alter; " +
name +
" can be altered in place on " +
(if dialect is Postgres { "PostgreSQL" } else { "MySQL" }),
)
}
let tmp = batch_table(name)
let out : Array[String] = [
@schema.create_table_sql(tmp, columns, dialect),
]
if copy.length() > 0 {
let list = copy.join(", ")
out.push(
"INSERT INTO " +
tmp +
" (" +
list +
") SELECT " +
list +
" FROM " +
name,
)
}
out.push(@schema.drop_table_sql(name))
out.push(@schema.rename_table_sql(tmp, name))
out
}
Execute(sql~) => [sql]
}
}
///|
/// `ALTER COLUMN`, spelled the way each engine spells it. A rename is a separate
/// statement everywhere; a type, a nullability and a default are one statement on
/// PostgreSQL and one `MODIFY COLUMN` on MySQL, and impossible in place on SQLite.
fn alter_column_sql(
table : String,
name : String,
new_name : String?,
col_type : @schema.ColumnType?,
nullable : Bool?,
default : String?,
drop_default : Bool,
dialect : @query.Dialect,
) -> Array[String] raise @moondb.DbError {
let column = new_name.unwrap_or(name)
let out : Array[String] = []
if new_name is Some(_) {
out.push(@schema.rename_column_sql(table, name, column))
}
match dialect {
Postgres => {
if col_type is Some(t) {
out.push(
"ALTER TABLE " +
table +
" ALTER COLUMN " +
column +
" TYPE " +
t.sql_for(Postgres),
)
}
if nullable is Some(allow) {
out.push(
"ALTER TABLE " +
table +
" ALTER COLUMN " +
column +
(if allow { " DROP NOT NULL" } else { " SET NOT NULL" }),
)
}
if drop_default {
out.push(
"ALTER TABLE " + table + " ALTER COLUMN " + column + " DROP DEFAULT",
)
} else if default is Some(expr) {
out.push(
"ALTER TABLE " +
table +
" ALTER COLUMN " +
column +
" SET DEFAULT " +
expr,
)
}
}
Mysql => {
// `MODIFY COLUMN` restates the whole definition, so it needs the type; anything
// else the server would have to guess, and guessing resets it.
if col_type is Some(t) {
let mut s = "ALTER TABLE " +
table +
" MODIFY COLUMN " +
column +
" " +
t.sql_for(Mysql)
if nullable is Some(allow) {
s = s + (if allow { " NULL" } else { " NOT NULL" })
}
if default is Some(expr) {
s = s + " DEFAULT " + expr
}
out.push(s)
} else if nullable is Some(_) || default is Some(_) {
raise @moondb.QueryError(
"MySQL restates a column to change its nullability or default; give alter_column a col_type for " +
table +
"." +
column,
)
}
if drop_default {
out.push(
"ALTER TABLE " + table + " ALTER COLUMN " + column + " DROP DEFAULT",
)
}
}
Sqlite =>
if col_type is Some(_) ||
nullable is Some(_) ||
default is Some(_) ||
drop_default {
raise @moondb.QueryError(
"SQLite cannot change a column's type, nullability or default in place: rebuild " +
table +
" with rebuild_table",
)
}
}
out
}