use crate::Conventions;
use crate::schema::{Column, Table};
pub const INCLUDE_DELETED: &str = "include_deleted";
pub fn apply(tables: &mut [Table], conventions: &Conventions) {
let version = conventions.version_column.as_deref();
for table in tables {
table.version_column = version
.filter(|name| table.columns.iter().any(|column| column.sql_name == *name))
.map(str::to_owned);
for column in &mut table.columns {
column.is_managed = is_managed(&column.sql_name, conventions);
}
let present = |name: &Option<String>| {
name.as_ref()
.filter(|name| table.columns.iter().any(|column| column.sql_name == **name))
.cloned()
};
let soft_delete_column = present(&conventions.soft_delete_column);
let tenant_column = present(&conventions.tenant_column);
table.soft_delete_column = soft_delete_column;
table.tenant_column = tenant_column;
}
}
fn is_managed(name: &str, conventions: &Conventions) -> bool {
conventions
.managed_columns
.iter()
.any(|managed| managed == name)
|| conventions.version_column.as_deref() == Some(name)
}
pub fn unsourced(table: &Table) -> Option<(&Column, String)> {
if !table.before_insert_triggers.is_empty() {
return None;
}
let column = table.columns.iter().find(|column| {
column.is_managed && !column.nullable && !column.has_default && !column.is_generated
})?;
let reason = format!(
"managed column `{}` is NOT NULL with no DEFAULT, and no BEFORE INSERT trigger is defined on the table, so inserts, which leave it out, have no value for it; add a DEFAULT, make the column nullable, or create a BEFORE INSERT trigger that sets it",
column.sql_name
);
Some((column, reason))
}
pub fn check(tables: &[Table], conventions: &Conventions) -> Result<(), String> {
let Some(name) = conventions.soft_delete_column.as_deref() else {
return Ok(());
};
for table in tables {
let Some(column) = table.columns.iter().find(|column| column.sql_name == name) else {
continue;
};
let subject = format!(
"soft-delete column `{}` of `{}`",
column.sql_name,
table.display_name()
);
if !is_timestamp(column) {
return Err(format!(
"{subject} must be a nullable TIMESTAMP or TIMESTAMPTZ, but its type maps to `{}`",
column.sqlx_type
));
}
if !column.nullable {
return Err(format!(
"{subject} must be a nullable TIMESTAMP or TIMESTAMPTZ, but it is NOT NULL"
));
}
let collision = table
.columns
.iter()
.find(|other| other.rust_name == INCLUDE_DELETED);
if let Some(other) = collision {
return Err(format!(
"column `{}` of `{}` collides with the search filter's `{INCLUDE_DELETED}` switch, which the {subject} adds",
other.sql_name,
table.display_name()
));
}
}
Ok(())
}
fn is_timestamp(column: &Column) -> bool {
column.base_type().is_timestamp()
}