Skip to main content

uqa_sql/assignment/
columns.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Declared mutation columns, generated-column assignment rules, and table-value coercion.
8use super::{conversion::coerce_assignment_value, AssignmentContext};
9use crate::{
10    ast::{ColumnDef, ColumnType, Expr, GeneratedColumnKind},
11    SQLError,
12};
13use std::collections::BTreeSet;
14use uqa_core::{RelationIdentity, Value};
15pub type ColumnCatalogError = Box<dyn std::error::Error>;
16
17/// What a written value needs to know of its column.
18#[derive(Debug, Clone, PartialEq)]
19pub struct ColumnShape {
20    pub ty: ColumnType,
21    pub generated: Option<GeneratedColumnKind>,
22    /// The sequence of an identity column, which `DEFAULT` draws its value from.
23    pub identity_sequence: Option<String>,
24}
25
26/// The sequence an identity column draws its values from: `None` for any other column.
27fn identity_sequence(definition: &ColumnDef) -> Option<String> {
28    definition
29        .auto_increment
30        .as_ref()
31        .filter(|provenance| provenance.is_identity())
32        .and_then(|provenance| provenance.sequence.clone())
33}
34
35pub trait AssignmentColumnCatalog {
36    fn try_describe_table(&self, table: &str)
37        -> Result<Option<Vec<ColumnDef>>, ColumnCatalogError>;
38    fn columns_declared(&self, table: &str) -> Result<bool, ColumnCatalogError>;
39    fn try_column_insert_default_expr(
40        &self,
41        table: &str,
42        column: &str,
43    ) -> Result<Option<Expr>, ColumnCatalogError>;
44    /// The sequence an identity DEFAULT draws from. An identity declaration with no sequence owned by it reports `PostgreSQL`'s error.
45    fn try_identity_column_sequence(
46        &self,
47        table: &str,
48        column: &str,
49    ) -> Result<Option<String>, SQLError> {
50        Ok(self
51            .try_column_shape(table, column)
52            .map_err(|error| SQLError::Internal(format!("read identity column: {error}")))?
53            .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?
54            .and_then(|shape| shape.identity_sequence))
55    }
56    /// The type and generated kind of one column: `None` for an unknown table, and an inner `None` for a column the table does not declare. Every written value asks for it, so a catalog answers from the column alone. The default describes the whole table, which copies every column and resolves every default expression.
57    fn try_column_shape(
58        &self,
59        table: &str,
60        column: &str,
61    ) -> Result<Option<Option<ColumnShape>>, ColumnCatalogError> {
62        Ok(self.try_describe_table(table)?.map(|columns| {
63            columns
64                .into_iter()
65                .find(|definition| definition.name == column)
66                .map(|definition| ColumnShape {
67                    identity_sequence: identity_sequence(&definition),
68                    ty: definition.ty,
69                    generated: definition.generated.map(|generated| generated.kind),
70                })
71        }))
72    }
73}
74fn dml_storage_error(action: &str, error: impl std::fmt::Display) -> SQLError {
75    SQLError::Internal(format!("{action} failed in storage backend: {error}"))
76}
77fn ddl_storage_error(action: &str, error: ColumnCatalogError) -> SQLError {
78    crate::catalog::errors::storage_error(action, error.as_ref())
79}
80/// Coerce a write value to fit the column's declared type.
81pub fn coerce_to_column_type(
82    assignment: &dyn AssignmentContext,
83    catalog: &dyn AssignmentColumnCatalog,
84    table: &str,
85    column: &str,
86    value: Value,
87) -> Result<Value, SQLError> {
88    coerce_to_column_type_from(assignment, catalog, table, column, value, None)
89}
90
91pub fn coerce_to_column_type_from(
92    assignment: &dyn AssignmentContext,
93    catalog: &dyn AssignmentColumnCatalog,
94    table: &str,
95    column: &str,
96    value: Value,
97    source: Option<&ColumnType>,
98) -> Result<Value, SQLError> {
99    let Some(Some(shape)) = catalog
100        .try_column_shape(table, column)
101        .map_err(|err| ddl_storage_error("column type coercion", err))?
102    else {
103        return Ok(value);
104    };
105    coerce_assignment_value(assignment, value, &shape.ty, source)
106}
107
108pub fn validate_mutation_columns<'a>(
109    catalog: &dyn AssignmentColumnCatalog,
110    table: &str,
111    columns: impl IntoIterator<Item = &'a str>,
112    action: &str,
113) -> Result<(), SQLError> {
114    let definitions = catalog
115        .try_describe_table(table)
116        .map_err(|err| dml_storage_error(action, err))?
117        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
118    // Programmatically-created document tables intentionally have no SQL
119    // schema and retain their open-field behavior. SQL CREATE TABLE always
120    // supplies definitions, and those targets must reject misspelled or
121    // repeated mutation columns instead of persisting arbitrary fields.
122    if definitions.is_empty() {
123        let declared = catalog
124            .columns_declared(table)
125            .map_err(|error| dml_storage_error(action, error))?;
126        if !declared {
127            return Ok(());
128        }
129    }
130    let known: BTreeSet<&str> = definitions
131        .iter()
132        .map(|definition| definition.name.as_str())
133        .collect();
134    let mut seen = BTreeSet::new();
135    for column in columns {
136        if !seen.insert(column) {
137            return Err(SQLError::TypeMismatch(format!(
138                "{action}: column `{column}` is specified more than once"
139            )));
140        }
141        if !known.contains(column) {
142            let relation = RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
143            return Err(SQLError::Routine {
144                sqlstate: "42703".into(),
145                message: format!(
146                    "column \"{column}\" of relation \"{}\" does not exist",
147                    relation.name
148                ),
149            });
150        }
151    }
152    Ok(())
153}
154
155/// The sequence `DEFAULT` draws an identity column's value from, or `None` for a column that is not an identity column.
156pub fn identity_column_sequence(
157    catalog: &dyn AssignmentColumnCatalog,
158    table: &str,
159    column: &str,
160) -> Result<Option<String>, SQLError> {
161    catalog.try_identity_column_sequence(table, column)
162}
163
164pub fn generated_column_kind(
165    catalog: &dyn AssignmentColumnCatalog,
166    table: &str,
167    column: &str,
168) -> Result<Option<GeneratedColumnKind>, SQLError> {
169    Ok(catalog
170        .try_column_shape(table, column)
171        .map_err(|error| SQLError::Internal(format!("read generated column: {error}")))?
172        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?
173        .and_then(|shape| shape.generated))
174}
175
176/// Validate partial targets without rejecting independent writes into the same column.
177pub fn validate_mutation_targets<'a, E: 'a>(
178    catalog: &dyn AssignmentColumnCatalog,
179    table: &str,
180    targets: impl IntoIterator<Item = &'a crate::ast::AssignmentTarget<E>>,
181    action: &str,
182    insert: bool,
183) -> Result<(), SQLError> {
184    let targets = targets.into_iter().collect::<Vec<_>>();
185    super::targets::validate_repeated_targets(targets.iter().copied(), insert)?;
186    let mut seen = BTreeSet::new();
187    let names = targets
188        .iter()
189        .map(|target| target.column.as_str())
190        .filter(|name| seen.insert(*name));
191    validate_mutation_columns(catalog, table, names, action)
192}