1use crate::{
9 ast::{BinaryOp, ColumnDef, TableConstraintSet},
10 binding::snapshot::BindingSnapshot,
11 catalog::index::EnforcedKey,
12 plan::AggregateClassifier,
13 plan::{ConflictPlan, ExpressionPlan, InsertPlan},
14 routines::RoutineResolution,
15 RowSchema, SQLError, SQLParam, ScalarExpr as Expr,
16};
17use std::collections::BTreeSet;
18use uqa_core::Value;
19
20pub trait ConflictCatalog {
21 fn try_describe_table(&self, table: &str) -> Result<Option<Vec<ColumnDef>>, String>;
22 fn enforced_keys(&self, table: &str) -> Result<Vec<EnforcedKey>, String>;
23 fn try_declared_table_constraints(&self, table: &str) -> Result<TableConstraintSet, String>;
24}
25pub trait InferenceBindingScope {
27 fn binding_scope(&self) -> Result<BindingSnapshot, SQLError>;
28}
29#[derive(Clone, Copy)]
30pub struct InferenceContext<'a> {
31 pub catalog: &'a dyn ConflictCatalog,
32 pub aggregates: &'a dyn AggregateClassifier,
33 pub routines: &'a dyn RoutineResolution,
34 pub binding: &'a dyn InferenceBindingScope,
35}
36
37pub fn prepare_inference_predicate<'a>(
39 context: InferenceContext<'_>,
40 statement: &'a InsertPlan,
41 params: &[SQLParam],
42) -> Result<std::borrow::Cow<'a, InsertPlan>, SQLError> {
43 if statement
44 .on_conflict
45 .as_ref()
46 .is_none_or(|conflict| conflict.predicate.is_none() && conflict.expressions.is_empty())
47 {
48 return Ok(std::borrow::Cow::Borrowed(statement));
49 }
50 let columns = context
51 .catalog
52 .try_describe_table(&statement.table)
53 .map_err(|error| SQLError::Internal(error.to_string()))?
54 .ok_or_else(|| SQLError::UnknownTable(statement.table.clone()))?;
55 let schema = RowSchema::with_qualified_types(
56 &statement.target_qualifier,
57 columns.iter().map(|column| column.name.clone()).collect(),
58 columns
59 .iter()
60 .map(|column| Some(column.ty.clone()))
61 .collect(),
62 );
63 let mut statement = statement.clone();
64 if let Some(conflict) = &mut statement.on_conflict {
65 for expression in conflict
66 .expressions
67 .iter_mut()
68 .chain(conflict.predicate.iter_mut().map(Box::as_mut))
69 {
70 prepare_inference_expression(
71 context,
72 expression,
73 &statement.target_qualifier,
74 &schema,
75 &columns,
76 params,
77 )?;
78 }
79 let expressions = std::mem::take(&mut conflict.expressions);
81 for expression in expressions {
82 if let Expr::Column(column) = expression {
83 conflict.conflict_columns.push(column);
84 } else {
85 conflict.expressions.push(expression);
86 }
87 }
88 }
89 Ok(std::borrow::Cow::Owned(statement))
90}
91
92fn prepare_inference_expression(
93 context: InferenceContext<'_>,
94 expression: &mut Expr,
95 qualifier: &str,
96 schema: &RowSchema,
97 columns: &[crate::ast::ColumnDef],
98 params: &[SQLParam],
99) -> Result<(), SQLError> {
100 let mut has_subquery = false;
101 expression.visit(&mut |part| {
102 has_subquery |= matches!(
103 part,
104 Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. }
105 );
106 });
107 if has_subquery {
108 return Err(predicate_error(
109 "0A000",
110 "cannot use subquery in index inference",
111 ));
112 }
113 if crate::semantics::aggregates::contains_aggregate(context.aggregates, expression) {
114 return Err(predicate_error(
115 "42803",
116 "aggregate functions are not allowed in index inference",
117 ));
118 }
119 if crate::semantics::windows::expr_has_window(expression) {
120 return Err(predicate_error(
121 "42P20",
122 "window functions are not allowed in index inference",
123 ));
124 }
125 let mut plan = ExpressionPlan {
126 scalar: expression.clone(),
127 subqueries: Vec::new(),
128 };
129 let binding = context.binding.binding_scope()?;
130 crate::binding::bind_expression_plan_routines_for_storage(
131 context.routines,
132 &mut plan,
133 params,
134 &binding.context(),
135 schema,
136 )?;
137 crate::plan::rewrite_scalar_expression(&mut plan.scalar, &mut |expression| {
138 if let Expr::QualifiedColumn {
139 qualifier: source,
140 column,
141 } = expression
142 {
143 if source == qualifier {
144 *expression = Expr::Column(column.clone());
145 }
146 }
147 });
148 crate::plan::rewrite_scalar_expression(&mut plan.scalar, &mut |expression| {
149 if let Expr::Cast { expr, ty } = expression {
150 if let Expr::Column(name) = expr.as_ref() {
151 if columns.iter().any(|column| {
152 column.name == *name
153 && crate::ast::ColumnType::from_sql_name(ty).ok().as_ref()
154 == Some(&column.ty)
155 }) {
156 *expression = Expr::Column(name.clone());
157 }
158 }
159 }
160 });
161 *expression = plan.scalar;
162 Ok(())
163}
164
165fn predicate_error(sqlstate: &str, message: &str) -> SQLError {
166 SQLError::Routine {
167 sqlstate: sqlstate.into(),
168 message: message.into(),
169 }
170}
171
172pub fn conflict_key_indices(
173 catalog: &dyn ConflictCatalog,
174 table: &str,
175 keys: &[EnforcedKey],
176 conflict: &ConflictPlan,
177) -> Result<Vec<usize>, SQLError> {
178 if let Some(name) = &conflict.constraint {
179 return constraint_target_index(catalog, table, keys, name);
180 }
181 if conflict.conflict_columns.is_empty() && conflict.expressions.is_empty() {
182 return Ok((0..keys.len()).collect());
183 }
184 validate_conflict_columns(catalog, table, &conflict.conflict_columns)?;
185 let target = conflict
186 .conflict_columns
187 .iter()
188 .map(String::as_str)
189 .collect::<BTreeSet<_>>();
190 let indexes = keys
191 .iter()
192 .enumerate()
193 .filter_map(|(index, key)| {
194 (key.columns
195 .iter()
196 .map(String::as_str)
197 .collect::<BTreeSet<_>>()
198 == target
199 && {
200 let expressions = key
201 .keys
202 .iter()
203 .filter_map(|key| match key {
204 crate::ast::IndexKey::Expression(expr) => {
205 Some(ExpressionPlan::lower((**expr).clone()).scalar)
206 }
207 crate::ast::IndexKey::Column(_) => None,
208 })
209 .collect::<Vec<_>>();
210 expressions
211 .iter()
212 .all(|expr| conflict.expressions.contains(expr))
213 && conflict
214 .expressions
215 .iter()
216 .all(|expr| expressions.contains(expr))
217 }
218 && key.predicate.as_deref().is_none_or(|required| {
219 conflict.predicate.as_deref().is_some_and(|given| {
220 implies(given, &ExpressionPlan::lower(required.clone()).scalar)
221 })
222 }))
223 .then_some(index)
224 })
225 .collect::<Vec<_>>();
226 if indexes.is_empty() {
227 return Err(SQLError::Routine {
228 sqlstate: "42P10".into(),
229 message:
230 "there is no unique or exclusion constraint matching the ON CONFLICT specification"
231 .into(),
232 });
233 }
234 Ok(indexes)
235}
236
237fn implies(given: &Expr, required: &Expr) -> bool {
239 if given == required || matches!(required, Expr::Literal(Value::Bool(true))) {
240 return true;
241 }
242 if matches!(given, Expr::Literal(Value::Bool(false) | Value::Null)) {
243 return true;
244 }
245 if let Expr::And(parts) = required {
246 return parts.iter().all(|part| implies(given, part));
247 }
248 if let Expr::Or(parts) = given {
249 return parts.iter().all(|part| implies(part, required));
250 }
251 if let Expr::And(parts) = given {
252 if parts.iter().any(|part| implies(part, required)) {
253 return true;
254 }
255 }
256 if let Expr::Or(parts) = required {
257 return parts.iter().any(|part| implies(given, part));
258 }
259 if let Some((left, given_op, given_value)) = comparison(given) {
260 if let Some((right, required_op, required_value)) = comparison(required) {
261 return left == right
262 && comparison_implies(given_op, given_value, required_op, required_value);
263 }
264 if let Expr::IsNull {
265 expr,
266 negated: true,
267 } = required
268 {
269 return left == expr.as_ref() && !matches!(given_value, Value::Null);
270 }
271 }
272 false
273}
274
275fn comparison(expr: &Expr) -> Option<(&Expr, BinaryOp, &Value)> {
276 let Expr::Binary { op, lhs, rhs } = expr else {
277 return None;
278 };
279 if !matches!(
280 op,
281 BinaryOp::Equal
282 | BinaryOp::NotEqual
283 | BinaryOp::Less
284 | BinaryOp::LessEqual
285 | BinaryOp::Greater
286 | BinaryOp::GreaterEqual
287 ) {
288 return None;
289 }
290 if let Expr::Literal(value) = rhs.as_ref() {
291 return Some((lhs, *op, value));
292 }
293 if let Expr::Literal(value) = lhs.as_ref() {
294 let reversed = match op {
295 BinaryOp::Less => BinaryOp::Greater,
296 BinaryOp::LessEqual => BinaryOp::GreaterEqual,
297 BinaryOp::Greater => BinaryOp::Less,
298 BinaryOp::GreaterEqual => BinaryOp::LessEqual,
299 other => *other,
300 };
301 return Some((rhs, reversed, value));
302 }
303 None
304}
305
306fn comparison_implies(given: BinaryOp, left: &Value, required: BinaryOp, right: &Value) -> bool {
307 use BinaryOp::{Equal, Greater, GreaterEqual, Less, LessEqual, NotEqual};
308 if matches!(left, Value::Null) || matches!(right, Value::Null) {
309 return false;
310 }
311 if std::mem::discriminant(left) != std::mem::discriminant(right) {
312 return false;
313 }
314 let order = left.cmp(right);
315 match (given, required) {
316 (Equal, Equal) | (NotEqual, NotEqual) => order.is_eq(),
317 (Equal, NotEqual) => !order.is_eq(),
318 (Equal | GreaterEqual, Greater) | (GreaterEqual, NotEqual) => order.is_gt(),
319 (Equal | LessEqual, Less) | (LessEqual, NotEqual) => order.is_lt(),
320 (Equal | Greater | GreaterEqual, GreaterEqual) | (Greater, Greater | NotEqual) => {
321 order.is_ge()
322 }
323 (Equal | Less | LessEqual, LessEqual) | (Less, Less | NotEqual) => order.is_le(),
324 _ => false,
325 }
326}
327
328pub fn validate_conflict_target(
329 catalog: &dyn ConflictCatalog,
330 table: &str,
331 conflict: &ConflictPlan,
332) -> Result<(), SQLError> {
333 let keys = catalog
334 .enforced_keys(table)
335 .map_err(|error| SQLError::Internal(format!("conflict target keys: {error}")))?;
336 conflict_key_indices(catalog, table, &keys, conflict).map(|_| ())
337}
338
339fn constraint_target_index(
340 catalog: &dyn ConflictCatalog,
341 table: &str,
342 keys: &[EnforcedKey],
343 name: &str,
344) -> Result<Vec<usize>, SQLError> {
345 if let Some(index) = keys
346 .iter()
347 .position(|key| key.constraint_owned && key.name.as_deref() == Some(name))
348 {
349 return Ok(vec![index]);
350 }
351 let snapshot = catalog
352 .try_declared_table_constraints(table)
353 .map_err(|error| SQLError::Internal(error.to_string()))?;
354 let columns = catalog
355 .try_describe_table(table)
356 .map_err(|error| SQLError::Internal(error.to_string()))?
357 .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
358 let exists = snapshot
359 .checks
360 .iter()
361 .any(|check| check.name.as_deref() == Some(name))
362 || snapshot
363 .foreign_keys
364 .iter()
365 .any(|key| key.name.as_deref() == Some(name))
366 || columns.iter().any(|column| {
367 column.not_null_name.as_deref() == Some(name)
368 || column.check_name.as_deref() == Some(name)
369 || column
370 .references
371 .as_ref()
372 .is_some_and(|reference| reference.name.as_deref() == Some(name))
373 });
374 if exists {
375 return Err(SQLError::Routine {
376 sqlstate: "42809".into(),
377 message: "constraint in ON CONFLICT clause has no associated index".into(),
378 });
379 }
380 Err(SQLError::Routine {
381 sqlstate: "42704".into(),
382 message: format!("constraint \"{name}\" for table \"{table}\" does not exist"),
383 })
384}
385
386fn validate_conflict_columns(
387 catalog: &dyn ConflictCatalog,
388 table: &str,
389 names: &[String],
390) -> Result<(), SQLError> {
391 let columns = catalog
392 .try_describe_table(table)
393 .map_err(|error| SQLError::Internal(error.to_string()))?
394 .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
395 for name in names {
396 if !columns.iter().any(|column| column.name == *name)
397 && !matches!(
398 name.as_str(),
399 "ctid" | "tableoid" | "xmin" | "xmax" | "cmin" | "cmax"
400 )
401 {
402 return Err(SQLError::UnknownColumn(name.clone()));
403 }
404 }
405 Ok(())
406}