Skip to main content

uqa_sql/semantics/
conflict.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Unique-index inference, target validation, and SQL predicate implication.
8use 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}
25/// Capture the stored-expression catalog scope only when an inference expression requires binding.
26pub 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
37/// Analyze the inference clause in the INSERT target's scope before uniqueness arbitration, including commands that produce no input rows.
38pub 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        // A rewritten view expression can resolve to a simple base-table attribute.
80        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
237/// Prove that every row for which `given` is true also makes `required` true. SQL NULL is never treated as false in a proof that would admit an invalid arbiter.
238fn 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}