///|
/// The SQL dialect used when a migration plan is rendered.
pub(all) enum Dialect {
SQLite
PostgreSQL
} derive(Eq, Debug, ToJson, FromJson)
///|
/// A conservative risk classification for a migration step.
pub(all) enum Risk {
Safe
Review
Destructive
} derive(Eq, Debug, ToJson, FromJson)
///|
/// A portable column description. `data_type` is intentionally retained as a
/// dialect-level string: the planner compares it deterministically without
/// pretending that PostgreSQL and SQLite type systems are interchangeable.
pub(all) struct Column {
name : String
data_type : String
nullable : Bool
default_value : String?
primary_key : Bool
unique : Bool
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Index {
name : String
columns : Array[String]
unique : Bool
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ForeignKey {
name : String
columns : Array[String]
referenced_table : String
referenced_columns : Array[String]
on_delete : String?
on_update : String?
} derive(Eq, Debug, ToJson, FromJson)
///|
/// A table-level `CHECK` constraint. The expression is a dialect SQL fragment
/// held under the same rules as a column default: validated for statement
/// delimiters, emitted verbatim, never parsed.
pub(all) struct CheckConstraint {
name : String
expression : String
} derive(Eq, Debug, ToJson, FromJson)
///|
/// A trigger on a table.
///
/// `timing` and `event` are validated against a fixed set because they are the
/// portable part. `action` is everything after `ON ` and is a
/// dialect-specific fragment: SQLite inlines statements between BEGIN and END,
/// while PostgreSQL executes a function. It is the one fragment the delimiter
/// check does not apply to, because a SQLite trigger body legitimately contains
/// semicolons.
pub(all) struct Trigger {
name : String
timing : String
event : String
action : String
} derive(Eq, Debug, ToJson, FromJson)
///|
/// A view. `definition` is the body after `AS`, held opaquely like a check
/// expression.
pub(all) struct View {
name : String
definition : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Table {
name : String
columns : Array[Column]
indexes : Array[Index]
foreign_keys : Array[ForeignKey]
checks : Array[CheckConstraint]
triggers : Array[Trigger]
} derive(Eq, Debug, ToJson)
///|
fn[T : @json.FromJson] required_field(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
match fields.get(key) {
Some(value) => @json.from_json(value, path=path.add_key(key))
None => raise @json.JsonDecodeError((path, "Missing field \{key}"))
}
}
///|
/// `checks` arrived after 0.2.0. A schema file written against that release
/// omits it, and refusing to read those files on upgrade would break every
/// stored schema over a field whose absence simply means "no check
/// constraints". Decoding therefore treats it as optional; encoding always
/// writes it, so a round trip normalises the document.
pub impl @json.FromJson for Table with fn from_json(json, path) {
guard json is Object(fields) else {
raise @json.JsonDecodeError((path, "expected an object describing a table"))
}
{
name: required_field(fields, "name", path),
columns: required_field(fields, "columns", path),
indexes: required_field(fields, "indexes", path),
foreign_keys: required_field(fields, "foreign_keys", path),
checks: match fields.get("checks") {
Some(value) => @json.from_json(value, path=path.add_key("checks"))
None => []
},
triggers: match fields.get("triggers") {
Some(value) => @json.from_json(value, path=path.add_key("triggers"))
None => []
},
}
}
///|
pub(all) struct Schema {
version : String
tables : Array[Table]
views : Array[View]
} derive(Eq, Debug, ToJson)
///|
/// `views` arrived after 0.3.0 and is optional for the same reason `checks`
/// was: a stored schema written against an earlier release omits it, and its
/// absence simply means the schema declares no views.
pub impl @json.FromJson for Schema with fn from_json(json, path) {
guard json is Object(fields) else {
raise @json.JsonDecodeError(
(path, "expected an object describing a schema"),
)
}
{
version: required_field(fields, "version", path),
tables: required_field(fields, "tables", path),
views: match fields.get("views") {
Some(value) => @json.from_json(value, path=path.add_key("views"))
None => []
},
}
}
///|
/// Explicit rename hints prevent the planner from making unsafe guesses.
pub(all) struct RenameHints {
tables : Array[(String, String)]
columns : Array[(String, String, String)]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn RenameHints::empty() -> RenameHints {
{ tables: [], columns: [], }
}
///|
pub(all) enum Change {
AddTable(Table)
DropTable(Table)
RenameTable(String, String)
AddColumn(String, Column)
DropColumn(String, Column)
RenameColumn(String, String, String)
AlterColumn(String, Column, Column)
AddIndex(String, Index)
DropIndex(String, Index)
AddForeignKey(String, ForeignKey)
DropForeignKey(String, ForeignKey)
AddCheck(String, CheckConstraint)
DropCheck(String, CheckConstraint)
AddTrigger(String, Trigger)
DropTrigger(String, Trigger)
AddView(View)
DropView(View)
RebuildTable(Table, Table, Array[(String, String)])
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct MigrationStep {
id : String
risk : Risk
reason : String
change : Change
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Plan {
from_version : String
to_version : String
dialect : Dialect
steps : Array[MigrationStep]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ValidationIssue {
path : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct RenderedPlan {
plan : Plan
sql : Array[String]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct RenderIssue {
step_id : String
message : String
} derive(Eq, Debug, ToJson, FromJson)