use std::fmt::Write;
use uqa_core::Value;
use uqa_sql::ast::{ForeignKeyAction, ForeignKeyMatch};
use uqa_sql::expr::quote_ident;
use uqa_sql::{ResultRow, SQLError};
use crate::catalog::context::CatalogContext;
use crate::catalog::{CatalogReadView, RelationNameResolution};
use super::super::helpers::constraints::{
constraint_catalog_rows, ConstraintCatalogKind, ConstraintCatalogRow,
};
use super::super::helpers::oids::namespace_oid;
use super::super::helpers::rows::{bool_value, int_value, row, str_value};
use super::constraints::{constraint_index_oid, constraint_row_oid};
pub fn pg_get_constraintdef_value(
context: &CatalogContext<'_>,
arguments: &[Value],
) -> Result<Value, SQLError> {
let (oid, pretty) = match arguments {
[oid] => (oid, &Value::Bool(false)),
[oid, pretty] => (oid, pretty),
_ => {
return Err(SQLError::BadArity {
name: "pg_get_constraintdef".into(),
expected: "1 or 2".into(),
actual: arguments.len(),
})
}
};
let (Value::Int(oid), Value::Bool(pretty)) = (oid, pretty) else {
if matches!(oid, Value::Null) || matches!(pretty, Value::Null) {
return Ok(Value::Null);
}
return Err(SQLError::TypeMismatch(
"pg_get_constraintdef requires oid and boolean arguments".into(),
));
};
let catalog = context.catalog_read_view();
let resolution = context.session_execution_view().relation_name_resolution();
if let Some(constraint) = constraint_catalog_rows(&catalog, &resolution)?
.into_iter()
.find(|constraint| constraint_row_oid(constraint) == *oid)
{
return relation_constraint_definition(
Some(&crate::catalog::projection::CatalogOutput(*context)),
&catalog,
&resolution,
&constraint,
*pretty,
)
.map(Value::Str);
}
domain_constraint_definition(
Some(&crate::catalog::projection::CatalogOutput(*context)),
&catalog,
&resolution,
*oid,
*pretty,
)
.map(|definition| definition.map_or(Value::Null, Value::Str))
}
fn relation_constraint_definition(
output: Option<&dyn uqa_sql::expr::EngineHook>,
catalog: &CatalogReadView,
resolution: &RelationNameResolution,
constraint: &ConstraintCatalogRow,
pretty: bool,
) -> Result<String, SQLError> {
let mut definition =
match constraint.kind {
ConstraintCatalogKind::NotNull => {
let column = constraint.columns.first().ok_or_else(|| {
SQLError::Internal("NOT NULL constraint has no column".into())
})?;
format!("NOT NULL {}", quote_ident(&column.name))
}
ConstraintCatalogKind::Check => {
let expression = constraint.expression.as_ref().ok_or_else(|| {
SQLError::Internal("CHECK constraint has no expression".into())
})?;
format!(
"CHECK ({})",
super::super::view_definition::stored_expression_definition(
output, catalog, resolution, expression, pretty,
)?
)
}
ConstraintCatalogKind::PrimaryKey | ConstraintCatalogKind::Unique { .. } => {
key_constraint_definition(catalog, resolution, constraint)?
}
ConstraintCatalogKind::ForeignKey => {
foreign_key_definition(catalog, resolution, constraint)?
}
};
if constraint.state.no_inherit()
&& matches!(
constraint.kind,
ConstraintCatalogKind::NotNull | ConstraintCatalogKind::Check
)
{
definition.push_str(" NO INHERIT");
}
if constraint.state.deferrable() {
definition.push_str(" DEFERRABLE");
}
if constraint.state.initially_deferred() {
definition.push_str(" INITIALLY DEFERRED");
}
if !constraint.state.enforced() {
definition.push_str(" NOT ENFORCED");
} else if !constraint.state.validated() {
definition.push_str(" NOT VALID");
}
Ok(definition)
}
fn key_constraint_definition(
catalog: &CatalogReadView,
resolution: &RelationNameResolution,
constraint: &ConstraintCatalogRow,
) -> Result<String, SQLError> {
let mut definition = if constraint.kind == ConstraintCatalogKind::PrimaryKey {
"PRIMARY KEY ".to_string()
} else if constraint.kind.nulls_distinct() == Some(false) {
"UNIQUE NULLS NOT DISTINCT ".to_string()
} else {
"UNIQUE ".to_string()
};
let mut keys = constraint
.columns
.iter()
.map(|column| quote_ident(&column.name))
.collect::<Vec<_>>();
if constraint.period {
if let Some(last) = keys.last_mut() {
last.push_str(" WITHOUT OVERLAPS");
}
}
write!(definition, "({})", keys.join(", ")).expect("writing to a String cannot fail");
let indexes = super::catalog_index_relations(catalog, resolution)?;
let index_oid = constraint_index_oid(constraint, &indexes);
if let Some(index) = indexes.iter().find(|index| index.oid() == index_oid) {
let included = &index.definition.included_columns;
if !included.is_empty() {
let included = included
.iter()
.map(|name| quote_ident(name))
.collect::<Vec<_>>();
write!(definition, " INCLUDE ({})", included.join(", "))
.expect("writing to a String cannot fail");
}
}
Ok(definition)
}
fn foreign_key_definition(
catalog: &CatalogReadView,
resolution: &RelationNameResolution,
constraint: &ConstraintCatalogRow,
) -> Result<String, SQLError> {
let foreign_key = constraint
.foreign_key
.as_ref()
.ok_or_else(|| SQLError::Internal("FOREIGN KEY constraint has no referenced key".into()))?;
let referenced_name = format!(
"{}.{}",
quote_ident(&foreign_key.schema),
quote_ident(&foreign_key.table)
);
let referenced = catalog
.table(resolution, &referenced_name)?
.ok_or_else(|| SQLError::UnknownTable(referenced_name.clone()))?;
let mut remote = foreign_key
.column_ordinals
.iter()
.map(|ordinal| {
uqa_sql::catalog::relation_attributes::column_by_number(&referenced.columns, *ordinal)
.map(|column| quote_ident(&column.name))
.ok_or_else(|| SQLError::Internal("referenced key column disappeared".into()))
})
.collect::<Result<Vec<_>, _>>()?;
let mut local = constraint
.columns
.iter()
.map(|column| quote_ident(&column.name))
.collect::<Vec<_>>();
if constraint.period {
for keys in [&mut local, &mut remote] {
if let Some(last) = keys.last_mut() {
*last = format!("PERIOD {last}");
}
}
}
let mut definition = format!(
"FOREIGN KEY ({}) REFERENCES {}({})",
local.join(", "),
relation_display_name(catalog, resolution, &foreign_key.schema, &foreign_key.table)?,
remote.join(", ")
);
if foreign_key.match_type == ForeignKeyMatch::Full {
definition.push_str(" MATCH FULL");
}
if let Some(action) = foreign_key_action(foreign_key.on_update) {
write!(definition, " ON UPDATE {action}").expect("writing to a String cannot fail");
}
if let Some(action) = foreign_key_action(foreign_key.on_delete) {
write!(definition, " ON DELETE {action}").expect("writing to a String cannot fail");
}
Ok(definition)
}
const fn foreign_key_action(action: ForeignKeyAction) -> Option<&'static str> {
match action {
ForeignKeyAction::NoAction => None,
ForeignKeyAction::Restrict => Some("RESTRICT"),
ForeignKeyAction::Cascade => Some("CASCADE"),
ForeignKeyAction::SetNull => Some("SET NULL"),
ForeignKeyAction::SetDefault => Some("SET DEFAULT"),
}
}
fn relation_display_name(
catalog: &CatalogReadView,
resolution: &RelationNameResolution,
schema: &str,
table: &str,
) -> Result<String, SQLError> {
let canonical = format!("{}.{}", quote_ident(schema), quote_ident(table));
let visible = catalog
.relation_kind_resolution(resolution, "e_ident(table))?
.into_found()
.is_some_and(|(found, _)| {
found == canonical
|| uqa_core::RelationIdentity::from_legacy_name(&found)
.is_ok_and(|identity| identity.schema == schema && identity.name == table)
});
Ok(if visible {
quote_ident(table)
} else {
canonical
})
}
fn domain_constraint_definition(
output: Option<&dyn uqa_sql::expr::EngineHook>,
catalog: &CatalogReadView,
resolution: &RelationNameResolution,
oid: i64,
pretty: bool,
) -> Result<Option<String>, SQLError> {
for domain in catalog.domains() {
if domain
.definition
.not_null
.as_ref()
.and_then(|constraint| constraint.catalog_identity)
.is_some_and(|identity| identity.oid == oid)
{
return Ok(Some("NOT NULL".into()));
}
if let Some(check) = domain.definition.checks.iter().find(|check| {
check
.catalog_identity
.is_some_and(|identity| identity.oid == oid)
}) {
return super::super::view_definition::stored_domain_expression_definition(
output,
catalog,
resolution,
&check.expression,
pretty,
)
.map(|expression| {
let validation = if check.validated { "" } else { " NOT VALID" };
Some(format!("CHECK ({expression}){validation}"))
});
}
}
Ok(None)
}
pub fn domain_constraint_rows(catalog: &CatalogReadView) -> Result<Vec<ResultRow>, SQLError> {
let mut rows = Vec::new();
for domain in catalog.domains() {
let namespace = namespace_oid(catalog, &domain.identity.schema);
let not_null = domain.definition.not_null.iter().map(|constraint| {
(
constraint.name.as_deref(),
constraint.catalog_identity,
"n",
true,
)
});
let checks = domain.definition.checks.iter().map(|constraint| {
(
constraint.name.as_deref(),
constraint.catalog_identity,
"c",
constraint.validated,
)
});
for (name, identity, kind, validated) in not_null.chain(checks) {
let (Some(name), Some(identity)) = (name, identity) else {
return Err(SQLError::Internal(format!(
"constraint of domain {} has no catalog identity",
domain.identity.qualified_name()
)));
};
rows.push(row([
("oid", int_value(identity.oid)),
("conname", str_value(name.to_string())),
("connamespace", int_value(namespace)),
("contype", str_value(kind)),
("condeferrable", bool_value(false)),
("condeferred", bool_value(false)),
("conenforced", bool_value(true)),
("convalidated", bool_value(validated)),
("conrelid", int_value(0)),
("contypid", int_value(i64::from(domain.oid))),
("conindid", int_value(0)),
("conparentid", int_value(0)),
("confrelid", int_value(0)),
("confupdtype", str_value(" ")),
("confdeltype", str_value(" ")),
("confmatchtype", str_value(" ")),
("conislocal", bool_value(true)),
("coninhcount", int_value(0)),
("connoinherit", bool_value(false)),
("conperiod", bool_value(false)),
("conkey", Value::Null),
("confkey", Value::Null),
("conpfeqop", Value::Null),
("conppeqop", Value::Null),
("conffeqop", Value::Null),
("conexclop", Value::Null),
("conbin", Value::Null),
]));
}
}
Ok(rows)
}