pub mod codegen;
pub use dibs_db_schema::{
CheckConstraint, Column, ForeignKey, Index, IndexColumn, NullsOrder, PgType, Schema, SortOrder,
SourceLocation, Table, TableDef, TriggerCheckConstraint,
};
pub trait SchemaCodegen {
fn to_sql(&self) -> String;
}
impl SchemaCodegen for Schema {
fn to_sql(&self) -> String {
codegen::schema_to_sql(self)
}
}
pub fn create_table_sql(table: &Table) -> String {
let mut sql = format!("CREATE TABLE {} (\n", crate::quote_ident(&table.name));
let pk_columns: Vec<&str> = table
.columns
.iter()
.filter(|c| c.primary_key)
.map(|c| c.name.as_str())
.collect();
let use_table_pk_constraint = pk_columns.len() > 1;
let mut parts: Vec<String> = table
.columns
.iter()
.map(|col| {
let mut def = format!(" {} {}", crate::quote_ident(&col.name), col.pg_type);
if col.is_identity() {
def.push_str(" GENERATED BY DEFAULT AS IDENTITY");
}
if col.primary_key && !use_table_pk_constraint {
def.push_str(" PRIMARY KEY");
}
if !col.nullable && (!col.primary_key || use_table_pk_constraint) {
def.push_str(" NOT NULL");
}
if col.unique && !col.primary_key {
def.push_str(" UNIQUE");
}
if let Some(default) = &col.default {
def.push_str(&format!(" DEFAULT {}", default));
}
def
})
.collect();
if use_table_pk_constraint {
let quoted_pk_cols: Vec<_> = pk_columns.iter().map(|c| crate::quote_ident(c)).collect();
parts.push(format!(" PRIMARY KEY ({})", quoted_pk_cols.join(", ")));
}
for check in &table.check_constraints {
parts.push(format!(
" CONSTRAINT {} CHECK ({})",
crate::quote_ident(&check.name),
check.expr
));
}
sql.push_str(&parts.join(",\n"));
sql.push_str("\n);");
sql
}
pub fn create_index_sql(table: &Table, idx: &Index) -> String {
let unique = if idx.unique { "UNIQUE " } else { "" };
let quoted_cols: Vec<_> = idx.columns.iter().map(index_column_to_sql).collect();
let where_clause = idx
.where_clause
.as_ref()
.map(|w| format!(" WHERE {}", w))
.unwrap_or_default();
format!(
"CREATE {}INDEX {} ON {} ({}){};",
unique,
crate::quote_ident(&idx.name),
crate::quote_ident(&table.name),
quoted_cols.join(", "),
where_clause
)
}
pub fn create_trigger_check_function_sql(trig: &TriggerCheckConstraint) -> String {
let fn_name = crate::trigger_check_function_name(&trig.name);
let message = trig
.message
.as_deref()
.unwrap_or("trigger check failed")
.replace('\'', "''");
format!(
"CREATE OR REPLACE FUNCTION {}() RETURNS trigger LANGUAGE plpgsql AS $$\n\
BEGIN\n\
IF NOT ({}) THEN\n\
RAISE EXCEPTION '{}' USING ERRCODE = '23514';\n\
END IF;\n\
RETURN NEW;\n\
END;\n\
$$;",
crate::quote_ident(&fn_name),
trig.expr,
message
)
}
pub fn create_trigger_check_sql(table: &Table, trig: &TriggerCheckConstraint) -> String {
let fn_name = crate::trigger_check_function_name(&trig.name);
format!(
"CREATE TRIGGER {} BEFORE INSERT OR UPDATE ON {} FOR EACH ROW EXECUTE FUNCTION {}();",
crate::quote_ident(&trig.name),
crate::quote_ident(&table.name),
crate::quote_ident(&fn_name)
)
}
pub fn index_column_to_sql(col: &IndexColumn) -> String {
format!(
"{}{}{}",
crate::quote_ident(&col.name),
col.order.to_sql(),
col.nulls.to_sql()
)
}
pub fn collect_schema() -> Schema {
let tables = inventory::iter::<TableDef>
.into_iter()
.filter_map(|def| def.to_table())
.map(|t| (t.name.clone(), t))
.collect();
let schema = Schema { tables };
assert!(
!schema.tables.is_empty(),
"dibs::collect_schema() found zero registered tables.\n\
This almost always means the crate defining your #[facet(dibs::table)] \
types was not linked in, so inventory saw no submissions.\n\
In your build.rs / binary, call your db crate's `ensure_linked()` before \
collecting the schema, e.g. `my_app_db::ensure_linked();`.\n\
A `TypeId::of::<_>()` / `type_name::<_>()` reference is NOT enough — it is \
a const intrinsic that creates no link-time dependency on the crate's statics."
);
schema
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_column_to_sql() {
let col = IndexColumn::new("name");
assert_eq!(index_column_to_sql(&col), "\"name\"");
let col = IndexColumn::desc("created_at");
assert_eq!(index_column_to_sql(&col), "\"created_at\" DESC");
let col = IndexColumn::nulls_first("reminder_sent_at");
assert_eq!(
index_column_to_sql(&col),
"\"reminder_sent_at\" NULLS FIRST"
);
let col = IndexColumn {
name: "priority".to_string(),
order: SortOrder::Desc,
nulls: NullsOrder::Last,
};
assert_eq!(index_column_to_sql(&col), "\"priority\" DESC NULLS LAST");
}
}