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
17pub trait AssignmentColumnCatalog {
18    fn try_describe_table(&self, table: &str)
19        -> Result<Option<Vec<ColumnDef>>, ColumnCatalogError>;
20    fn columns_declared(&self, table: &str) -> Result<bool, ColumnCatalogError>;
21    fn try_column_insert_default_expr(
22        &self,
23        table: &str,
24        column: &str,
25    ) -> Result<Option<Expr>, ColumnCatalogError>;
26}
27fn dml_storage_error(action: &str, error: impl std::fmt::Display) -> SQLError {
28    SQLError::Internal(format!("{action} failed in storage backend: {error}"))
29}
30fn ddl_storage_error(action: &str, error: ColumnCatalogError) -> SQLError {
31    crate::catalog::errors::storage_error(action, error.as_ref())
32}
33/// Coerce a write value to fit the column's declared type.
34pub fn coerce_to_column_type(
35    assignment: &dyn AssignmentContext,
36    catalog: &dyn AssignmentColumnCatalog,
37    table: &str,
38    column: &str,
39    value: Value,
40) -> Result<Value, SQLError> {
41    coerce_to_column_type_from(assignment, catalog, table, column, value, None)
42}
43
44pub fn coerce_to_column_type_from(
45    assignment: &dyn AssignmentContext,
46    catalog: &dyn AssignmentColumnCatalog,
47    table: &str,
48    column: &str,
49    value: Value,
50    source: Option<&ColumnType>,
51) -> Result<Value, SQLError> {
52    let cols = match catalog
53        .try_describe_table(table)
54        .map_err(|err| ddl_storage_error("column type coercion", err))?
55    {
56        Some(c) => c,
57        None => return Ok(value),
58    };
59    let Some(def) = cols.iter().find(|c| c.name == column) else {
60        return Ok(value);
61    };
62    coerce_assignment_value(assignment, value, &def.ty, source)
63}
64
65pub fn validate_mutation_columns<'a>(
66    catalog: &dyn AssignmentColumnCatalog,
67    table: &str,
68    columns: impl IntoIterator<Item = &'a str>,
69    action: &str,
70) -> Result<(), SQLError> {
71    let definitions = catalog
72        .try_describe_table(table)
73        .map_err(|err| dml_storage_error(action, err))?
74        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
75    // Programmatically-created document tables intentionally have no SQL
76    // schema and retain their open-field behavior. SQL CREATE TABLE always
77    // supplies definitions, and those targets must reject misspelled or
78    // repeated mutation columns instead of persisting arbitrary fields.
79    if definitions.is_empty() {
80        let declared = catalog
81            .columns_declared(table)
82            .map_err(|error| dml_storage_error(action, error))?;
83        if !declared {
84            return Ok(());
85        }
86    }
87    let known: BTreeSet<&str> = definitions
88        .iter()
89        .map(|definition| definition.name.as_str())
90        .collect();
91    let mut seen = BTreeSet::new();
92    for column in columns {
93        if !seen.insert(column) {
94            return Err(SQLError::TypeMismatch(format!(
95                "{action}: column `{column}` is specified more than once"
96            )));
97        }
98        if !known.contains(column) {
99            let relation = RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
100            return Err(SQLError::Routine {
101                sqlstate: "42703".into(),
102                message: format!(
103                    "column \"{column}\" of relation \"{}\" does not exist",
104                    relation.name
105                ),
106            });
107        }
108    }
109    Ok(())
110}
111
112pub fn generated_column_kind(
113    catalog: &dyn AssignmentColumnCatalog,
114    table: &str,
115    column: &str,
116) -> Result<Option<GeneratedColumnKind>, SQLError> {
117    Ok(catalog
118        .try_describe_table(table)
119        .map_err(|error| SQLError::Internal(format!("read generated column: {error}")))?
120        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?
121        .into_iter()
122        .find(|definition| definition.name == column)
123        .and_then(|definition| definition.generated.map(|generated| generated.kind)))
124}