use super::{ConstraintContext, ConstraintStatement};
use crate::catalog::projection::CatalogOutput;
use crate::mutation::errors::dml_storage_error;
use uqa_core::Value;
use uqa_sql::{
ast::{ColumnType, GeneratedColumnKind},
result::format_postgres_text,
semantics::{partition::PartitionRejection, view_mutation::ViewMutationTarget},
SQLError,
};
use uqa_storage::document_store::Document;
const DESCRIBED_VALUE_BYTES: usize = 64;
pub(super) fn not_null_violation(
context: ConstraintContext<'_>,
statement: Option<ConstraintStatement<'_>>,
table: &str,
column: &str,
document: &Document,
) -> SQLError {
let relation = relation_name(table);
let Some(statement) = statement else {
return violation(
"23502",
format!("column \"{column}\" of relation \"{relation}\" contains null values"),
Ok(None),
);
};
violation(
"23502",
format!(
"null value in column \"{column}\" of relation \"{relation}\" violates not-null constraint"
),
failing_row_detail(context, statement, table, document),
)
}
pub(super) fn check_violation(
context: ConstraintContext<'_>,
statement: Option<ConstraintStatement<'_>>,
table: &str,
constraint: &str,
document: &Document,
) -> SQLError {
let relation = relation_name(table);
let Some(statement) = statement else {
return violation(
"23514",
format!(
"check constraint \"{constraint}\" of relation \"{relation}\" is violated by some row"
),
Ok(None),
);
};
violation(
"23514",
format!("new row for relation \"{relation}\" violates check constraint \"{constraint}\""),
failing_row_detail(context, statement, table, document),
)
}
pub fn partition_rejection_error(
context: ConstraintContext<'_>,
statement: ConstraintStatement<'_>,
rejection: PartitionRejection,
document: &Document,
) -> SQLError {
match rejection {
PartitionRejection::Constraint { relation } => violation(
"23514",
format!(
"new row for relation \"{}\" violates partition constraint",
relation_name(&relation)
),
failing_row_detail(context, statement, &relation, document),
),
PartitionRejection::NoPartition { relation, keys } => violation(
"23514",
format!(
"no partition of relation \"{}\" found for row",
relation_name(&relation)
),
partition_key_detail(context, statement, &relation, &keys),
),
}
}
pub fn view_check_violation(
context: ConstraintContext<'_>,
statement: ConstraintStatement<'_>,
view: &str,
table: &str,
document: &Document,
) -> SQLError {
violation(
"44000",
format!(
"new row violates check option for view \"{}\"",
relation_name(view)
),
failing_row_detail(context, statement, table, document),
)
}
pub fn trigger_view_check_violation(
context: ConstraintContext<'_>,
view: &str,
target: &ViewMutationTarget,
supplied: &[String],
values: &[Value],
) -> SQLError {
violation(
"44000",
format!(
"new row violates check option for view \"{}\"",
relation_name(view)
),
view_row_detail(context, target, supplied, values),
)
}
fn violation(
sqlstate: &str,
message: String,
detail: Result<Option<String>, SQLError>,
) -> SQLError {
match detail {
Ok(detail) => SQLError::Diagnostic {
sqlstate: sqlstate.into(),
message,
detail,
hint: None,
},
Err(error) => error,
}
}
fn failing_row_detail(
context: ConstraintContext<'_>,
statement: ConstraintStatement<'_>,
table: &str,
document: &Document,
) -> Result<Option<String>, SQLError> {
let relation = described_relation(context, statement, table)?;
let columns = context
.catalog
.try_describe_table(&relation)
.map_err(|error| dml_storage_error("constraint violation", error))?
.ok_or_else(|| SQLError::UnknownTable(relation.clone()))?;
let diagnostics = context.diagnostics.diagnostic_context();
let visible = diagnostics.authorization.row_description_columns(
&relation,
statement.columns,
statement.referential_action,
)?;
let output = CatalogOutput(diagnostics.catalog);
describe_row(
visible.as_deref(),
columns.iter().map(|column| {
let virtual_column = column
.generated
.as_ref()
.is_some_and(|generated| generated.kind == GeneratedColumnKind::Virtual);
DescribedColumn {
name: &column.name,
value: if virtual_column {
DescribedValue::Virtual
} else {
DescribedValue::Stored(document.get(&column.name))
},
ty: Some(&column.ty),
}
}),
&output,
)
}
fn view_row_detail(
context: ConstraintContext<'_>,
target: &ViewMutationTarget,
supplied: &[String],
values: &[Value],
) -> Result<Option<String>, SQLError> {
let diagnostics = context.diagnostics.diagnostic_context();
let visible = diagnostics.authorization.view_row_description_columns(
&target.definition,
&target.columns,
supplied,
)?;
let output = CatalogOutput(diagnostics.catalog);
describe_row(
visible.as_deref(),
target
.columns
.iter()
.zip(&target.types)
.zip(values)
.map(|((name, ty), value)| DescribedColumn {
name,
value: DescribedValue::Stored(Some(value)),
ty: ty.as_ref(),
}),
&output,
)
}
struct DescribedColumn<'a> {
name: &'a str,
value: DescribedValue<'a>,
ty: Option<&'a ColumnType>,
}
enum DescribedValue<'a> {
Stored(Option<&'a Value>),
Virtual,
}
fn describe_row<'a>(
visible: Option<&[String]>,
columns: impl Iterator<Item = DescribedColumn<'a>>,
output: &CatalogOutput<'_>,
) -> Result<Option<String>, SQLError> {
let mut names = Vec::new();
let mut values = Vec::new();
for column in columns {
if let Some(visible) = visible {
if !visible.iter().any(|name| name == column.name) {
continue;
}
names.push(column.name);
}
values.push(match column.value {
DescribedValue::Virtual => "virtual".to_string(),
DescribedValue::Stored(None | Some(Value::Null)) => "null".to_string(),
DescribedValue::Stored(Some(value)) => clip(match column.ty {
Some(ty) => format_postgres_text(value, ty, Some(output))?,
None => uqa_sql::expr::value_to_string(value)?,
}),
});
}
Ok(match visible {
None => Some(format!("Failing row contains ({}).", values.join(", "))),
Some(_) if names.is_empty() => None,
Some(_) => Some(format!(
"Failing row contains ({}) = ({}).",
names.join(", "),
values.join(", ")
)),
})
}
fn partition_key_detail(
context: ConstraintContext<'_>,
statement: ConstraintStatement<'_>,
relation: &str,
keys: &[Value],
) -> Result<Option<String>, SQLError> {
let hierarchy = context
.partitions
.catalog
.try_table_hierarchy(relation)
.map_err(|error| SQLError::Internal(format!("read partition metadata: {error}")))?;
let spec = hierarchy.partition_spec.as_ref().ok_or_else(|| {
SQLError::Internal(format!(
"partitioned table `{relation}` has no partition key"
))
})?;
let diagnostics = context.diagnostics.diagnostic_context();
if !diagnostics.authorization.can_view_partition_key(
relation,
&spec.keys,
statement.referential_action,
)? {
return Ok(None);
}
let catalog = diagnostics.catalog.catalog_read_view();
let resolution = diagnostics
.catalog
.session_execution_view()
.relation_name_resolution();
let names = crate::catalog::projection::partition_key_columns(
Some(&crate::catalog::projection::CatalogOutput(
diagnostics.catalog,
)),
&catalog,
&resolution,
spec,
true,
)?;
let types = crate::catalog::projection::partition_key_types_for_table(
&diagnostics.catalog,
&catalog,
&resolution,
relation,
)?;
let output = CatalogOutput(diagnostics.catalog);
let values = keys
.iter()
.zip(&types)
.map(|(value, ty)| match value {
Value::Null => Ok("null".to_string()),
value => format_postgres_text(value, ty, Some(&output)).map(clip),
})
.collect::<Result<Vec<_>, SQLError>>()?;
Ok(Some(format!(
"Partition key of the failing row contains ({names}) = ({}).",
values.join(", ")
)))
}
fn described_relation(
context: ConstraintContext<'_>,
statement: ConstraintStatement<'_>,
table: &str,
) -> Result<String, SQLError> {
if statement.relation == table
|| context
.catalog
.hierarchy_scan_tables(statement.relation, true)?
.iter()
.any(|descendant| descendant == table)
{
return Ok(statement.relation.to_string());
}
Ok(table.to_string())
}
fn relation_name(table: &str) -> String {
uqa_core::RelationIdentity::from_legacy_name(table)
.map_or_else(|_| table.to_string(), |identity| identity.name)
}
fn clip(mut text: String) -> String {
if text.len() <= DESCRIBED_VALUE_BYTES {
return text;
}
let mut end = DESCRIBED_VALUE_BYTES;
while !text.is_char_boundary(end) {
end -= 1;
}
text.truncate(end);
text.push_str("...");
text
}
#[cfg(test)]
mod tests {
use super::clip;
#[test]
fn a_described_value_is_cut_at_a_character_boundary_after_64_bytes() {
assert_eq!(clip("a".repeat(64)), "a".repeat(64));
assert_eq!(clip("a".repeat(65)), format!("{}...", "a".repeat(64)));
let greek = "\u{3ba}\u{3cc}\u{3c3}\u{3bc}\u{3b5}".repeat(10);
let clipped = clip(greek.clone());
assert_eq!(
clipped,
format!("{}...", greek.chars().take(32).collect::<String>())
);
let wide = format!("{}\u{20ac}", "a".repeat(63));
assert_eq!(clip(wide), format!("{}...", "a".repeat(63)));
}
}