Skip to main content

uqa_sql/schema/
columns.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` column-name and stored-type declaration rules.
8
9use crate::{ColumnType, SQLError};
10
11pub const POSTGRES_SYSTEM_COLUMNS: [&str; 6] = ["tableoid", "xmin", "cmin", "xmax", "cmax", "ctid"];
12
13pub fn validate_postgres_column_name(name: &str) -> Result<(), SQLError> {
14    if POSTGRES_SYSTEM_COLUMNS.contains(&name) {
15        return Err(SQLError::Routine {
16            sqlstate: "42701".into(),
17            message: format!("column name \"{name}\" conflicts with a system column name"),
18        });
19    }
20    Ok(())
21}
22
23pub fn validate_postgres_relation_column_type(name: &str, ty: &ColumnType) -> Result<(), SQLError> {
24    let pseudo_type = match ty {
25        ColumnType::Void | ColumnType::AnyArray | ColumnType::Record => Some(ty.sql_name()),
26        ColumnType::Array(element)
27            if matches!(
28                element.as_ref(),
29                ColumnType::Void | ColumnType::AnyArray | ColumnType::Record
30            ) =>
31        {
32            Some(ty.sql_name())
33        }
34        _ => None,
35    };
36    if let Some(pseudo_type) = pseudo_type {
37        return Err(SQLError::Routine {
38            sqlstate: "42P16".into(),
39            message: format!("column \"{name}\" has pseudo-type {pseudo_type}"),
40        });
41    }
42    Ok(())
43}
44
45/// Append a declared column after checking its name against the existing schema.
46pub fn append_registered_column(
47    table: &str,
48    columns: &mut Vec<crate::ast::ColumnDef>,
49    column: crate::ast::ColumnDef,
50) -> Result<(), String> {
51    if columns.iter().any(|existing| existing.name == column.name) {
52        return Err(format!(
53            "column `{}` already exists on table `{table}`",
54            column.name
55        ));
56    }
57    columns.push(column);
58    Ok(())
59}
60
61/// Reject `SET DEFAULT`, when `setting`, or `DROP DEFAULT` on an identity or generated column, as `PostgreSQL`'s `ATExecColumnDefault` does: its hint names the command that changes such a column.
62pub fn reject_default_change(
63    catalog: &dyn crate::assignment::columns::AssignmentColumnCatalog,
64    table: &str,
65    column: &str,
66    setting: bool,
67) -> Result<(), SQLError> {
68    let Some(shape) = catalog
69        .try_column_shape(table, column)
70        .map_err(|error| SQLError::Internal(format!("read column `{column}`: {error}")))?
71        .flatten()
72    else {
73        return Ok(());
74    };
75    let relation = uqa_core::RelationIdentity::from_legacy_name(table)
76        .map_err(|error| SQLError::Internal(format!("resolve ALTER TABLE target: {error}")))?
77        .name;
78    let (kind, hint) = if shape.identity_sequence.is_some() {
79        (
80            "an identity column",
81            (!setting).then_some("ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY"),
82        )
83    } else if let Some(generated) = shape.generated {
84        (
85            "a generated column",
86            if setting {
87                Some("ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION")
88            } else {
89                (generated == crate::ast::GeneratedColumnKind::Stored)
90                    .then_some("ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION")
91            },
92        )
93    } else {
94        return Ok(());
95    };
96    Err(SQLError::Diagnostic {
97        sqlstate: "42601".into(),
98        message: format!("column \"{column}\" of relation \"{relation}\" is {kind}"),
99        detail: None,
100        hint: hint.map(|command| format!("Use {command} instead.")),
101    })
102}
103
104pub mod addition;
105
106pub mod publication;
107
108pub mod alteration;
109
110pub mod removal;
111
112pub mod removal_metadata;