use super::hierarchy::HierarchyCatalog;
use super::{columns::rows::RewriteRows, indexes::IndexBuildKeys};
use crate::mutation::constraints::context::ConstraintContext;
use uqa_core::Value;
use uqa_sql::SQLError;
pub struct KeyValidationContext<'a> {
pub catalog: &'a dyn HierarchyCatalog,
pub constraints: ConstraintContext<'a>,
}
fn ddl_storage_error(action: &str, error: uqa_storage::StorageBackendError) -> SQLError {
uqa_sql::catalog::errors::storage_error(action, &error)
}
pub fn validate_key_constraint_rows(
context: &KeyValidationContext<'_>,
table: &str,
rows: &mut RewriteRows,
) -> Result<(), SQLError> {
rows.spill()?;
for constraint in context
.catalog
.try_key_constraints(table)
.map_err(|error| ddl_storage_error("table rewrite", error))?
{
if constraint.without_overlaps {
continue;
}
let mut keys = IndexBuildKeys::new(
constraint.columns.len(),
context.constraints.memory.work_mem_bytes()?,
);
for position in 0..rows.len() {
let document = rows.get(position)?.document;
let values = constraint
.columns
.iter()
.map(|column| document.get(column).cloned().unwrap_or(Value::Null))
.collect::<Vec<_>>();
if constraint.kind == uqa_sql::ast::TableKeyConstraintKind::PrimaryKey {
if let Some(column) = constraint
.columns
.iter()
.zip(&values)
.find_map(|(column, value)| matches!(value, Value::Null).then_some(column))
{
return Err(null_key_column(table, column));
}
}
if constraint.kind == uqa_sql::ast::TableKeyConstraintKind::Unique
&& values.iter().any(|value| matches!(value, Value::Null))
&& !constraint.nulls_not_distinct
{
continue;
}
keys.push(values)?;
}
if let Some(values) = keys.first_duplicate(true, constraint.nulls_not_distinct)? {
return Err(duplicated_key(context, table, &constraint, &values)?);
}
}
Ok(())
}
pub fn validate_temporal_key_rows(
context: &KeyValidationContext<'_>,
table: &str,
) -> Result<(), SQLError> {
for constraint in context
.catalog
.try_key_constraints(table)
.map_err(|error| ddl_storage_error("table rewrite", error))?
{
if constraint.without_overlaps {
validate_key_index_rows(context, table, &constraint)?;
}
}
Ok(())
}
fn duplicated_key(
context: &KeyValidationContext<'_>,
table: &str,
constraint: &uqa_sql::ast::TableKeyConstraint,
values: &[Value],
) -> Result<SQLError, SQLError> {
Ok(super::indexes::unique_build::duplicated_index_key(
index_name(constraint)?,
crate::mutation::constraints::enforced_key_description(
context.constraints,
table,
&constraint.clone().into(),
values,
)?,
))
}
fn index_name(constraint: &uqa_sql::ast::TableKeyConstraint) -> Result<&str, SQLError> {
constraint
.name
.as_deref()
.ok_or_else(|| SQLError::Internal("key validation requires its reserved index name".into()))
}
pub fn validate_key_index_rows(
context: &KeyValidationContext<'_>,
table: &str,
constraint: &uqa_sql::ast::TableKeyConstraint,
) -> Result<(), SQLError> {
if constraint.without_overlaps {
for doc_id in context.constraints.reads.live_table_doc_ids(table)? {
let Some(document) = context.constraints.reads.get_document(table, doc_id)? else {
continue;
};
if crate::mutation::constraints::without_overlaps_conflict(
context.constraints,
table,
constraint,
&document,
Some(doc_id),
)? {
return Err(SQLError::Routine {
sqlstate: "23P01".into(),
message: format!(
"could not create constraint because relation \"{table}\" contains overlapping key values"
),
});
}
}
return Ok(());
}
let columns = context
.catalog
.try_describe_table(table)
.map_err(|error| ddl_storage_error("unique index build", error))?
.ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
let key_types = constraint
.columns
.iter()
.map(|name| {
columns
.iter()
.find(|column| column.name == *name)
.map(|column| column.ty.clone())
.ok_or_else(|| SQLError::UnknownColumn(name.clone()))
})
.collect::<Result<Vec<_>, _>>()?;
let keys = constraint
.columns
.iter()
.cloned()
.map(uqa_sql::ast::IndexKey::Column)
.collect::<Vec<_>>();
super::indexes::unique_build::validate_unique_index_build(
super::indexes::unique_build::UniqueBuildContext::of(&context.constraints),
&super::indexes::unique_build::UniqueIndexBuild {
table,
name: index_name(constraint)?,
keys: &keys,
key_types: &key_types,
predicate: None,
nulls_not_distinct: constraint.nulls_not_distinct,
},
)
}
pub fn validate_primary_key_rows(
context: &KeyValidationContext<'_>,
table: &str,
constraint: &uqa_sql::ast::TableKeyConstraint,
) -> Result<(), SQLError> {
if constraint.kind != uqa_sql::ast::TableKeyConstraintKind::PrimaryKey {
return Ok(());
}
let columns = context
.catalog
.try_describe_table(table)
.map_err(|error| ddl_storage_error("ALTER TABLE ADD CONSTRAINT", error))?
.ok_or_else(|| SQLError::UnknownTable(table.to_string()))?
.into_iter()
.map(|column| column.name)
.filter(|column| constraint.columns.contains(column))
.collect::<Vec<_>>();
for doc_id in context.constraints.reads.live_table_doc_ids(table)? {
let Some(document) = context.constraints.reads.get_document(table, doc_id)? else {
continue;
};
if let Some(column) = columns.iter().find(|column| {
matches!(
document.get(column.as_str()).unwrap_or(&Value::Null),
Value::Null
)
}) {
return Err(null_key_column(table, column));
}
}
Ok(())
}
fn null_key_column(table: &str, column: &str) -> SQLError {
let relation = uqa_core::RelationIdentity::from_legacy_name(table)
.map_or_else(|_| table.to_string(), |identity| identity.name);
SQLError::Routine {
sqlstate: "23502".into(),
message: format!("column \"{column}\" of relation \"{relation}\" contains null values"),
}
}