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