// Autogenerate: compare a declared schema with a reflected one and say what would have
// to change. Between the two is an ordered list of operations, and — because every
// operation here has an exact inverse over data we already hold — the list that undoes
// it as well. That is the difference from Alembic, which leaves `downgrade()` empty and
// asks you to write it.
///|
/// One change and the change that undoes it, both derived from the same comparison —
/// so the downgrade of an autogenerated revision is written for you and cannot drift
/// from the upgrade.
pub(all) struct Change {
up : Op
down : Op
}
///|
/// The difference between a declared schema and a reflected one, as up/down pairs.
///
/// The order is the order a migration must run in: create missing tables and their
/// indexes first, then bring existing tables in line column by column, then drop the
/// indexes and tables that are gone. Columns are compared by name; a type compares as
/// `dialect` spells it, so an `Int64` reflected as `INTEGER` on SQLite is not a change;
/// nullability is compared only off the primary key, which SQLite reports as nullable
/// because `INTEGER PRIMARY KEY` is the rowid.
pub fn autogenerate_changes(
declared : Array[@schema.TableSpec],
reflected : Array[@schema.TableSpec],
dialect? : @query.Dialect,
) -> Array[Change] {
let out : Array[Change] = []
// Creates: tables the model declares that the database has never seen.
for table in declared {
if !has_table(reflected, table.name) {
out.push({
up: create_table(table.name, table.columns),
down: drop_table(table.name, if_exists=true),
})
for ix in table.indexes {
out.push(index_change(table, ix))
}
}
}
// Alters: tables on both sides.
for table in declared {
match find_table(reflected, table.name) {
None => ()
Some(live) => {
for column in table.columns {
match find_column(live.columns, column.name) {
None =>
out.push({
up: add_column(table.name, column),
down: drop_column(table.name, column.name),
})
Some(was) =>
for change in column_changes(table.name, was, column, dialect) {
out.push(change)
}
}
}
for column in live.columns {
if find_column(table.columns, column.name) is None {
out.push({
up: drop_column(table.name, column.name),
down: add_column(table.name, column),
})
}
}
for change in index_changes(table, live, dialect) {
out.push(change)
}
}
}
}
// Drops: tables the model no longer declares.
for table in reflected {
if !has_table(declared, table.name) {
out.push({
up: drop_table(table.name, if_exists=true),
down: create_table(table.name, table.columns),
})
for ix in table.indexes {
out.push({
up: drop_index(
index_name_of(table.name, ix),
table=table.name,
if_exists=true,
),
down: create_index(
index_name_of(table.name, ix),
table.name,
ix.columns,
unique=ix.unique,
),
})
}
}
}
out
}
///|
/// Just the operations an upgrade needs, in order — what a migration body contains.
pub fn autogenerate(
declared : Array[@schema.TableSpec],
reflected : Array[@schema.TableSpec],
dialect? : @query.Dialect,
) -> Array[Op] {
autogenerate_changes(declared, reflected, dialect?).map(c => c.up)
}
///|
/// A revision built from the difference, with its `up` and `down` both filled in. The
/// downgrade is exact over the reflected state, which is what makes it safe to run.
pub fn autogenerate_revision(
id~ : String,
parents~ : Array[String] = [],
name~ : String,
declared : Array[@schema.TableSpec],
reflected : Array[@schema.TableSpec],
dialect? : @query.Dialect,
) -> Revision {
let changes = autogenerate_changes(declared, reflected, dialect?)
let up : Array[Op] = []
for change in changes {
up.push(change.up)
}
// Undo in the reverse order: the last thing created is the first thing dropped.
let down : Array[Op] = []
for i = changes.length() - 1; i >= 0; i = i - 1 {
down.push(changes[i].down)
}
Revision::new(id~, parents~, name~, up~, down~)
}
///|
fn has_table(tables : Array[@schema.TableSpec], name : String) -> Bool {
tables.iter().find_first(t => t.name == name) is Some(_)
}
///|
fn find_table(
tables : Array[@schema.TableSpec],
name : String,
) -> @schema.TableSpec? {
tables.iter().find_first(t => t.name == name)
}
///|
fn find_column(
columns : Array[@schema.Column],
name : String,
) -> @schema.Column? {
columns.iter().find_first(c => c.name == name)
}
///|
/// The name an index is known by, applying the `ix_`/`uq_` default the DDL uses.
fn index_name_of(table : String, ix : @schema.Index) -> String {
match ix.name {
Some(n) => n
None =>
(if ix.unique { "uq_" } else { "ix_" }) +
table +
"_" +
ix.columns.join("_")
}
}
///|
/// The changes one column needs: its type, and its nullability when it is not a key.
/// Both are `AlterColumn` operations carrying only the property that moved, so the
/// statement stays a single `ALTER` rather than a full redefinition.
fn column_changes(
table : String,
was : @schema.Column,
want : @schema.Column,
dialect : @query.Dialect?,
) -> Array[Change] {
let out : Array[Change] = []
let same_type = match dialect {
Some(d) => was.col_type.sql_for(d) == want.col_type.sql_for(d)
None => was.col_type == want.col_type
}
if !same_type {
out.push({
up: alter_column(table, want.name, col_type=want.col_type),
down: alter_column(table, want.name, col_type=was.col_type),
})
}
if !want.primary_key && !was.primary_key && was.nullable != want.nullable {
out.push({
up: alter_column(table, want.name, nullable=want.nullable),
down: alter_column(table, want.name, nullable=was.nullable),
})
}
out
}
///|
/// The changes an index needs, comparing by name and then by what it covers.
fn index_changes(
declared : @schema.TableSpec,
reflected : @schema.TableSpec,
dialect : @query.Dialect?,
) -> Array[Change] {
let out : Array[Change] = []
for ix in declared.indexes {
let name = index_name_of(declared.name, ix)
match
reflected.indexes
.iter()
.find_first(other => index_name_of(reflected.name, other) == name) {
None =>
out.push({
up: create_index(
name,
declared.name,
ix.columns,
unique=ix.unique,
if_not_exists=true,
),
down: drop_index(name, table=declared.name, if_exists=true),
})
Some(live) =>
if live.columns != ix.columns || live.unique != ix.unique {
// An index is replaced, not altered: drop the old, create the new, and the
// inverse is the same pair the other way round.
out.push({
up: drop_index(name, table=declared.name, if_exists=true),
down: create_index(
name,
declared.name,
live.columns,
unique=live.unique,
),
})
out.push({
up: create_index(name, declared.name, ix.columns, unique=ix.unique),
down: drop_index(name, table=declared.name, if_exists=true),
})
}
}
}
for ix in reflected.indexes {
let name = index_name_of(reflected.name, ix)
if declared.indexes
.iter()
.find_first(other => index_name_of(declared.name, other) == name)
is None {
out.push({
up: drop_index(name, table=reflected.name, if_exists=true),
down: create_index(name, reflected.name, ix.columns, unique=ix.unique),
})
}
}
ignore(dialect)
out
}
///|
/// One index introduced with a table: created with it, dropped before it.
fn index_change(table : @schema.TableSpec, ix : @schema.Index) -> Change {
let name = index_name_of(table.name, ix)
{
up: create_index(name, table.name, ix.columns, unique=ix.unique),
down: drop_index(name, table=table.name, if_exists=true),
}
}