use std::collections::BTreeSet;
use crate::config::{Cardinality, Config, Hook, ManualRelationship};
use crate::error::{GenError, Result};
use crate::schema::{Schema, TableDef, TableKind};
use crate::typemap;
#[derive(Debug, Clone)]
pub(crate) struct Model {
pub table: String,
pub marker: String,
pub row: String,
pub kind: TableKind,
pub writable: bool,
pub columns: Vec<ModelColumn>,
pub pk: Vec<usize>,
pub belongs_to: Vec<BelongsTo>,
pub has_many: Vec<HasMany>,
pub hooks: Vec<Hook>,
}
impl Model {
pub(crate) fn column(&self, db_name: &str) -> Option<(usize, &ModelColumn)> {
self.columns
.iter()
.enumerate()
.find(|(_, c)| c.db_name == db_name)
}
pub(crate) fn holds_relations(&self) -> bool {
self.writable || !self.belongs_to.is_empty() || !self.has_many.is_empty()
}
pub(crate) fn entry(&self) -> &'static str {
if self.writable { "table" } else { "view" }
}
}
#[derive(Debug, Clone)]
pub(crate) struct ModelColumn {
pub db_name: String,
pub field: String,
pub rust_type: String,
pub nullable: bool,
pub overridden: bool,
pub db_type: String,
pub default: Option<String>,
pub autoincrement: bool,
pub unique: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct BelongsTo {
pub name: String,
pub fk_column: usize,
pub target: String,
pub ref_column: String,
}
#[derive(Debug, Clone)]
pub(crate) struct HasMany {
pub name: String,
pub child: String,
pub child_fk_column: String,
pub parent_key_column: String,
pub to_one: bool,
}
pub(crate) fn resolve(schema: &Schema, config: &Config) -> Result<Vec<Model>> {
let mut tables: Vec<&TableDef> = schema
.tables
.iter()
.filter(|t| config.includes_table(&t.name))
.collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
let mut models: Vec<Model> = Vec::with_capacity(tables.len());
for t in &tables {
models.push(resolve_table(t, config)?);
}
if config.output.factories
&& let Some(m) = models.iter().find(|m| m.writable && m.kind.is_view())
{
return Err(GenError::Unsupported(format!(
"`{}` is a writable view, and `[output] factories = true` cannot cover it: \
a view reports no auto-increment columns and no unique constraints, so the \
generated template would have nothing to draw distinct values from. Drop \
`[tables.{}] key`, or generate factories with `{}` in `except`.",
m.table, m.table, m.table
)));
}
for t in &tables {
for fk in &t.foreign_keys {
if fk.columns.len() != 1 {
continue;
}
add_relation(
&mut models,
config,
&RelSpec {
child_table: &t.name,
child_column: &fk.columns[0],
parent_table: &fk.ref_table,
parent_column: fk.ref_columns.first().map_or("", String::as_str),
name: None,
no_back_reference: false,
to_one_back: false,
declared: None,
},
)?;
}
}
for (i, r) in config.relationships.iter().enumerate() {
let cardinality = check_declared(schema, config, i, r)?;
add_relation(
&mut models,
config,
&RelSpec {
child_table: &r.table,
child_column: &r.column,
parent_table: &r.ref_table,
parent_column: &r.ref_column,
name: r.name.clone(),
no_back_reference: r.no_back_reference,
to_one_back: cardinality == Cardinality::OneToOne,
declared: Some(declared_key(i, r)),
},
)?;
}
for m in &mut models {
m.has_many
.sort_by(|a, b| (&a.child, &a.name).cmp(&(&b.child, &b.name)));
}
for m in &models {
check_rel_names(m)?;
}
Ok(models)
}
fn declared_key(i: usize, r: &ManualRelationship) -> String {
format!(
"[[relationships]] #{} (`{}.{}` -> `{}.{}`)",
i + 1,
r.table,
r.column,
r.ref_table,
r.ref_column
)
}
fn check_declared(
schema: &Schema,
config: &Config,
i: usize,
r: &ManualRelationship,
) -> Result<Cardinality> {
let key = declared_key(i, r);
let child = relation_named(schema, &r.table, "table", &key)?;
let parent = relation_named(schema, &r.ref_table, "ref_table", &key)?;
let child_col = column_named(child, &r.column, "column", &key)?;
let parent_col = column_named(parent, &r.ref_column, "ref_column", &key)?;
let child_ty = typemap::resolve(config.dialect, &config.types, child, child_col)?;
let parent_ty = typemap::resolve(config.dialect, &config.types, parent, parent_col)?;
if child_ty.rust_type != parent_ty.rust_type {
return Err(GenError::Config(format!(
"{key}: the join columns are not comparable — `{}.{}` is `{}` (db type `{}`) but \
`{}.{}` is `{}` (db type `{}`). Give them the same Rust type with [types.map] or \
[[types.override]] if the comparison really is sound.",
child.name,
child_col.name,
child_ty.rust_type,
child_col.db_type,
parent.name,
parent_col.name,
parent_ty.rust_type,
parent_col.db_type,
)));
}
match r.cardinality {
Some(c) => Ok(c),
None if child.kind.is_view() || parent.kind.is_view() => {
let view = if child.kind.is_view() { child } else { parent };
Err(GenError::Config(format!(
"{key}: `cardinality` is required because `{}` is a {} — a view has no key \
and no constraint, so nothing in the catalog says how many rows sit on each \
end. Add `cardinality = \"many_to_one\"` or `cardinality = \"one_to_one\"`.",
view.name,
view.kind.noun()
)))
}
None => Ok(Cardinality::ManyToOne),
}
}
fn relation_named<'a>(
schema: &'a Schema,
name: &str,
what: &str,
key: &str,
) -> Result<&'a TableDef> {
schema
.tables
.iter()
.find(|t| t.name == name)
.ok_or_else(|| {
GenError::Config(format!(
"{key}: `{what} = \"{name}\"` names no table or view in the introspected schema \
(it holds: {})",
join_names(schema.tables.iter().map(|t| t.name.as_str()))
))
})
}
fn column_named<'a>(
t: &'a TableDef,
name: &str,
what: &str,
key: &str,
) -> Result<&'a crate::schema::ColumnDef> {
t.column(name).ok_or_else(|| {
GenError::Config(format!(
"{key}: `{what} = \"{name}\"` names no column of {} `{}` (it has: {})",
t.kind.noun(),
t.name,
join_names(t.columns.iter().map(|c| c.name.as_str()))
))
})
}
fn join_names<'a>(names: impl Iterator<Item = &'a str>) -> String {
names.collect::<Vec<_>>().join(", ")
}
fn resolve_table(t: &TableDef, config: &Config) -> Result<Model> {
if t.name == "mod" {
return Err(GenError::Unsupported(
"a table named `mod` cannot become a module file".to_owned(),
));
}
let aliases = config.aliases.get(&t.name);
let singular = aliases
.and_then(|a| a.singular.clone())
.unwrap_or_else(|| crate::names::singular(&t.name, &config.inflections));
let marker = crate::names::pascal(&t.name);
let row = crate::names::pascal(&singular);
if marker == row {
return Err(GenError::Config(format!(
"`{name}` singularises to itself, so the model marker and the row struct would both \
be `{marker}` in one module. Name the row struct with `[aliases.{name}] singular = \
\"…\"`, or, if `{name}` is an irregular plural, map it in `[inflections]`.",
name = t.name,
)));
}
let single_unique: BTreeSet<&str> = t
.unique_keys
.iter()
.chain(std::iter::once(&t.primary_key))
.filter(|k| k.len() == 1)
.map(|k| k[0].as_str())
.collect();
let mut columns: Vec<ModelColumn> = Vec::new();
let mut kept: Vec<&str> = Vec::new();
for c in &t.columns {
if !config.includes_column(&t.name, &c.name) {
continue;
}
kept.push(&c.name);
let field = aliases
.and_then(|a| a.columns.get(&c.name).cloned())
.unwrap_or_else(|| c.name.clone());
let resolved = typemap::resolve(config.dialect, &config.types, t, c)?;
columns.push(ModelColumn {
db_name: c.name.clone(),
field,
rust_type: resolved.rust_type,
nullable: c.nullable,
overridden: resolved.overridden,
db_type: c.db_type.clone(),
default: c.default.clone(),
autoincrement: c.autoincrement,
unique: single_unique.contains(c.name.as_str()),
});
}
if columns.is_empty() {
return Err(GenError::Config(format!(
"table `{}` has no columns left after filters",
t.name
)));
}
check_field_names(&t.name, &columns)?;
let catalog_pk: Vec<usize> = t
.primary_key
.iter()
.filter_map(|name| columns.iter().position(|c| c.db_name == *name))
.collect();
let catalog_key_intact = t.kind == TableKind::Table
&& !catalog_pk.is_empty()
&& catalog_pk.len() == t.primary_key.len();
let declared_key = config
.tables
.get(&t.name)
.map(|tc| tc.key.as_slice())
.unwrap_or_default();
let declared_pk = check_declared_key(t, &columns, declared_key)?;
let (pk, writable) = match (catalog_key_intact, declared_pk) {
(true, _) => (catalog_pk, true),
(false, Some(pk)) => {
for i in &pk {
columns[*i].nullable = false;
}
(pk, true)
}
(false, None) => (vec![], false),
};
let hooks = config
.tables
.get(&t.name)
.map(|tc| tc.hooks.clone())
.unwrap_or_default();
let mut hooks = hooks;
hooks.sort();
hooks.dedup();
if !writable && let Some(h) = hooks.iter().find(|h| **h != Hook::AfterSelect) {
return Err(GenError::Config(format!(
"`{}` is SELECT-only ({}) but configures the `{h}` hook; only `after_select` \
applies. A {} becomes writable by declaring `[tables.{}] key`, which needs the \
engine to say writes reach it.",
t.name,
if t.kind.is_view() {
"a view with no declared key"
} else {
"no primary key"
},
t.kind.noun(),
t.name
)));
}
Ok(Model {
table: t.name.clone(),
marker,
row,
kind: t.kind,
writable,
columns,
pk,
belongs_to: Vec::new(),
has_many: Vec::new(),
hooks,
})
}
fn check_declared_key(
t: &TableDef,
columns: &[ModelColumn],
key: &[String],
) -> Result<Option<Vec<usize>>> {
if key.is_empty() {
return Ok(None);
}
let at = format!("[tables.{}] key", t.name);
if !t.primary_key.is_empty() {
return Err(GenError::Config(format!(
"{at}: `{}` already has a primary key (`{}`) — the catalog's answer is not the \
configuration's to overrule; remove the key, or the column filters that dropped \
part of it.",
t.name,
t.primary_key.join("`, `")
)));
}
if !t.kind.is_updatable() {
return Err(GenError::Config(format!(
"{at}: `{}` is a view this engine will not write through, so declaring its \
identity cannot make it writable. {} Remove the key to generate a `SELECT`-only \
model — relations do not need it.",
t.name, NOT_UPDATABLE_HINT,
)));
}
let mut seen = BTreeSet::new();
let mut pk = Vec::with_capacity(key.len());
for name in key {
if !seen.insert(name) {
return Err(GenError::Config(format!("{at}: `{name}` is listed twice")));
}
let Some(i) = columns.iter().position(|c| c.db_name == *name) else {
return Err(GenError::Config(format!(
"{at}: `{name}` is not a generated column of `{}` (it has: {})",
t.name,
join_names(columns.iter().map(|c| c.db_name.as_str()))
)));
};
pk.push(i);
}
Ok(Some(pk))
}
const NOT_UPDATABLE_HINT: &str = "PostgreSQL writes through a view only when it is \
auto-updatable (one table, no aggregate/DISTINCT/set operation/GROUP BY, …) or carries \
`INSTEAD OF` triggers; MySQL reports one `IS_UPDATABLE` flag it computes the same way and \
has no `INSTEAD OF` triggers at all; SQLite writes through a view only when it carries \
`INSTEAD OF` triggers for all three of INSERT, UPDATE and DELETE.";
fn check_field_names(table: &str, columns: &[ModelColumn]) -> Result<()> {
let mut seen = BTreeSet::new();
for c in columns {
if c.field == "rel" {
return Err(GenError::Config(format!(
"column `{table}.{}` would be named `rel`, which the relations field owns; \
alias it in [aliases.{table}.columns]",
c.db_name
)));
}
if matches!(c.field.as_str(), "table" | "view" | "all_columns") {
return Err(GenError::Config(format!(
"column `{table}.{}` collides with the generated `{}()` fn; \
alias it in [aliases.{table}.columns]",
c.db_name, c.field
)));
}
if !seen.insert(&c.field) {
return Err(GenError::Config(format!(
"two columns of `{table}` are both named `{}` after aliasing",
c.field
)));
}
}
Ok(())
}
struct RelSpec<'a> {
child_table: &'a str,
child_column: &'a str,
parent_table: &'a str,
parent_column: &'a str,
name: Option<String>,
no_back_reference: bool,
to_one_back: bool,
declared: Option<String>,
}
fn add_relation(models: &mut [Model], config: &Config, spec: &RelSpec<'_>) -> Result<()> {
let RelSpec {
child_table,
child_column,
parent_table,
parent_column,
..
} = *spec;
let gone = |what: &str, why: &str| -> Result<()> {
match &spec.declared {
Some(key) => Err(GenError::Config(format!("{key}: {what} {why}"))),
None => Ok(()),
}
};
const FILTERED: &str = "is excluded by the `only`/`except` filters, so the relation has \
nothing to hang off";
const COL_FILTERED: &str = "is excluded by its table's `only_columns`/`except_columns`, so \
the relation has no column to join on";
let Some(child_idx) = models.iter().position(|m| m.table == child_table) else {
return gone(&format!("`{child_table}`"), FILTERED);
};
let Some(parent_idx) = models.iter().position(|m| m.table == parent_table) else {
return gone(&format!("`{parent_table}`"), FILTERED);
};
let Some((fk_column, _)) = models[child_idx].column(child_column) else {
return gone(&format!("`{child_table}.{child_column}`"), COL_FILTERED);
};
if models[parent_idx].column(parent_column).is_none() {
return gone(&format!("`{parent_table}.{parent_column}`"), COL_FILTERED);
}
let default_name =
spec.name
.clone()
.unwrap_or_else(|| match child_column.strip_suffix("_id") {
Some(stem) if !stem.is_empty() => stem.to_owned(),
_ => crate::names::singular(parent_table, &config.inflections),
});
let name = config
.aliases
.get(child_table)
.and_then(|a| a.relationships.get(&default_name).cloned())
.unwrap_or(default_name);
models[child_idx].belongs_to.push(BelongsTo {
name: name.clone(),
fk_column,
target: parent_table.to_owned(),
ref_column: parent_column.to_owned(),
});
if config.no_back_referencing || spec.no_back_reference {
return Ok(());
}
let base = config
.aliases
.get(child_table)
.and_then(|a| a.plural.clone())
.unwrap_or_else(|| child_table.to_owned());
let clashes = models[parent_idx]
.has_many
.iter()
.any(|h| h.child == child_table);
let default_back = if clashes {
format!("{base}_via_{name}")
} else {
base.clone()
};
if clashes {
let child_names: std::collections::BTreeMap<String, String> = models[child_idx]
.belongs_to
.iter()
.map(|b| {
(
models[child_idx].columns[b.fk_column].db_name.clone(),
b.name.clone(),
)
})
.collect();
for h in &mut models[parent_idx].has_many {
if h.child == child_table && h.name == base {
let earlier = child_names
.get(&h.child_fk_column)
.cloned()
.unwrap_or_else(|| h.child_fk_column.clone());
h.name = format!("{base}_via_{earlier}");
}
}
}
let back_name = config
.aliases
.get(parent_table)
.and_then(|a| a.relationships.get(&default_back).cloned())
.unwrap_or(default_back);
models[parent_idx].has_many.push(HasMany {
name: back_name,
child: child_table.to_owned(),
child_fk_column: child_column.to_owned(),
parent_key_column: parent_column.to_owned(),
to_one: spec.to_one_back,
});
Ok(())
}
fn check_rel_names(m: &Model) -> Result<()> {
let mut seen = BTreeSet::new();
for name in m
.belongs_to
.iter()
.map(|b| &b.name)
.chain(m.has_many.iter().map(|h| &h.name))
{
if !seen.insert(name.clone()) {
return Err(GenError::Config(format!(
"model `{}` has two relations named `{name}`; \
rename one in [aliases.{}.relationships]",
m.table, m.table
)));
}
}
Ok(())
}