1use 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 local_types: Vec<ColumnType>,
27 pub referenced_types: Vec<ColumnType>,
28 pub exact_reference_lookup: bool,
29 pub exact_local_lookup: bool,
31}
32
33#[cfg(test)]
34mod tests;
35
36impl ForeignKeyComparison {
37 pub fn normalize(&self, values: Vec<Value>) -> Result<Vec<Value>, SQLError> {
38 normalize_foreign_key_values(values, &self.comparison_types)
39 }
40
41 pub fn normalize_local(&self, values: Vec<Value>) -> Result<Vec<Value>, SQLError> {
42 normalize_typed_values(values, &self.local_types, &self.comparison_types)
43 }
44
45 pub fn normalize_referenced(&self, values: Vec<Value>) -> Result<Vec<Value>, SQLError> {
46 normalize_typed_values(values, &self.referenced_types, &self.comparison_types)
47 }
48}
49
50pub fn foreign_key_relation_name(table: &str) -> String {
51 uqa_core::RelationIdentity::from_legacy_name(table)
52 .map_or_else(|_| table.to_string(), |relation| relation.name)
53}
54
55pub fn foreign_key_lookup_values(
56 catalog: &dyn PartitionCatalog,
57 table: &str,
58 fk: &ForeignKey,
59 document: &Document,
60) -> Result<Option<ForeignKeyLookup>, SQLError> {
61 let comparison = foreign_key_comparison_types(catalog, table, fk)?;
62 Ok(foreign_key_values(table, fk, document, &comparison)?
63 .map(|values| ForeignKeyLookup { values, comparison }))
64}
65
66pub fn foreign_key_values(
68 table: &str,
69 fk: &ForeignKey,
70 document: &Document,
71 comparison: &ForeignKeyComparison,
72) -> Result<Option<Vec<Value>>, SQLError> {
73 let local_values: Vec<Value> = fk
74 .local_columns
75 .iter()
76 .map(|c| document.get(c).cloned().unwrap_or(Value::Null))
77 .collect();
78 let null_count = local_values
79 .iter()
80 .filter(|value| matches!(value, Value::Null))
81 .count();
82 if null_count == 0 {
83 if local_values.len() != fk.ref_columns.len() {
84 return Err(SQLError::Internal(
85 "FOREIGN KEY local and referenced column counts diverged after validation".into(),
86 ));
87 }
88 return comparison.normalize_local(local_values).map(Some);
89 }
90 match fk.match_type {
91 ForeignKeyMatch::Simple => Ok(None),
92 ForeignKeyMatch::Full if null_count == local_values.len() => Ok(None),
93 ForeignKeyMatch::Full => Err(SQLError::Diagnostic {
94 sqlstate: "23503".into(),
95 message: format!(
96 "insert or update on table \"{}\" violates foreign key constraint \"{}\"",
97 foreign_key_relation_name(table),
98 fk.name.as_deref().unwrap_or("<unnamed>")
99 ),
100 detail: Some("MATCH FULL does not allow mixing of null and nonnull key values.".into()),
101 hint: None,
102 }),
103 }
104}
105
106pub fn foreign_key_comparison_types(
107 catalog: &dyn PartitionCatalog,
108 table: &str,
109 fk: &ForeignKey,
110) -> Result<ForeignKeyComparison, SQLError> {
111 if fk.local_columns.len() != fk.ref_columns.len() {
112 return Err(SQLError::Internal(
113 "FOREIGN KEY local and referenced column counts diverged after validation".into(),
114 ));
115 }
116 let local_columns = catalog
117 .try_describe_table(table)
118 .map_err(|error| dml_storage_error("FOREIGN KEY local columns", error))?
119 .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
120 let referenced_columns = catalog
121 .try_describe_table(&fk.ref_table)
122 .map_err(|error| dml_storage_error("FOREIGN KEY referenced columns", error))?
123 .ok_or_else(|| SQLError::UnknownTable(fk.ref_table.clone()))?;
124 let mut comparison_types = Vec::with_capacity(fk.local_columns.len());
125 let mut local_types = Vec::with_capacity(fk.local_columns.len());
126 let mut referenced_types = Vec::with_capacity(fk.ref_columns.len());
127 let mut exact_reference_lookup = true;
128 let mut exact_local_lookup = true;
129 for (local_column, referenced_column) in fk.local_columns.iter().zip(&fk.ref_columns) {
130 let local_type = local_columns
131 .iter()
132 .find(|definition| definition.name == *local_column)
133 .map(|definition| &definition.ty)
134 .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{local_column}")))?;
135 let referenced_type = referenced_columns
136 .iter()
137 .find(|definition| definition.name == *referenced_column)
138 .map(|definition| &definition.ty)
139 .ok_or_else(|| {
140 SQLError::UnknownColumn(format!("{}.{referenced_column}", fk.ref_table))
141 })?;
142 let comparison_type =
143 crate::type_resolution::foreign_key_operand_type(local_type, referenced_type).map_err(|_| {
144 SQLError::Routine {
145 sqlstate: "42804".into(),
146 message: format!(
147 "foreign key constraint cannot be implemented: key columns \"{local_column}\" and \"{referenced_column}\" are of incompatible types: {} and {}",
148 local_type.sql_name(),
149 referenced_type.sql_name()
150 ),
151 }
152 })?;
153 exact_reference_lookup &= comparison_type == *referenced_type;
154 exact_local_lookup &= comparison_type == *local_type;
155 comparison_types.push(comparison_type);
156 local_types.push(local_type.clone());
157 referenced_types.push(referenced_type.clone());
158 }
159 Ok(ForeignKeyComparison {
160 comparison_types,
161 local_types,
162 referenced_types,
163 exact_reference_lookup,
164 exact_local_lookup,
165 })
166}
167
168pub fn foreign_key_parent_values(
169 fk: &ForeignKey,
170 document: &Document,
171 comparison: &ForeignKeyComparison,
172) -> Result<Vec<Value>, SQLError> {
173 comparison.normalize_referenced(
174 fk.ref_columns
175 .iter()
176 .map(|column| document.get(column).cloned().unwrap_or(Value::Null))
177 .collect(),
178 )
179}
180
181fn normalize_typed_values(
182 values: Vec<Value>,
183 source_types: &[ColumnType],
184 comparison_types: &[ColumnType],
185) -> Result<Vec<Value>, SQLError> {
186 if values.len() != source_types.len() || values.len() != comparison_types.len() {
187 return Err(SQLError::Internal(
188 "FOREIGN KEY value and comparison-type counts diverged after validation".into(),
189 ));
190 }
191 values
192 .into_iter()
193 .zip(source_types)
194 .zip(comparison_types)
195 .map(|((value, source), target)| {
196 crate::assignment::conversion::convert_value_to_column_type_from(
197 value,
198 target,
199 Some(source),
200 )
201 })
202 .collect()
203}
204
205pub fn normalize_foreign_key_values(
206 values: Vec<Value>,
207 comparison_types: &[ColumnType],
208) -> Result<Vec<Value>, SQLError> {
209 if values.len() != comparison_types.len() {
210 return Err(SQLError::Internal(
211 "FOREIGN KEY value and comparison-type counts diverged after validation".into(),
212 ));
213 }
214 values
215 .into_iter()
216 .zip(comparison_types)
217 .map(|(value, ty)| crate::assignment::conversion::convert_value_to_column_type(value, ty))
218 .collect()
219}