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