Skip to main content

uqa_sql/semantics/
mutation_patch.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Constraint dependency analysis for UPDATE's storage patch path.
8use crate::{
9    catalog::errors::dml_storage_error,
10    semantics::{
11        constraint_catalog::ConstraintCatalog,
12        referential::{referrers_to_for_actions, ReferentialCatalog},
13    },
14    SQLError,
15};
16use std::collections::{BTreeMap, BTreeSet};
17use uqa_core::Value;
18pub fn can_patch_update_without_full_row(
19    catalog: &dyn ConstraintCatalog,
20    table: &str,
21    referrers: &dyn ReferentialCatalog,
22    updates: &BTreeMap<String, Value>,
23) -> Result<bool, SQLError> {
24    if catalog
25        .try_check_constraint_definitions(table)
26        .map_err(|err| dml_storage_error("UPDATE", err))?
27        .iter()
28        .any(|constraint| constraint.enforced)
29    {
30        return Ok(false);
31    }
32    let update_keys: BTreeSet<&str> = updates.keys().map(String::as_str).collect();
33    if catalog
34        .try_describe_table(table)
35        .map_err(|err| dml_storage_error("UPDATE", err))?
36        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?
37        .iter()
38        .any(|col| {
39            col.not_null
40                && col.auto_increment.is_none()
41                && matches!(updates.get(&col.name), Some(Value::Null))
42        })
43    {
44        return Ok(false);
45    }
46    if catalog
47        .enforced_keys(table)
48        .map_err(|err| dml_storage_error("UPDATE", err))?
49        .iter()
50        .any(|constraint| {
51            constraint.predicate.as_deref().is_some_and(|predicate| {
52                update_keys.iter().any(|column| {
53                    crate::schema::dependencies::schema_expr_references_column(predicate, column)
54                })
55            }) || constraint.keys.iter().any(|key| {
56                update_keys.iter().any(|column| {
57                    crate::schema::dependencies::schema_expr_references_column(
58                        &key.expression(),
59                        column,
60                    )
61                })
62            })
63        })
64    {
65        return Ok(false);
66    }
67    if catalog
68        .try_foreign_keys(table)
69        .map_err(|err| dml_storage_error("UPDATE", err))?
70        .iter()
71        .filter(|fk| fk.enforced)
72        .any(|fk| {
73            fk.local_columns
74                .iter()
75                .any(|column| update_keys.contains(column.as_str()))
76        })
77    {
78        return Ok(false);
79    }
80    if referrers_to_for_actions(referrers, table)?
81        .iter()
82        .any(|(_, fk)| {
83            fk.ref_columns
84                .iter()
85                .any(|column| update_keys.contains(column.as_str()))
86        })
87    {
88        return Ok(false);
89    }
90    Ok(true)
91}
92
93pub fn point_lookup_field_is_unique(
94    catalog: &dyn ConstraintCatalog,
95    table: &str,
96    lookup_field: &str,
97) -> Result<bool, SQLError> {
98    Ok(catalog
99        .enforced_keys(table)
100        .map_err(|err| dml_storage_error("UPDATE", err))?
101        .iter()
102        .any(|constraint| {
103            constraint.predicate.is_none()
104                && constraint.keys.len() == 1
105                && constraint.keys[0].column() == Some(lookup_field)
106        }))
107}