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
23/// `column "x" of relation "t" does not exist`, which `ALTER TABLE` reports for a column the relation lacks.
24pub fn undefined_relation_column(table: &str, column: &str) -> SQLError {
25    match uqa_core::RelationIdentity::from_legacy_name(table) {
26        Ok(relation) => SQLError::Routine {
27            sqlstate: "42703".into(),
28            message: format!(
29                "column \"{column}\" of relation \"{}\" does not exist",
30                relation.name
31            ),
32        },
33        Err(error) => SQLError::Internal(format!("resolve ALTER TABLE target `{table}`: {error}")),
34    }
35}
36
37pub fn validate_postgres_relation_column_type(name: &str, ty: &ColumnType) -> Result<(), SQLError> {
38    let pseudo_type = match ty {
39        ColumnType::Void | ColumnType::AnyArray | ColumnType::Record => Some(ty.sql_name()),
40        ColumnType::Array(element)
41            if matches!(
42                element.as_ref(),
43                ColumnType::Void | ColumnType::AnyArray | ColumnType::Record
44            ) =>
45        {
46            Some(ty.sql_name())
47        }
48        _ => None,
49    };
50    if let Some(pseudo_type) = pseudo_type {
51        return Err(SQLError::Routine {
52            sqlstate: "42P16".into(),
53            message: format!("column \"{name}\" has pseudo-type {pseudo_type}"),
54        });
55    }
56    Ok(())
57}
58
59/// Append a declared column after checking its name against the existing schema.
60pub fn append_registered_column(
61    table: &str,
62    columns: &mut Vec<crate::ast::ColumnDef>,
63    column: crate::ast::ColumnDef,
64) -> Result<(), String> {
65    if columns.iter().any(|existing| existing.name == column.name) {
66        return Err(format!(
67            "column `{}` already exists on table `{table}`",
68            column.name
69        ));
70    }
71    columns.push(column);
72    Ok(())
73}
74
75/// Check a DROP target before dependency traversal. Missing `IF EXISTS` targets return false so execution can publish its notice without changing the relation.
76pub fn validate_drop_column(
77    columns: &[crate::ast::ColumnDef],
78    table: &str,
79    column: &str,
80    if_exists: bool,
81) -> Result<bool, SQLError> {
82    if POSTGRES_SYSTEM_COLUMNS.contains(&column) {
83        return Err(SQLError::Routine {
84            sqlstate: "0A000".into(),
85            message: format!("cannot drop system column \"{column}\""),
86        });
87    }
88    if columns.iter().any(|definition| definition.name == column) {
89        return Ok(true);
90    }
91    if if_exists {
92        Ok(false)
93    } else {
94        Err(undefined_relation_column(table, column))
95    }
96}
97
98/// The notice for a missing column, using the unqualified name of the already bound relation.
99pub fn missing_drop_column_notice(table: &str, column: &str) -> crate::SQLNotice {
100    crate::SQLNotice::notice(format!(
101        "column \"{column}\" of relation \"{table}\" does not exist, skipping"
102    ))
103}
104
105/// The column an `ALTER TABLE ... ALTER COLUMN` action names, as `get_attnum` finds it for `ATExecColumnDefault` and its siblings.
106pub fn altered_column<'a>(
107    table: &str,
108    columns: &'a [crate::ast::ColumnDef],
109    column: &str,
110) -> Result<&'a crate::ast::ColumnDef, SQLError> {
111    columns
112        .iter()
113        .find(|definition| definition.name == column)
114        .ok_or_else(|| missing_altered_column(table, column))
115}
116
117/// The error for an `ALTER COLUMN` target that is not a column of the relation: every relation has the system columns, which cannot be altered, and any other name does not exist.
118pub fn missing_altered_column(table: &str, column: &str) -> SQLError {
119    if POSTGRES_SYSTEM_COLUMNS.contains(&column) {
120        return SQLError::Routine {
121            sqlstate: "0A000".into(),
122            message: format!("cannot alter system column \"{column}\""),
123        };
124    }
125    undefined_relation_column(table, column)
126}
127
128/// `ATExecColumnDefault`'s checks of the column whose default `SET DEFAULT` (`setting`) or `DROP DEFAULT` changes: an identity column takes its values from its sequence and a generated column from its expression.
129pub fn validate_default_change(
130    table: &str,
131    column: &crate::ast::ColumnDef,
132    setting: bool,
133) -> Result<(), SQLError> {
134    let relation = uqa_core::RelationIdentity::from_legacy_name(table).map_err(|error| {
135        SQLError::Internal(format!("resolve ALTER TABLE target `{table}`: {error}"))
136    })?;
137    if column
138        .auto_increment
139        .as_ref()
140        .is_some_and(crate::ast::AutoIncrement::is_identity)
141    {
142        return Err(SQLError::Diagnostic {
143            sqlstate: "42601".into(),
144            message: format!(
145                "column \"{}\" of relation \"{}\" is an identity column",
146                column.name, relation.name
147            ),
148            detail: None,
149            hint: (!setting)
150                .then(|| "Use ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY instead.".into()),
151        });
152    }
153    if let Some(generated) = &column.generated {
154        let hint = if setting {
155            Some("Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.")
156        } else if generated.kind == crate::ast::GeneratedColumnKind::Stored {
157            Some("Use ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION instead.")
158        } else {
159            None
160        };
161        return Err(SQLError::Diagnostic {
162            sqlstate: "42601".into(),
163            message: format!(
164                "column \"{}\" of relation \"{}\" is a generated column",
165                column.name, relation.name
166            ),
167            detail: None,
168            hint: hint.map(Into::into),
169        });
170    }
171    Ok(())
172}
173
174pub mod addition;
175
176pub mod publication;
177
178pub mod alteration;
179
180pub mod removal_metadata;
181
182pub mod type_target;
183
184pub mod type_transform;