Skip to main content

uqa_sql/schema/
composites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The attribute checks of `DefineCompositeType` in `PostgreSQL`'s order: `MergeAttributes` rejects too many attributes and then a repeated name, `BuildDescForRelation` resolves each attribute's type, requires `USAGE` on it, derives its collation and rejects `SETOF`, and `CheckAttributeNamesTypes` then rejects pseudo-types.
8
9use crate::ast::{ColumnType, CompositeAttributeDefinition};
10use crate::catalog::composite_type::StoredCompositeAttribute;
11use crate::type_resolution::{resolve_declared_column_type, FunctionTypeResolver};
12use crate::SQLError;
13
14pub mod type_changes;
15
16/// `renameatt_internal` checks the source before the destination; standalone composites have no system attributes.
17pub fn validate_renamed_attribute(
18    definition: &crate::catalog::composite_type::StoredComposite,
19    from: &str,
20    to: &str,
21) -> Result<(), SQLError> {
22    super::columns::renamed_column_position(
23        &definition.identity.name,
24        definition
25            .live_attributes()
26            .map(|attribute| attribute.name.as_str()),
27        from,
28        to,
29        false,
30    )
31    .map(|_| ())
32}
33
34/// Resolve a live composite attribute before dependency traversal. Composite relations have no system columns.
35pub fn validate_removed_attribute(
36    definition: &crate::catalog::composite_type::StoredComposite,
37    removal: &crate::ast::CompositeAttributeRemoval,
38) -> Result<bool, SQLError> {
39    if definition
40        .live_attributes()
41        .any(|attribute| attribute.name == removal.name)
42    {
43        return Ok(true);
44    }
45    if removal.if_exists {
46        Ok(false)
47    } else {
48        Err(super::columns::undefined_relation_column(
49            &definition.identity.name,
50            &removal.name,
51        ))
52    }
53}
54
55/// `ATExecAddColumn` for a composite relation, after its owner and kind have been checked.
56pub fn prepare_added_attribute(
57    types: &dyn FunctionTypeResolver,
58    composites: &dyn crate::expr::composites::CompositeTypeCatalog,
59    definition: &crate::catalog::composite_type::StoredComposite,
60    addition: &crate::ast::CompositeAttributeAddition,
61) -> Result<StoredCompositeAttribute, SQLError> {
62    let attribute = &addition.attribute;
63    if definition
64        .live_attributes()
65        .any(|current| current.name == attribute.name)
66    {
67        return Err(SQLError::Routine {
68            sqlstate: "42701".into(),
69            message: format!(
70                "column \"{}\" of relation \"{}\" already exists",
71                attribute.name, definition.identity.name
72            ),
73        });
74    }
75    super::table_creation::column_declarations::check_serial_array(&addition.declaration)?;
76    let number = definition.next_attribute_number();
77    if usize::try_from(number).map_or(true, |number| number > MAX_ATTRIBUTES) {
78        return Err(SQLError::Routine {
79            sqlstate: "54011".into(),
80            message: format!("tables can have at most {MAX_ATTRIBUTES} columns"),
81        });
82    }
83    let declared = match &attribute.ty {
84        ColumnType::Named(name) if addition.declaration.serial => {
85            crate::compiler::compile_retained_type_declaration(name)?
86        }
87        ColumnType::Named(name) => crate::compiler::compile_retained_type_reference(name)?,
88        other => other.clone(),
89    };
90    prepare_attribute(
91        types, composites, definition, attribute, &declared, number, true,
92    )
93}
94
95fn prepare_attribute(
96    types: &dyn FunctionTypeResolver,
97    composites: &dyn crate::expr::composites::CompositeTypeCatalog,
98    definition: &crate::catalog::composite_type::StoredComposite,
99    attribute: &CompositeAttributeDefinition,
100    declared: &ColumnType,
101    number: i16,
102    check_setof: bool,
103) -> Result<StoredCompositeAttribute, SQLError> {
104    let ty = resolve_declared_column_type(types, declared)?;
105    types.require_type_usage(&ty)?;
106    let collation = attribute_collation(&ty, attribute.collation.as_deref())?;
107    if check_setof && attribute.setof {
108        return Err(SQLError::Routine {
109            sqlstate: "42P16".into(),
110            message: format!("column \"{}\" cannot be declared SETOF", attribute.name),
111        });
112    }
113    super::columns::validate_postgres_relation_column_type(&attribute.name, &ty)?;
114    if crate::expr::composites::type_contains_composite(&ty, definition.oid, composites)? {
115        return Err(SQLError::Routine {
116            sqlstate: "42P16".into(),
117            message: format!(
118                "composite type {} cannot be made a member of itself",
119                definition.identity.name
120            ),
121        });
122    }
123    Ok(StoredCompositeAttribute {
124        name: attribute.name.clone(),
125        ty,
126        collation,
127        number,
128        dropped: false,
129    })
130}
131
132/// `MaxHeapAttributeNumber`.
133pub const MAX_ATTRIBUTES: usize = 1600;
134
135/// Whether values of a type carry a collation.
136#[must_use]
137pub fn type_is_collatable(ty: &ColumnType) -> bool {
138    match ty {
139        ColumnType::Text
140        | ColumnType::Varchar(_)
141        | ColumnType::Bpchar
142        | ColumnType::Character(_)
143        | ColumnType::Name
144        | ColumnType::PgNodeTree => true,
145        ColumnType::Domain { base, .. } | ColumnType::Array(base) => type_is_collatable(base),
146        _ => false,
147    }
148}
149
150pub use crate::catalog::collations::builtin_collation_oid;
151
152/// `GetColumnDefCollation`: an explicit collation must exist and the type must be collatable. The canonical collation name is kept.
153fn attribute_collation(
154    ty: &ColumnType,
155    collation: Option<&str>,
156) -> Result<Option<String>, SQLError> {
157    let Some(collation) = collation else {
158        return Ok(None);
159    };
160    let names = crate::compiler::parse_regobject_name(collation)
161        .ok_or_else(|| SQLError::Internal(format!("malformed collation name {collation}")))?;
162    let (schema, name) = match names.as_slice() {
163        [name] => (None, name.as_str()),
164        [schema, name] => (Some(schema.as_str()), name.as_str()),
165        _ => {
166            return Err(SQLError::Internal(format!(
167                "malformed collation name {collation}"
168            )))
169        }
170    };
171    if schema.is_some_and(|schema| schema != "pg_catalog") || builtin_collation_oid(name).is_none()
172    {
173        return Err(SQLError::Routine {
174            sqlstate: "42704".into(),
175            message: format!(
176                "collation \"{}\" for encoding \"UTF8\" does not exist",
177                names.join(".")
178            ),
179        });
180    }
181    if !type_is_collatable(ty) {
182        return Err(SQLError::Routine {
183            sqlstate: "42804".into(),
184            message: format!("collations are not supported by type {}", ty.display_name()),
185        });
186    }
187    Ok(Some(name.to_owned()))
188}
189
190/// Check and bind the declared attributes of a new composite type, numbering them from one.
191pub fn prepare_composite_attributes(
192    types: &dyn FunctionTypeResolver,
193    attributes: &[CompositeAttributeDefinition],
194) -> Result<Vec<StoredCompositeAttribute>, SQLError> {
195    if attributes.len() > MAX_ATTRIBUTES {
196        return Err(SQLError::Routine {
197            sqlstate: "54011".into(),
198            message: format!("tables can have at most {MAX_ATTRIBUTES} columns"),
199        });
200    }
201    for (position, attribute) in attributes.iter().enumerate() {
202        if attributes[position + 1..]
203            .iter()
204            .any(|later| later.name == attribute.name)
205        {
206            return Err(SQLError::Routine {
207                sqlstate: "42701".into(),
208                message: format!("column \"{}\" specified more than once", attribute.name),
209            });
210        }
211    }
212    let mut prepared = Vec::with_capacity(attributes.len());
213    for (position, attribute) in attributes.iter().enumerate() {
214        let ty = resolve_declared_column_type(types, &attribute.ty)?;
215        types.require_type_usage(&ty)?;
216        let collation = attribute_collation(&ty, attribute.collation.as_deref())?;
217        if attribute.setof {
218            return Err(SQLError::Routine {
219                sqlstate: "42P16".into(),
220                message: format!("column \"{}\" cannot be declared SETOF", attribute.name),
221            });
222        }
223        prepared.push(StoredCompositeAttribute {
224            name: attribute.name.clone(),
225            ty,
226            collation,
227            number: i16::try_from(position + 1)
228                .map_err(|_| SQLError::Internal("composite attribute number overflow".into()))?,
229            dropped: false,
230        });
231    }
232    for attribute in &prepared {
233        super::columns::validate_postgres_relation_column_type(&attribute.name, &attribute.ty)?;
234    }
235    Ok(prepared)
236}
237
238#[cfg(test)]
239mod tests;