use crate::IdGeneration;
use crate::schema::{BaseType, Column, ForeignKey, Table, TypedId};
use super::{DeriveOptions, param_name, param_type, push_derives, push_doc, serde_derives};
pub fn resolve(tables: &[Table]) -> Vec<Table> {
let mut resolved = tables.to_vec();
for table in &mut resolved {
let Some(id) = own_typed_id(table) else {
continue;
};
let key = table.primary_key[0].clone();
if let Some(column) = table.columns.iter_mut().find(|c| c.sql_name == key) {
column.typed_id = Some(id);
}
}
for table in &mut resolved {
for foreign_key in &table.foreign_keys {
let Some((id, key_type)) = referenced_id(tables, foreign_key) else {
continue;
};
let column = table
.columns
.iter_mut()
.find(|column| column.sql_name == foreign_key.column);
if let Some(column) = column
&& column.typed_id.is_none()
&& column.sqlx_type == key_type
{
column.typed_id = Some(id);
}
}
}
resolved
}
pub fn check_names(tables: &[Table]) -> Result<(), String> {
const HINT: &str = "rename a module under `[modules]`";
let table_of = |module: &str| {
tables
.iter()
.find(|table| table.module_name == module)
.map_or_else(|| module.to_owned(), Table::display_name)
};
for table in tables {
let mut imported: Vec<&TypedId> = table
.columns
.iter()
.filter_map(|column| column.typed_id.as_ref())
.filter(|id| id.module != table.module_name)
.collect();
imported.sort_by(|a, b| (&a.name, &a.module).cmp(&(&b.name, &b.module)));
imported.dedup();
for pair in imported.windows(2) {
if pair[0].name == pair[1].name {
return Err(format!(
"`{}` would import two typed ids named `{}`, from `{}` and `{}`; {HINT}",
table.display_name(),
pair[0].name,
table_of(&pair[0].module),
table_of(&pair[1].module)
));
}
}
let own_kind = if table.key_columns().len() > 1 {
"key struct"
} else {
"typed id"
};
let mut defined: Vec<(&str, String)> = vec![("row struct", table.struct_name.clone())];
defined.extend(key_items(table).into_iter().map(|name| (own_kind, name)));
if !table.insert_columns().is_empty() {
defined.push(("create input", format!("New{}", table.struct_name)));
}
if !table.primary_key.is_empty() && !table.update_columns().is_empty() {
defined.push(("update patch", format!("Update{}", table.struct_name)));
}
for id in imported {
if let Some((kind, name)) = defined.iter().find(|(_, name)| *name == id.name) {
return Err(format!(
"the typed id `{}` imported from `{}` collides with the {kind} `{name}` that `{}` generates; {HINT}",
id.name,
table_of(&id.module),
table.display_name()
));
}
}
}
Ok(())
}
fn wraps_in_typed_id(key: &Column) -> bool {
key.supports_typed_id() || key.base_type() == BaseType::Enum
}
fn own_typed_id(table: &Table) -> Option<TypedId> {
match table.primary_key_columns().as_slice() {
[key] if wraps_in_typed_id(key) => Some(TypedId {
module: table.module_name.clone(),
name: format!("{}Id", table.struct_name),
}),
_ => None,
}
}
fn referenced_id(tables: &[Table], foreign_key: &ForeignKey) -> Option<(TypedId, String)> {
let target = referenced_table(tables, foreign_key)?;
let id = own_typed_id(target)?;
match target.primary_key_columns().as_slice() {
[key] if key.sql_name == foreign_key.referenced_column => Some((id, key.sqlx_type.clone())),
_ => None,
}
}
pub fn referenced_table<'a>(tables: &'a [Table], foreign_key: &ForeignKey) -> Option<&'a Table> {
fn normalize(schema: Option<&str>) -> Option<&str> {
schema.filter(|schema| *schema != "public")
}
tables.iter().find(|table| {
table.sql_name == foreign_key.referenced_table
&& normalize(table.schema.as_deref())
== normalize(foreign_key.referenced_schema.as_deref())
})
}
fn key_struct_name(table: &Table) -> String {
format!("{}Key", table.struct_name)
}
pub fn key_type(table: &Table) -> String {
match table.key_columns().as_slice() {
[] => "()".to_owned(),
[key] => key.value_type().to_owned(),
_ => key_struct_name(table),
}
}
pub fn key_binding(table: &Table) -> String {
match table.key_columns().as_slice() {
[] => "()".to_owned(),
[key] => param_name(key),
_ => "key".to_owned(),
}
}
pub fn key_params(table: &Table) -> Vec<String> {
let mut params = tenant_params(table);
if !table.key_columns().is_empty() {
params.push(key_param(table));
}
params
}
pub fn key_args(table: &Table) -> Vec<String> {
let mut args: Vec<String> = table.tenant().map(param_name).into_iter().collect();
if !table.key_columns().is_empty() {
args.push(key_binding(table));
}
args
}
pub fn tenant_params(table: &Table) -> Vec<String> {
table
.tenant()
.map(|tenant| format!("{}: {}", param_name(tenant), param_type(tenant)))
.into_iter()
.collect()
}
pub fn scoped_params(table: &Table, params: &[String]) -> Vec<String> {
let mut scoped = tenant_params(table);
scoped.extend(params.iter().cloned());
scoped
}
pub fn tenant_bind(table: &Table) -> Option<String> {
table.tenant().map(param_name)
}
pub fn key_param_type(table: &Table) -> String {
match table.key_columns().as_slice() {
[key] => param_type(key),
keys if keys.iter().all(|key| key.is_copy()) => key_struct_name(table),
_ => format!("&{}", key_struct_name(table)),
}
}
pub fn key_param(table: &Table) -> String {
format!("{}: {}", key_binding(table), key_param_type(table))
}
pub fn key_binds(table: &Table) -> Vec<String> {
match table.key_columns().as_slice() {
[key] => vec![param_name(key)],
keys => keys
.iter()
.map(|key| {
let borrow = if key.is_copy() { "" } else { "&" };
format!("{borrow}key.{}", key.rust_name)
})
.collect(),
}
}
pub fn scoped_key_binds(table: &Table) -> Vec<String> {
let mut binds: Vec<String> = tenant_bind(table).into_iter().collect();
binds.extend(key_binds(table));
binds
}
pub fn key_items(table: &Table) -> Vec<String> {
match table.key_columns().as_slice() {
[] => Vec::new(),
[key] => key
.typed_id
.iter()
.filter(|id| id.module == table.module_name)
.map(|id| id.name.clone())
.collect(),
_ => vec![key_struct_name(table)],
}
}
pub fn param_columns(table: &Table) -> Vec<&Column> {
let keys = table.key_columns();
let single = if keys.len() == 1 { keys } else { Vec::new() };
table.tenant().into_iter().chain(single).collect()
}
pub fn id_imports<'a>(
table: &Table,
columns: impl IntoIterator<Item = &'a Column>,
in_models: bool,
) -> Vec<String> {
let mut paths: Vec<String> = columns
.into_iter()
.filter_map(|column| column.typed_id.as_ref())
.filter_map(|id| {
if id.module != table.module_name {
Some(format!("super::super::{}::models::{}", id.module, id.name))
} else if in_models {
None
} else {
Some(format!("super::models::{}", id.name))
}
})
.collect();
paths.sort();
paths.dedup();
paths
}
pub fn render_key_type(output: &mut String, table: &Table, derives: DeriveOptions) {
let DeriveOptions { serde, validate } = derives;
let keys = table.key_columns();
match keys.as_slice() {
[] => {}
[key] => {
if let Some(id) = key
.typed_id
.as_ref()
.filter(|id| id.module == table.module_name)
{
render_typed_id(output, table, key, &id.name, serde);
if validate {
match key.base_type() {
BaseType::Text => super::validate::push_measured_id(output, &id.name),
BaseType::Integer => {
super::validate::push_ranged_id(output, &id.name, &key.sqlx_type);
}
_ => {}
}
}
}
}
_ => render_key_struct(output, table, &keys, serde),
}
}
fn render_typed_id(output: &mut String, table: &Table, key: &Column, name: &str, serde: bool) {
let inner = &key.sqlx_type;
let is_enum = key.base_type() == BaseType::Enum;
push_doc(
output,
0,
&[
&format!(
"The primary key of the `{}` table, as a distinct type so it",
table.display_name()
),
"cannot be mixed up with another table's key.",
],
);
let copy = if key.is_copy() || is_enum {
"Copy, "
} else {
""
};
let ord = if is_enum { "" } else { "PartialOrd, Ord, " };
output.push_str(&format!(
"#[derive(Debug, Clone, {copy}PartialEq, Eq, {ord}Hash, sqlx::Type)]\n"
));
push_derives(output, serde_derives(serde));
output.push_str("#[sqlx(transparent)]\n");
if serde {
output.push_str("#[serde(transparent)]\n");
}
output.push_str(&format!("pub struct {name}(pub {inner});\n\n"));
output.push_str(&format!(
"impl From<{inner}> for {name} {{\n fn from(value: {inner}) -> Self {{\n Self(value)\n }}\n}}\n\n"
));
output.push_str(&format!(
"impl From<{name}> for {inner} {{\n fn from(id: {name}) -> Self {{\n id.0\n }}\n}}\n\n"
));
if is_enum {
return;
}
output.push_str(&format!(
"impl std::fmt::Display for {name} {{\n fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{\n std::fmt::Display::fmt(&self.0, formatter)\n }}\n}}\n\n"
));
if let (Some(ids), Some(expr)) = (key.id_generation, key.new_id_expr()) {
let what = match ids {
IdGeneration::UuidV4 => "a random UUID (version 4)",
IdGeneration::UuidV7 => "a UUID version 7, ordered by creation time",
IdGeneration::Ulid => "a ULID, ordered by creation time",
};
output.push_str(&format!("impl {name} {{\n"));
push_doc(
output,
4,
&[&format!(
"A new id: {what}. `create` uses it when the input leaves the id out."
)],
);
output.push_str(&format!(
" #[must_use]\n pub fn generate() -> Self {{\n Self({expr})\n }}\n}}\n\n"
));
}
}
fn render_key_struct(output: &mut String, table: &Table, keys: &[&Column], serde: bool) {
push_doc(
output,
0,
&[&format!(
"The composite primary key of the `{}` table{}.",
table.display_name(),
if table.key_columns().len() < table.primary_key.len() {
", without the tenant column"
} else {
""
}
)],
);
let derives = if keys.iter().all(|key| key.supports_typed_id()) {
let copy = if keys.iter().all(|key| key.is_copy()) {
"Copy, "
} else {
""
};
format!("Debug, Clone, {copy}PartialEq, Eq, PartialOrd, Ord, Hash")
} else {
"Debug, Clone, PartialEq".to_owned()
};
output.push_str(&format!("#[derive({derives})]\n"));
push_derives(output, serde_derives(serde));
output.push_str(&format!("pub struct {} {{\n", key_struct_name(table)));
for key in keys {
push_doc(
output,
4,
&[&format!("The {} column.", super::doc_name(&key.sql_name))],
);
output.push_str(&format!(
" pub {}: {},\n",
key.rust_name,
key.value_type()
));
}
output.push_str("}\n\n");
}
pub fn render_row_key(output: &mut String, table: &Table) {
let keys = table.key_columns();
if keys.len() < 2 {
return;
}
let name = key_struct_name(table);
output.push_str(&format!("\nimpl {} {{\n", table.struct_name));
push_doc(output, 4, &["The row's primary key."]);
output.push_str(&format!(
" #[must_use]\n pub fn key(&self) -> {name} {{\n"
));
output.push_str(&format!(" {name} {{\n"));
for key in keys {
let clone = if key.is_copy() { "" } else { ".clone()" };
output.push_str(&format!(
" {}: self.{}{clone},\n",
key.rust_name, key.rust_name
));
}
output.push_str(" }\n }\n}\n");
}