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
14/// `ATExecAddColumn` for a composite relation, after its owner and kind have been checked.
15pub fn prepare_added_attribute(
16    types: &dyn FunctionTypeResolver,
17    composites: &dyn crate::expr::composites::CompositeTypeCatalog,
18    definition: &crate::catalog::composite_type::StoredComposite,
19    addition: &crate::ast::CompositeAttributeAddition,
20) -> Result<StoredCompositeAttribute, SQLError> {
21    let attribute = &addition.attribute;
22    if definition
23        .live_attributes()
24        .any(|current| current.name == attribute.name)
25    {
26        return Err(SQLError::Routine {
27            sqlstate: "42701".into(),
28            message: format!(
29                "column \"{}\" of relation \"{}\" already exists",
30                attribute.name, definition.identity.name
31            ),
32        });
33    }
34    super::table_creation::column_declarations::check_serial_array(&addition.declaration)?;
35    let number = definition.next_attribute_number();
36    if usize::try_from(number).map_or(true, |number| number > MAX_ATTRIBUTES) {
37        return Err(SQLError::Routine {
38            sqlstate: "54011".into(),
39            message: format!("tables can have at most {MAX_ATTRIBUTES} columns"),
40        });
41    }
42    let declared = match &attribute.ty {
43        ColumnType::Named(name) if addition.declaration.serial => {
44            crate::compiler::compile_retained_type_declaration(name)?
45        }
46        ColumnType::Named(name) => crate::compiler::compile_retained_type_reference(name)?,
47        other => other.clone(),
48    };
49    let ty = resolve_declared_column_type(types, &declared)?;
50    types.require_type_usage(&ty)?;
51    let collation = attribute_collation(&ty, attribute.collation.as_deref())?;
52    if attribute.setof {
53        return Err(SQLError::Routine {
54            sqlstate: "42P16".into(),
55            message: format!("column \"{}\" cannot be declared SETOF", attribute.name),
56        });
57    }
58    super::columns::validate_postgres_relation_column_type(&attribute.name, &ty)?;
59    if crate::expr::composites::type_contains_composite(&ty, definition.oid, composites)? {
60        return Err(SQLError::Routine {
61            sqlstate: "42P16".into(),
62            message: format!(
63                "composite type {} cannot be made a member of itself",
64                definition.identity.name
65            ),
66        });
67    }
68    Ok(StoredCompositeAttribute {
69        name: attribute.name.clone(),
70        ty,
71        collation,
72        number,
73        dropped: false,
74    })
75}
76
77/// `MaxHeapAttributeNumber`.
78pub const MAX_ATTRIBUTES: usize = 1600;
79
80/// The collations every `PostgreSQL` 18 database has, independently of the operating system and ICU: their names and OIDs.
81const BUILTIN_COLLATIONS: [(&str, i64); 7] = [
82    ("default", 100),
83    ("pg_c_utf8", 811),
84    ("C", 950),
85    ("POSIX", 951),
86    ("ucs_basic", 962),
87    ("unicode", 963),
88    ("pg_unicode_fast", 6411),
89];
90
91/// Whether values of a type carry a collation.
92#[must_use]
93pub fn type_is_collatable(ty: &ColumnType) -> bool {
94    match ty {
95        ColumnType::Text
96        | ColumnType::Varchar(_)
97        | ColumnType::Bpchar
98        | ColumnType::Character(_)
99        | ColumnType::Name
100        | ColumnType::PgNodeTree => true,
101        ColumnType::Domain { base, .. } | ColumnType::Array(base) => type_is_collatable(base),
102        _ => false,
103    }
104}
105
106/// The `pg_collation` OID of a built-in collation name.
107#[must_use]
108pub fn builtin_collation_oid(name: &str) -> Option<i64> {
109    BUILTIN_COLLATIONS
110        .iter()
111        .find(|(collation, _)| *collation == name)
112        .map(|(_, oid)| *oid)
113}
114
115/// `GetColumnDefCollation`: an explicit collation must exist and the type must be collatable. The canonical collation name is kept.
116fn attribute_collation(
117    ty: &ColumnType,
118    collation: Option<&str>,
119) -> Result<Option<String>, SQLError> {
120    let Some(collation) = collation else {
121        return Ok(None);
122    };
123    let names = crate::compiler::parse_regobject_name(collation)
124        .ok_or_else(|| SQLError::Internal(format!("malformed collation name {collation}")))?;
125    let (schema, name) = match names.as_slice() {
126        [name] => (None, name.as_str()),
127        [schema, name] => (Some(schema.as_str()), name.as_str()),
128        _ => {
129            return Err(SQLError::Internal(format!(
130                "malformed collation name {collation}"
131            )))
132        }
133    };
134    if schema.is_some_and(|schema| schema != "pg_catalog") || builtin_collation_oid(name).is_none()
135    {
136        return Err(SQLError::Routine {
137            sqlstate: "42704".into(),
138            message: format!(
139                "collation \"{}\" for encoding \"UTF8\" does not exist",
140                names.join(".")
141            ),
142        });
143    }
144    if !type_is_collatable(ty) {
145        return Err(SQLError::Routine {
146            sqlstate: "42804".into(),
147            message: format!("collations are not supported by type {}", ty.display_name()),
148        });
149    }
150    Ok(Some(name.to_owned()))
151}
152
153/// Check and bind the declared attributes of a new composite type, numbering them from one.
154pub fn prepare_composite_attributes(
155    types: &dyn FunctionTypeResolver,
156    attributes: &[CompositeAttributeDefinition],
157) -> Result<Vec<StoredCompositeAttribute>, SQLError> {
158    if attributes.len() > MAX_ATTRIBUTES {
159        return Err(SQLError::Routine {
160            sqlstate: "54011".into(),
161            message: format!("tables can have at most {MAX_ATTRIBUTES} columns"),
162        });
163    }
164    for (position, attribute) in attributes.iter().enumerate() {
165        if attributes[position + 1..]
166            .iter()
167            .any(|later| later.name == attribute.name)
168        {
169            return Err(SQLError::Routine {
170                sqlstate: "42701".into(),
171                message: format!("column \"{}\" specified more than once", attribute.name),
172            });
173        }
174    }
175    let mut prepared = Vec::with_capacity(attributes.len());
176    for (position, attribute) in attributes.iter().enumerate() {
177        let ty = resolve_declared_column_type(types, &attribute.ty)?;
178        types.require_type_usage(&ty)?;
179        let collation = attribute_collation(&ty, attribute.collation.as_deref())?;
180        if attribute.setof {
181            return Err(SQLError::Routine {
182                sqlstate: "42P16".into(),
183                message: format!("column \"{}\" cannot be declared SETOF", attribute.name),
184            });
185        }
186        prepared.push(StoredCompositeAttribute {
187            name: attribute.name.clone(),
188            ty,
189            collation,
190            number: i16::try_from(position + 1)
191                .map_err(|_| SQLError::Internal("composite attribute number overflow".into()))?,
192            dropped: false,
193        });
194    }
195    for attribute in &prepared {
196        super::columns::validate_postgres_relation_column_type(&attribute.name, &attribute.ty)?;
197    }
198    Ok(prepared)
199}
200
201#[cfg(test)]
202mod tests;