Skip to main content

uqa_sql/semantics/
foreign_keys.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-key comparison types, null matching, and normalized key values.
8use crate::{
9    ast::{ForeignKey, ForeignKeyMatch},
10    semantics::partition::PartitionCatalog,
11    ColumnType, ResultRow as Document, SQLError,
12};
13use uqa_core::Value;
14fn dml_storage_error(action: &str, error: impl std::fmt::Display) -> SQLError {
15    SQLError::Internal(format!("{action} failed in storage backend: {error}"))
16}
17
18pub struct ForeignKeyLookup {
19    pub values: Vec<Value>,
20    pub comparison: ForeignKeyComparison,
21}
22
23pub struct ForeignKeyComparison {
24    pub comparison_types: Vec<ColumnType>,
25    pub exact_reference_lookup: bool,
26}
27
28impl ForeignKeyComparison {
29    pub fn normalize(&self, values: Vec<Value>) -> Result<Vec<Value>, SQLError> {
30        normalize_foreign_key_values(values, &self.comparison_types)
31    }
32}
33
34pub fn foreign_key_relation_name(table: &str) -> String {
35    uqa_core::RelationIdentity::from_legacy_name(table)
36        .map_or_else(|_| table.to_string(), |relation| relation.name)
37}
38
39pub fn foreign_key_lookup_values(
40    catalog: &dyn PartitionCatalog,
41    table: &str,
42    fk: &ForeignKey,
43    document: &Document,
44) -> Result<Option<ForeignKeyLookup>, SQLError> {
45    let comparison = foreign_key_comparison_types(catalog, table, fk)?;
46    Ok(foreign_key_values(fk, document, &comparison)?
47        .map(|values| ForeignKeyLookup { values, comparison }))
48}
49
50pub fn foreign_key_values(
51    fk: &ForeignKey,
52    document: &Document,
53    comparison: &ForeignKeyComparison,
54) -> Result<Option<Vec<Value>>, SQLError> {
55    let local_values: Vec<Value> = fk
56        .local_columns
57        .iter()
58        .map(|c| document.get(c).cloned().unwrap_or(Value::Null))
59        .collect();
60    let null_count = local_values
61        .iter()
62        .filter(|value| matches!(value, Value::Null))
63        .count();
64    if null_count == 0 {
65        if local_values.len() != fk.ref_columns.len() {
66            return Err(SQLError::Internal(
67                "FOREIGN KEY local and referenced column counts diverged after validation".into(),
68            ));
69        }
70        return comparison.normalize(local_values).map(Some);
71    }
72    match fk.match_type {
73        ForeignKeyMatch::Simple => Ok(None),
74        ForeignKeyMatch::Full if null_count == local_values.len() => Ok(None),
75        ForeignKeyMatch::Full => {
76            Err(SQLError::Routine {
77                sqlstate: "23503".into(),
78                message: format!(
79                    "insert or update on table violates foreign key constraint \"{}\": MATCH FULL does not allow mixing of null and nonnull key values",
80                    fk.name.as_deref().unwrap_or("<unnamed>")
81                ),
82            })
83        }
84    }
85}
86
87pub fn foreign_key_comparison_types(
88    catalog: &dyn PartitionCatalog,
89    table: &str,
90    fk: &ForeignKey,
91) -> Result<ForeignKeyComparison, SQLError> {
92    if fk.local_columns.len() != fk.ref_columns.len() {
93        return Err(SQLError::Internal(
94            "FOREIGN KEY local and referenced column counts diverged after validation".into(),
95        ));
96    }
97    let local_columns = catalog
98        .try_describe_table(table)
99        .map_err(|error| dml_storage_error("FOREIGN KEY local columns", error))?
100        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
101    let referenced_columns = catalog
102        .try_describe_table(&fk.ref_table)
103        .map_err(|error| dml_storage_error("FOREIGN KEY referenced columns", error))?
104        .ok_or_else(|| SQLError::UnknownTable(fk.ref_table.clone()))?;
105    let mut comparison_types = Vec::with_capacity(fk.local_columns.len());
106    let mut exact_reference_lookup = true;
107    for (local_column, referenced_column) in fk.local_columns.iter().zip(&fk.ref_columns) {
108        let local_type = local_columns
109            .iter()
110            .find(|definition| definition.name == *local_column)
111            .map(|definition| &definition.ty)
112            .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{local_column}")))?;
113        let referenced_type = referenced_columns
114            .iter()
115            .find(|definition| definition.name == *referenced_column)
116            .map(|definition| &definition.ty)
117            .ok_or_else(|| {
118                SQLError::UnknownColumn(format!("{}.{referenced_column}", fk.ref_table))
119            })?;
120        let comparison_type =
121            crate::type_resolution::foreign_key_operand_type(local_type, referenced_type).map_err(|_| {
122                SQLError::Routine {
123                    sqlstate: "42804".into(),
124                    message: format!(
125                        "foreign key constraint cannot be implemented: key columns \"{local_column}\" and \"{referenced_column}\" are of incompatible types: {} and {}",
126                        local_type.sql_name(),
127                        referenced_type.sql_name()
128                    ),
129                }
130            })?;
131        exact_reference_lookup &= comparison_type == *referenced_type;
132        comparison_types.push(comparison_type);
133    }
134    Ok(ForeignKeyComparison {
135        comparison_types,
136        exact_reference_lookup,
137    })
138}
139
140pub fn foreign_key_parent_values(
141    fk: &ForeignKey,
142    document: &Document,
143    comparison: &ForeignKeyComparison,
144) -> Result<Vec<Value>, SQLError> {
145    comparison.normalize(
146        fk.ref_columns
147            .iter()
148            .map(|column| document.get(column).cloned().unwrap_or(Value::Null))
149            .collect(),
150    )
151}
152
153pub fn normalize_foreign_key_values(
154    values: Vec<Value>,
155    comparison_types: &[ColumnType],
156) -> Result<Vec<Value>, SQLError> {
157    if values.len() != comparison_types.len() {
158        return Err(SQLError::Internal(
159            "FOREIGN KEY value and comparison-type counts diverged after validation".into(),
160        ));
161    }
162    values
163        .into_iter()
164        .zip(comparison_types)
165        .map(|(value, ty)| crate::assignment::conversion::convert_value_to_column_type(value, ty))
166        .collect()
167}