Skip to main content

uqa_sql/schema/
inheritance.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! CREATE TABLE inheritance and partition row-type preparation.
8
9use crate::semantics::partition::{
10    validate_hash_partition_spec, validate_new_partition_bound, PartitionContext,
11};
12use crate::{
13    ast::{CreateTable, TableCheck, TableConstraintSet},
14    SQLError,
15};
16/// Parent lookup and constraint declarations used while assembling a new row type.
17pub trait InheritanceCatalog {
18    fn resolve_parent(&self, name: &str) -> Result<String, SQLError>;
19    fn declared_constraints(&self, table: &str) -> Result<TableConstraintSet, String>;
20    fn check_definitions(&self, table: &str) -> Result<Vec<TableCheck>, String>;
21}
22pub struct InheritanceContext<'a> {
23    pub catalog: &'a dyn InheritanceCatalog,
24    pub partitions: PartitionContext<'a>,
25    pub roles: &'a dyn crate::expr::EngineHook,
26}
27use std::collections::BTreeSet;
28
29#[expect(
30    clippy::too_many_lines,
31    reason = "preserves DDL dependency and action order"
32)]
33pub fn prepare_create_table_hierarchy(
34    context: &InheritanceContext<'_>,
35    table: &mut CreateTable,
36) -> Result<(), SQLError> {
37    table.hierarchy.local_columns = table
38        .columns
39        .iter()
40        .map(|column| column.name.clone())
41        .collect();
42    if table.hierarchy.parents.is_empty() {
43        if table.hierarchy.partition_bound.is_some() {
44            return Err(SQLError::Internal(
45                "partition bound has no parent relation".into(),
46            ));
47        }
48        validate_partition_keys(context, table)?;
49        return Ok(());
50    }
51    let is_partition = table.hierarchy.partition_bound.is_some();
52    if is_partition && table.hierarchy.parents.len() != 1 {
53        return Err(SQLError::Internal(
54            "a partition must have exactly one parent".into(),
55        ));
56    }
57    let mut canonical_parents = Vec::with_capacity(table.hierarchy.parents.len());
58    let mut inherited_columns = Vec::new();
59    let mut inherited_checks = Vec::new();
60    let mut inherited_foreign_keys = Vec::new();
61    let mut inherited_keys = Vec::new();
62    for requested_parent in &table.hierarchy.parents {
63        let parent = context.catalog.resolve_parent(requested_parent)?;
64        if parent == table.name {
65            return Err(SQLError::Routine {
66                sqlstate: "42P17".into(),
67                message: "circular inheritance not allowed".into(),
68            });
69        }
70        let parent_hierarchy = context
71            .partitions
72            .catalog
73            .try_table_hierarchy(&parent)
74            .map_err(|error| SQLError::Internal(format!("read parent hierarchy: {error}")))?;
75        if is_partition {
76            let Some(parent_spec) = parent_hierarchy.partition_spec.as_ref() else {
77                return Err(SQLError::Routine {
78                    sqlstate: "42809".into(),
79                    message: format!("relation \"{requested_parent}\" is not partitioned"),
80                });
81            };
82            validate_partition_bound_strategy(
83                parent_spec.strategy,
84                table.hierarchy.partition_bound.as_ref().ok_or_else(|| {
85                    SQLError::Internal("partition lost its bound during validation".into())
86                })?,
87            )?;
88        } else if parent_hierarchy.partition_spec.is_some() {
89            return Err(SQLError::Routine {
90                sqlstate: "42809".into(),
91                message: format!("cannot inherit from partitioned table \"{requested_parent}\""),
92            });
93        }
94        let mut columns = context
95            .partitions
96            .catalog
97            .try_describe_table(&parent)
98            .map_err(|error| SQLError::Internal(format!("read inherited row type: {error}")))?
99            .ok_or_else(|| SQLError::UnknownTable(parent.clone()))?;
100        for column in &mut columns {
101            column.not_null_identity = None;
102            if let Some(reference) = &mut column.references {
103                reference.catalog_identity = None;
104            }
105            if column.not_null_no_inherit {
106                column.not_null = false;
107                column.not_null_explicit = false;
108                column.not_null_name = None;
109                column.not_null_no_inherit = false;
110                column.not_null_validated = true;
111            }
112            column.not_null_is_local = !column.not_null;
113            // CHECKs inherit as named constraints independently of the merged column's origin.
114            column.check = None;
115            column.check_name = None;
116            column.check_object_id = None;
117            column.check_catalog_oid = None;
118            column.check_is_local = true;
119            column.check_enforced = true;
120            column.check_validated = true;
121            column.check_no_inherit = false;
122        }
123        if !is_partition {
124            // PostgreSQL inherits the NOT NULL property of an identity column, but not its identity generation attribute or owned sequence. SERIAL is different: its nextval default is ordinary inherited metadata and therefore keeps pointing at the parent's sequence.
125            for column in &mut columns {
126                column.references = None;
127                if column
128                    .auto_increment
129                    .as_ref()
130                    .is_some_and(crate::ast::AutoIncrement::is_identity)
131                {
132                    column.auto_increment = None;
133                }
134            }
135        }
136        merge_columns(&mut inherited_columns, columns)?;
137        let constraints = context
138            .catalog
139            .declared_constraints(&parent)
140            .map_err(|error| SQLError::Internal(format!("read inherited constraints: {error}")))?;
141        for mut check in context
142            .catalog
143            .check_definitions(&parent)
144            .map_err(|error| SQLError::Internal(format!("read inherited CHECKs: {error}")))?
145            .into_iter()
146            .filter(|check| !check.no_inherit)
147        {
148            super::check_inheritance::bind_parent_check_columns(&parent, &mut check.expr)?;
149            check.is_local = false;
150            check.object_id = None;
151            check.catalog_oid = None;
152            check.validated = check.enforced;
153            inherited_checks.push(check);
154        }
155        if is_partition {
156            inherited_foreign_keys.extend(constraints.foreign_keys.into_iter().map(|mut key| {
157                key.catalog_identity = None;
158                key
159            }));
160            inherited_keys.extend(constraints.key_constraints.into_iter().map(|mut key| {
161                key.name = None;
162                key.catalog_identity = None;
163                key
164            }));
165        }
166        canonical_parents.push(parent);
167    }
168    merge_columns(&mut inherited_columns, std::mem::take(&mut table.columns))?;
169    table.columns = inherited_columns;
170    inherited_checks.append(&mut table.checks);
171    table.checks = inherited_checks;
172    if is_partition {
173        inherited_foreign_keys.append(&mut table.foreign_keys);
174        inherited_keys.append(&mut table.key_constraints);
175        table.foreign_keys = inherited_foreign_keys;
176        table.key_constraints = inherited_keys;
177    }
178    table.hierarchy.parents = canonical_parents;
179    validate_partition_keys(context, table)?;
180    if let (Some(parent), Some(bound)) = (
181        table.hierarchy.parents.first(),
182        table.hierarchy.partition_bound.as_ref(),
183    ) {
184        validate_new_partition_bound(&context.partitions, parent, bound)?;
185    }
186    Ok(())
187}
188
189fn validate_partition_bound_strategy(
190    strategy: crate::ast::PartitionStrategy,
191    bound: &crate::ast::PartitionBound,
192) -> Result<(), SQLError> {
193    use crate::ast::{PartitionBound, PartitionStrategy};
194    if matches!(
195        (strategy, bound),
196        (PartitionStrategy::Hash, PartitionBound::Default)
197    ) {
198        return Err(SQLError::Routine {
199            sqlstate: "42P16".into(),
200            message: "a hash-partitioned table may not have a default partition".into(),
201        });
202    }
203    let matches = matches!(bound, PartitionBound::Default)
204        || matches!(
205            (strategy, bound),
206            (PartitionStrategy::List, PartitionBound::List(_))
207                | (PartitionStrategy::Range, PartitionBound::Range { .. })
208                | (PartitionStrategy::Hash, PartitionBound::Hash { .. })
209        );
210    if matches {
211        Ok(())
212    } else {
213        Err(SQLError::Internal(
214            "partition bound strategy differs from its parent".into(),
215        ))
216    }
217}
218
219fn merge_columns(
220    merged: &mut Vec<crate::ast::ColumnDef>,
221    incoming: Vec<crate::ast::ColumnDef>,
222) -> Result<(), SQLError> {
223    for column in incoming {
224        if let Some(existing) = merged.iter_mut().find(|item| item.name == column.name) {
225            merge_same_column(existing, column)?;
226        } else {
227            merged.push(column);
228        }
229    }
230    Ok(())
231}
232
233pub fn merge_same_column(
234    inherited: &mut crate::ast::ColumnDef,
235    declared: crate::ast::ColumnDef,
236) -> Result<(), SQLError> {
237    if inherited.ty != declared.ty {
238        return Err(SQLError::Routine {
239            sqlstate: "42804".into(),
240            message: format!(
241                "inherited column \"{}\" has a type conflict",
242                inherited.name
243            ),
244        });
245    }
246    if inherited.generated.is_some() != declared.generated.is_some() {
247        return Err(SQLError::Routine {
248            sqlstate: "42P17".into(),
249            message: format!(
250                "inherited column \"{}\" has a generation conflict",
251                inherited.name
252            ),
253        });
254    }
255    let not_null_is_local = (inherited.not_null && inherited.not_null_is_local)
256        || (declared.not_null && declared.not_null_is_local);
257    if declared.not_null && (!inherited.not_null || declared.not_null_is_local) {
258        inherited.not_null_name.clone_from(&declared.not_null_name);
259        inherited.not_null_identity = declared.not_null_identity;
260        inherited.not_null_validated = declared.not_null_validated;
261        inherited.not_null_no_inherit = declared.not_null_no_inherit;
262    }
263    inherited.not_null |= declared.not_null;
264    inherited.not_null_is_local = !inherited.not_null || not_null_is_local;
265    inherited.not_null_explicit |= declared.not_null_explicit;
266    inherited.primary_key |= declared.primary_key;
267    inherited.unique |= declared.unique;
268    if declared.auto_increment.is_some() {
269        inherited.auto_increment = declared.auto_increment;
270    }
271    if declared.default.is_some() {
272        inherited.default = declared.default;
273    }
274    if declared.generated.is_some() {
275        inherited.generated = declared.generated;
276    }
277    if declared.check.is_some() {
278        inherited.check = declared.check;
279        inherited.check_name = declared.check_name;
280        inherited.check_enforced = declared.check_enforced;
281        inherited.check_validated = declared.check_validated;
282        inherited.check_no_inherit = declared.check_no_inherit;
283        inherited.check_is_local = declared.check_is_local;
284        inherited.check_object_id = declared.check_object_id;
285    }
286    if declared.references.is_some() {
287        inherited.references = declared.references;
288    }
289    Ok(())
290}
291
292fn validate_partition_keys(
293    context: &InheritanceContext<'_>,
294    table: &CreateTable,
295) -> Result<(), SQLError> {
296    let Some(spec) = table.hierarchy.partition_spec.as_ref() else {
297        return Ok(());
298    };
299    let column_names = table
300        .columns
301        .iter()
302        .map(|column| column.name.as_str())
303        .collect::<BTreeSet<_>>();
304    for key in &spec.keys {
305        let scalar = crate::plan::ExpressionPlan::lower(key.clone()).scalar;
306        let mut referenced_columns = BTreeSet::new();
307        scalar.collect_columns(&mut referenced_columns);
308        for column in referenced_columns {
309            if !column_names.contains(column.as_str()) {
310                return Err(SQLError::Routine {
311                    sqlstate: "42703".into(),
312                    message: format!("column \"{column}\" named in partition key does not exist"),
313                });
314            }
315        }
316    }
317    validate_hash_partition_spec(&context.partitions, spec, &table.columns)?;
318    for key in &spec.keys {
319        crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
320            context.roles,
321            key,
322            None,
323        )?;
324    }
325    Ok(())
326}
327
328pub mod alter;
329
330pub mod detachment;
331pub mod origins;
332pub mod restoration;