// Inheritance mapping — the three shapes a class hierarchy takes in a relational
// store, each expressed as one `Model` over one record type.
//
// SQLAlchemy derives these from Python inheritance and dispatches on the class at
// runtime, handing back subclass instances from a base query. MoonBit has neither
// inheritance nor a downcast, so the mapping is declared: a subclass is a `Model[T]`
// that carries its own predicate (single table), its own join (joined table), or its
// own table outright (concrete table). The record type is what decides which columns
// exist, and the model decides which rows and which tables they come from.
///|
/// A nullable text column named `name`. Used for the discriminator a single-table
/// subclass adds when the base did not declare one; built as a literal because the
/// `column` constructor is shadowed by this file's `column~` parameter.
fn text_column(name : String) -> Column {
{
name,
col_type: TextType,
primary_key: false,
autoincrement: false,
nullable: true,
unique: false,
default: None,
checks: [],
references: None,
on_delete: None,
on_update: None,
}
}
///|
/// A single-table subclass: the table is shared with its base, and a discriminator
/// column tells the rows apart. The returned model
///
/// * filters every read to `column = identity`,
/// * forces `column = identity` into every insert, so a subclass row cannot be
/// written as a base row by accident, and
/// * declares the discriminator column if the base already does — adding it to the
/// column list only when it is missing.
///
/// The base's own model stays as it was and reads every row of the table; a subclass
/// model reads only its own. This is the mapping SQLAlchemy spells `polymorphic_on`
/// plus `polymorphic_identity`.
pub fn[T] Model::discriminate(
self : Model[T],
column~ : String,
identity~ : String,
) -> Model[T] {
let columns = self.columns.copy()
let mut declared = false
for c in columns {
if c.name == column {
declared = true
}
}
if !declared {
columns.push(text_column(column))
}
let forced : @moondb.Value = @moondb.Text(identity)
let inner = self.to_columns
{
table: self.table,
columns,
from_row: self.from_row,
to_columns: fn(record : T) -> Array[(String, @moondb.Value)] {
let out = inner(record)
let mut present = false
for pair in out {
if pair.0 == column {
present = true
}
}
if !present {
out.push((column, forced))
}
out
},
indexes: self.indexes,
filter: [(column, forced)],
joins: self.joins.copy(),
projection: self.projection.copy(),
parent: self.parent,
ident: Map([]),
}
}
///|
/// The column both tables of a joined mapping share: the parent's primary key, which
/// the subclass row's foreign key points at. Falls back to the first parent column,
/// and then to the first declared column, so a mapping with no declared key still has
/// a deterministic join rather than an index error.
fn[T] Model::key_of(self : Model[T], parent_columns : Array[String]) -> String {
for name in parent_columns {
for c in self.columns {
if c.name == name && c.primary_key {
return name
}
}
}
if parent_columns.length() > 0 {
parent_columns[0]
} else if self.columns.length() > 0 {
self.columns[0].name
} else {
""
}
}
///|
/// A joined-table subclass: the base's columns live in `parent_table`, this class's
/// own columns in the model's table, and one row is the join of the two on the
/// primary key. The returned model
///
/// * projects `parent_table`'s columns qualified, then this table's, so `from_row`
/// reads the parent's half before the subclass's — the order the columns were
/// declared in,
/// * joins `parent_table` on `. = .`, and
/// * splits writes: an insert writes the parent row first, then the subclass row
/// carrying the same key, which is the only order the foreign key allows.
///
/// `fk` names the subclass column that references the parent key; it defaults to the
/// parent key's own name, which is the usual `id` in both tables.
pub fn[T] Model::joined(
self : Model[T],
parent_table~ : String,
parent_columns~ : Array[String],
fk? : String,
) -> Model[T] {
let key = self.key_of(parent_columns)
let fk_column = fk.unwrap_or(key)
let projection : Array[String] = []
for c in self.columns {
if parent_columns.contains(c.name) {
projection.push(parent_table + "." + c.name)
} else {
projection.push(self.table + "." + c.name)
}
}
{
table: self.table,
columns: self.columns,
from_row: self.from_row,
to_columns: self.to_columns,
indexes: self.indexes,
filter: self.filter.copy(),
joins: [
(
parent_table,
parent_table + "." + key + " = " + self.table + "." + fk_column,
),
],
projection,
parent: Some((parent_table, parent_columns, fk_column)),
ident: Map([]),
}
}
///|
/// Split a record's bound columns into one group per table it is written to: one
/// `(table, columns)` pair for a plain model, and parent-then-subclass for a joined
/// mapping. The subclass group carries the foreign key even though the key column
/// belongs to the parent, because the subclass row is what holds the reference.
///
/// The order is the order the rows must be written in: a foreign key means the
/// parent row has to exist first.
pub fn[T] Model::split_columns(
self : Model[T],
columns : Array[(String, @moondb.Value)],
) -> Array[(String, Array[(String, @moondb.Value)])] {
match self.parent {
None => [(self.table, columns)]
Some((parent_table, parent_columns, fk)) => {
let key = self.parent_key()
let mut key_value : @moondb.Value = Null
for pair in columns {
if pair.0 == key {
key_value = pair.1
}
}
let parent : Array[(String, @moondb.Value)] = []
let child : Array[(String, @moondb.Value)] = []
for pair in columns {
if parent_columns.contains(pair.0) {
parent.push(pair)
} else {
child.push(pair)
}
}
let mut has_fk = false
for pair in child {
if pair.0 == fk {
has_fk = true
}
}
if !has_fk {
child.push((fk, key_value))
}
[(parent_table, parent), (self.table, child)]
}
}
}
///|
/// The parent key column of a joined-table subclass — the column both tables share,
/// which the child's foreign key points at. Empty for a plain model.
pub fn[T] Model::parent_key(self : Model[T]) -> String {
match self.parent {
None => ""
Some((_, parent_columns, _)) => self.key_of(parent_columns)
}
}
///|
/// Whether this model's rows are split across two tables (a joined-table subclass).
pub fn[T] Model::is_joined(self : Model[T]) -> Bool {
self.parent is Some(_)
}
///|
/// The table a joined-table subclass's parent half lives in, or `""`.
pub fn[T] Model::parent_table(self : Model[T]) -> String {
match self.parent {
None => ""
Some((table, _, _)) => table
}
}
///|
/// The subclass column that references the parent key, or `""`.
pub fn[T] Model::parent_fk(self : Model[T]) -> String {
match self.parent {
None => ""
Some((_, _, fk)) => fk
}
}