// A table as data, and a whole schema as data. A `Model` is a typed mapping over
// one table; a `TableSpec` is the same table without the record type, which is what
// autogenerate compares and what a model tree feeds it. Reflection produces the same
// shape, so a declared schema and a live one diff as two values.
///|
/// One table described by its columns and indexes — the record-type-free view of a
/// `Model`. Two `TableSpec`s are what a schema comparison needs: a name, the ordered
/// columns, and the indexes on them.
pub(all) struct TableSpec {
name : String
columns : Array[Column]
indexes : Array[Index]
}
///|
/// Describe a table.
pub fn TableSpec::new(
name : String,
columns : Array[Column],
indexes~ : Array[Index] = [],
) -> TableSpec {
{ name, columns, indexes, }
}
///|
/// A set of tables — a declared metadata tree, or everything reflected off a
/// database. The unit autogenerate compares and the unit a snapshot renders.
pub(all) struct Schema {
tables : Array[TableSpec]
}
///|
/// A schema from its tables.
pub fn Schema::new(tables : Array[TableSpec]) -> Schema {
{ tables, }
}
///|
/// The table named `name`, or `None`.
pub fn Schema::table(self : Schema, name : String) -> TableSpec? {
self.tables.iter().find_first(t => t.name == name)
}
///|
/// The table names, in order.
pub fn Schema::names(self : Schema) -> Array[String] {
self.tables.map(t => t.name)
}
///|
/// A normalised text rendering of the schema, for comparing two snapshots of the same
/// database. Column types are written as `dialect` spells them, so an `Int64` and an
/// `Int` both reflected as `INTEGER` on SQLite compare equal — the physical structure
/// is what a round trip has to preserve, not the declared vocabulary.
///
/// Lines are `table`, then one `column` line per column in order, then one `index`
/// line per index sorted by name, so a diff points at the table and the line that
/// changed. Tables are sorted by name for the same reason.
pub fn Schema::snapshot(self : Schema, dialect : @query.Dialect) -> String {
let ordered = self.tables.copy()
ordered.sort_by(fn(a, b) { a.name.compare(b.name) })
let lines : Array[String] = []
for t in ordered {
lines.push("table " + t.name)
for c in t.columns {
let mut line = " column " +
c.name +
" " +
c.col_type.sql_for(dialect) +
(if c.nullable { " null" } else { " not null" })
if c.primary_key {
line = line + " pk"
}
if c.autoincrement {
line = line + " auto"
}
lines.push(line)
}
let indexes = t.indexes.copy()
indexes.sort_by(fn(a, b) { index_name(t, a).compare(index_name(t, b)) })
for ix in indexes {
lines.push(
" index " +
index_name(t, ix) +
(if ix.unique { " unique" } else { "" }) +
" (" +
ix.columns.join(", ") +
")",
)
}
}
lines.join("\n")
}
///|
/// The name of an index, applying the same default the DDL uses: `ix__`,
/// or `uq_` when unique.
pub fn index_name(table : TableSpec, ix : Index) -> String {
match ix.name {
Some(n) => n
None =>
(if ix.unique { "uq_" } else { "ix_" }) +
table.name +
"_" +
ix.columns.join("_")
}
}
///|
/// This model's table as a `TableSpec` — the bridge from a typed model to the schema
/// data autogenerate compares.
pub fn[T] Model::spec(self : Model[T]) -> TableSpec {
{ name: self.table, columns: self.columns, indexes: self.indexes, }
}