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                reference.referenced_partitions.clear();
105            }
106            if column.not_null_no_inherit {
107                column.not_null = false;
108                column.not_null_explicit = false;
109                column.not_null_name = None;
110                column.not_null_no_inherit = false;
111                column.not_null_validated = true;
112            }
113            column.not_null_is_local = !column.not_null;
114            // CHECKs inherit as named constraints independently of the merged column's origin.
115            column.check = None;
116            column.check_name = None;
117            column.check_object_id = None;
118            column.check_catalog_oid = None;
119            column.check_is_local = true;
120            column.check_enforced = true;
121            column.check_validated = true;
122            column.check_no_inherit = false;
123        }
124        if !is_partition {
125            // 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.
126            for column in &mut columns {
127                column.references = None;
128                if column
129                    .auto_increment
130                    .as_ref()
131                    .is_some_and(crate::ast::AutoIncrement::is_identity)
132                {
133                    column.auto_increment = None;
134                }
135            }
136        }
137        merge_columns(&mut inherited_columns, columns)?;
138        let constraints = context
139            .catalog
140            .declared_constraints(&parent)
141            .map_err(|error| SQLError::Internal(format!("read inherited constraints: {error}")))?;
142        for mut check in context
143            .catalog
144            .check_definitions(&parent)
145            .map_err(|error| SQLError::Internal(format!("read inherited CHECKs: {error}")))?
146            .into_iter()
147            .filter(|check| !check.no_inherit)
148        {
149            super::check_inheritance::bind_parent_check_columns(&parent, &mut check.expr)?;
150            check.is_local = false;
151            check.object_id = None;
152            check.catalog_oid = None;
153            check.validated = check.enforced;
154            inherited_checks.push(check);
155        }
156        if is_partition {
157            inherited_foreign_keys.extend(constraints.foreign_keys.into_iter().map(|mut key| {
158                key.catalog_identity = None;
159                key.referenced_partitions.clear();
160                key
161            }));
162            inherited_keys.extend(constraints.key_constraints.into_iter().map(|mut key| {
163                key.name = None;
164                key.catalog_identity = None;
165                key
166            }));
167        }
168        canonical_parents.push(parent);
169    }
170    merge_columns(&mut inherited_columns, std::mem::take(&mut table.columns))?;
171    table.columns = inherited_columns;
172    inherited_checks.append(&mut table.checks);
173    table.checks = inherited_checks;
174    if is_partition {
175        inherited_foreign_keys.append(&mut table.foreign_keys);
176        inherited_keys.append(&mut table.key_constraints);
177        table.foreign_keys = inherited_foreign_keys;
178        table.key_constraints = inherited_keys;
179    }
180    table.hierarchy.parents = canonical_parents;
181    validate_partition_keys(context, table)?;
182    if let (Some(parent), Some(bound)) = (
183        table.hierarchy.parents.first(),
184        table.hierarchy.partition_bound.as_ref(),
185    ) {
186        validate_new_partition_bound(&context.partitions, parent, bound)?;
187    }
188    Ok(())
189}
190
191fn validate_partition_bound_strategy(
192    strategy: crate::ast::PartitionStrategy,
193    bound: &crate::ast::PartitionBound,
194) -> Result<(), SQLError> {
195    use crate::ast::{PartitionBound, PartitionStrategy};
196    if matches!(
197        (strategy, bound),
198        (PartitionStrategy::Hash, PartitionBound::Default)
199    ) {
200        return Err(SQLError::Routine {
201            sqlstate: "42P16".into(),
202            message: "a hash-partitioned table may not have a default partition".into(),
203        });
204    }
205    let matches = matches!(bound, PartitionBound::Default)
206        || matches!(
207            (strategy, bound),
208            (PartitionStrategy::List, PartitionBound::List(_))
209                | (PartitionStrategy::Range, PartitionBound::Range { .. })
210                | (PartitionStrategy::Hash, PartitionBound::Hash { .. })
211        );
212    if matches {
213        Ok(())
214    } else {
215        Err(SQLError::Internal(
216            "partition bound strategy differs from its parent".into(),
217        ))
218    }
219}
220
221fn merge_columns(
222    merged: &mut Vec<crate::ast::ColumnDef>,
223    incoming: Vec<crate::ast::ColumnDef>,
224) -> Result<(), SQLError> {
225    for column in incoming {
226        if let Some(existing) = merged.iter_mut().find(|item| item.name == column.name) {
227            merge_same_column(existing, column)?;
228        } else {
229            merged.push(column);
230        }
231    }
232    Ok(())
233}
234
235pub fn merge_same_column(
236    inherited: &mut crate::ast::ColumnDef,
237    declared: crate::ast::ColumnDef,
238) -> Result<(), SQLError> {
239    if inherited.ty != declared.ty {
240        return Err(SQLError::Routine {
241            sqlstate: "42804".into(),
242            message: format!(
243                "inherited column \"{}\" has a type conflict",
244                inherited.name
245            ),
246        });
247    }
248    if inherited.generated.is_some() != declared.generated.is_some() {
249        return Err(SQLError::Routine {
250            sqlstate: "42P17".into(),
251            message: format!(
252                "inherited column \"{}\" has a generation conflict",
253                inherited.name
254            ),
255        });
256    }
257    let not_null_is_local = (inherited.not_null && inherited.not_null_is_local)
258        || (declared.not_null && declared.not_null_is_local);
259    if declared.not_null && (!inherited.not_null || declared.not_null_is_local) {
260        inherited.not_null_name.clone_from(&declared.not_null_name);
261        inherited.not_null_identity = declared.not_null_identity;
262        inherited.not_null_validated = declared.not_null_validated;
263        inherited.not_null_no_inherit = declared.not_null_no_inherit;
264    }
265    inherited.not_null |= declared.not_null;
266    inherited.not_null_is_local = !inherited.not_null || not_null_is_local;
267    inherited.not_null_explicit |= declared.not_null_explicit;
268    inherited.primary_key |= declared.primary_key;
269    inherited.unique |= declared.unique;
270    if declared.auto_increment.is_some() {
271        inherited.auto_increment = declared.auto_increment;
272    }
273    if declared.default.is_some() {
274        inherited.default = declared.default;
275    }
276    if declared.generated.is_some() {
277        inherited.generated = declared.generated;
278    }
279    if declared.check.is_some() {
280        inherited.check = declared.check;
281        inherited.check_name = declared.check_name;
282        inherited.check_enforced = declared.check_enforced;
283        inherited.check_validated = declared.check_validated;
284        inherited.check_no_inherit = declared.check_no_inherit;
285        inherited.check_is_local = declared.check_is_local;
286        inherited.check_object_id = declared.check_object_id;
287    }
288    if declared.references.is_some() {
289        inherited.references = declared.references;
290    }
291    Ok(())
292}
293
294fn validate_partition_keys(
295    context: &InheritanceContext<'_>,
296    table: &CreateTable,
297) -> Result<(), SQLError> {
298    let Some(spec) = table.hierarchy.partition_spec.as_ref() else {
299        return Ok(());
300    };
301    let column_names = table
302        .columns
303        .iter()
304        .map(|column| column.name.as_str())
305        .collect::<BTreeSet<_>>();
306    for key in &spec.keys {
307        let scalar = crate::plan::ExpressionPlan::lower(key.clone()).scalar;
308        let mut referenced_columns = BTreeSet::new();
309        scalar.collect_columns(&mut referenced_columns);
310        for column in referenced_columns {
311            if !column_names.contains(column.as_str()) {
312                return Err(SQLError::Routine {
313                    sqlstate: "42703".into(),
314                    message: format!("column \"{column}\" named in partition key does not exist"),
315                });
316            }
317        }
318    }
319    validate_hash_partition_spec(&context.partitions, spec, &table.columns)?;
320    for key in &spec.keys {
321        crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
322            context.roles,
323            key,
324            None,
325        )?;
326    }
327    Ok(())
328}
329
330pub mod alter;
331
332pub mod detachment;
333pub mod foreign_keys;
334pub mod origins;
335pub mod restoration;