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::schema::table_creation::not_nulls::InheritedNotNull;
10use crate::semantics::partition::{
11    transform_partition_bound, validate_new_partition_bound, PartitionContext,
12};
13use crate::{
14    ast::{CreateTable, TableCheck, TableConstraintSet},
15    SQLError,
16};
17/// Parent lookup and constraint declarations used while assembling a new row type.
18pub trait InheritanceCatalog {
19    fn resolve_parent(&self, name: &str) -> Result<String, SQLError>;
20    fn declared_constraints(&self, table: &str) -> Result<TableConstraintSet, String>;
21    fn check_definitions(&self, table: &str) -> Result<Vec<TableCheck>, String>;
22    /// The key attributes of each unique index of `table` that no key constraint owns; a partition builds an index for each.
23    fn unique_index_keys(&self, table: &str) -> Result<Vec<Vec<crate::ast::IndexKey>>, String>;
24}
25pub struct InheritanceContext<'a> {
26    pub catalog: &'a dyn InheritanceCatalog,
27    pub partitions: PartitionContext<'a>,
28    pub roles: &'a dyn crate::expr::EngineHook,
29}
30
31/// The parent relation that `INHERITS` names: `table_openrv` refuses indexes and composite types, and `MergeAttributes` accepts only tables. Foreign tables are not inheritance parents here.
32pub fn inheritance_parent_target(
33    resolution: crate::catalog::resolution::RelationResolution,
34    requested: &str,
35) -> Result<String, SQLError> {
36    use crate::catalog::resolution::RelationResolution;
37    match resolution {
38        RelationResolution::Found(canonical, "table") => Ok(canonical),
39        RelationResolution::Found(canonical, kind @ ("index" | "composite type")) => {
40            Err(crate::catalog::analysis::UnopenableRelation {
41                name: local_relation_name(&canonical),
42                kinds: if kind == "index" {
43                    "indexes"
44                } else {
45                    "composite types"
46                },
47            }
48            .error())
49        }
50        RelationResolution::Found(canonical, "view" | "materialized view" | "sequence") => {
51            Err(SQLError::Routine {
52                sqlstate: "42809".into(),
53                message: format!(
54                    "inherited relation \"{}\" is not a table or foreign table",
55                    local_relation_name(&canonical)
56                ),
57            })
58        }
59        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
60            sqlstate: "3F000".into(),
61            message: format!("schema \"{schema}\" does not exist"),
62        }),
63        RelationResolution::Found(_, _) | RelationResolution::MissingRelation => {
64            Err(SQLError::UnknownTable(requested.to_string()))
65        }
66    }
67}
68
69fn local_relation_name(canonical: &str) -> String {
70    uqa_core::RelationIdentity::from_legacy_name(canonical)
71        .map_or_else(|_| canonical.to_string(), |relation| relation.name)
72}
73
74/// `MergeAttributes`: the columns, CHECK constraints and partition keys a new table inherits from its parents, ahead of its own, with the notices that report each merged column. `DefineRelation` first looks up every parent, rejecting one named twice; the table's own columns cannot repeat a name. The table's partition key and bound are bound by [`bind_create_table_partitioning`] once its row type is described, as `DefineRelation` computes them after creating the relation.
75#[expect(
76    clippy::too_many_lines,
77    reason = "preserves DDL dependency and action order"
78)]
79pub fn merge_create_table_hierarchy(
80    context: &InheritanceContext<'_>,
81    table: &mut CreateTable,
82    notices: &mut Vec<crate::SQLNotice>,
83) -> Result<MergedParents, SQLError> {
84    table.hierarchy.local_columns = table
85        .columns
86        .iter()
87        .map(|column| column.name.clone())
88        .collect();
89    if table.hierarchy.parents.is_empty() {
90        if table.hierarchy.partition_bound.is_some() {
91            return Err(SQLError::Internal(
92                "partition bound has no parent relation".into(),
93            ));
94        }
95        if let Some(column) = table.untyped_columns.first() {
96            return Err(SQLError::Internal(format!("column `{column}` has no type")));
97        }
98        column_merge::check_column_count(table.columns.len())?;
99        column_merge::reject_repeated_columns(&table.columns)?;
100        return Ok(MergedParents::default());
101    }
102    let is_partition = table.hierarchy.partition_bound.is_some();
103    if is_partition && table.hierarchy.parents.len() != 1 {
104        return Err(SQLError::Internal(
105            "a partition must have exactly one parent".into(),
106        ));
107    }
108    let mut canonical_parents = Vec::with_capacity(table.hierarchy.parents.len());
109    for requested_parent in &table.hierarchy.parents {
110        // A parent with the new table's own name is the relation that already has the name, which `heap_create_with_catalog` reports once the columns are described.
111        let parent = context.catalog.resolve_parent(requested_parent)?;
112        if canonical_parents.contains(&parent) {
113            return Err(SQLError::Routine {
114                sqlstate: "42P07".into(),
115                message: format!(
116                    "relation \"{}\" would be inherited from more than once",
117                    local_relation_name(&parent)
118                ),
119            });
120        }
121        canonical_parents.push(parent);
122    }
123    column_merge::check_column_count(table.columns.len())?;
124    column_merge::reject_repeated_columns(&table.columns)?;
125    let mut inherited = column_merge::InheritedColumns::default();
126    let mut inherited_not_nulls = Vec::new();
127    let mut inherited_checks = Vec::new();
128    let mut inherited_foreign_keys = Vec::new();
129    let mut inherited_keys = Vec::new();
130    for parent in &canonical_parents {
131        let parent_name = local_relation_name(parent);
132        let parent_hierarchy = context
133            .partitions
134            .catalog
135            .try_table_hierarchy(parent)
136            .map_err(|error| SQLError::Internal(format!("read parent hierarchy: {error}")))?;
137        // A partition's parent that is not partitioned is reported with its bound.
138        if !is_partition {
139            if parent_hierarchy.partition_spec.is_some() {
140                return Err(SQLError::Routine {
141                    sqlstate: "42809".into(),
142                    message: format!("cannot inherit from partitioned table \"{parent_name}\""),
143                });
144            }
145            if parent_hierarchy.is_partition() {
146                return Err(SQLError::Routine {
147                    sqlstate: "42809".into(),
148                    message: format!("cannot inherit from partition \"{parent_name}\""),
149                });
150            }
151        }
152        let constraints = context
153            .catalog
154            .declared_constraints(parent)
155            .map_err(|error| SQLError::Internal(format!("read inherited constraints: {error}")))?;
156        check_parent_persistence(
157            &parent_name,
158            constraints.persistence,
159            table.persistence,
160            is_partition,
161        )?;
162        let mut columns = context
163            .partitions
164            .catalog
165            .try_describe_table(parent)
166            .map_err(|error| SQLError::Internal(format!("read inherited row type: {error}")))?
167            .ok_or_else(|| SQLError::UnknownTable(parent.clone()))?;
168        for column in &mut columns {
169            column.attribute_number = None;
170            column.not_null_identity = None;
171            // The child stores its own copy of an inherited default.
172            column.default_catalog_oid = None;
173            if let Some(reference) = &mut column.references {
174                reference.catalog_identity = None;
175                reference.referenced_partitions.clear();
176            }
177            if column.not_null_no_inherit {
178                column.not_null = false;
179                column.not_null_explicit = false;
180                column.not_null_name = None;
181                column.not_null_no_inherit = false;
182                column.not_null_validated = true;
183            }
184            column.not_null_is_local = !column.not_null;
185            // CHECKs inherit as named constraints independently of the merged column's origin.
186            column.check = None;
187            column.check_name = None;
188            column.check_object_id = None;
189            column.check_catalog_oid = None;
190            column.check_is_local = true;
191            column.check_enforced = true;
192            column.check_validated = true;
193            column.check_no_inherit = false;
194        }
195        if !is_partition {
196            // 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. Key constraints, like foreign keys, are not inherited: only their columns' NOT NULL constraints are.
197            for column in &mut columns {
198                column.references = None;
199                column.primary_key = false;
200                column.unique = false;
201                if column
202                    .auto_increment
203                    .as_ref()
204                    .is_some_and(crate::ast::AutoIncrement::is_identity)
205                {
206                    column.auto_increment = None;
207                }
208            }
209        }
210        for column in &columns {
211            if column.not_null {
212                InheritedNotNull::record(
213                    &mut inherited_not_nulls,
214                    &column.name,
215                    column.not_null_name.as_deref(),
216                );
217            }
218        }
219        for column in columns {
220            inherited.merge_parent_column(column, notices)?;
221        }
222        // The relation cache holds a parent's CHECK constraints in name order, and `MergeAttributes` takes them in that order.
223        let mut parent_checks = context
224            .catalog
225            .check_definitions(parent)
226            .map_err(|error| SQLError::Internal(format!("read inherited CHECKs: {error}")))?
227            .into_iter()
228            .filter(|check| !check.no_inherit)
229            .collect::<Vec<_>>();
230        parent_checks.sort_by(|left, right| left.name.cmp(&right.name));
231        for mut check in parent_checks {
232            super::check_inheritance::bind_parent_check_columns(parent, &mut check.expr)?;
233            check.is_local = false;
234            check.object_id = None;
235            check.catalog_oid = None;
236            check.validated = check.enforced;
237            super::check_inheritance::merge_inherited_check(
238                &mut inherited_checks,
239                check,
240                &inherited.columns,
241            )?;
242        }
243        if is_partition {
244            inherited_foreign_keys.extend(constraints.foreign_keys.into_iter().map(|mut key| {
245                key.catalog_identity = None;
246                key.referenced_partitions.clear();
247                key
248            }));
249            inherited_keys.extend(constraints.key_constraints.into_iter().map(|mut key| {
250                key.name = None;
251                key.catalog_identity = None;
252                key
253            }));
254        }
255    }
256    for (position, column) in std::mem::take(&mut table.columns).into_iter().enumerate() {
257        if is_partition {
258            inherited.merge_partition_option(column)?;
259        } else {
260            inherited.merge_declared_column(position, column, notices)?;
261        }
262    }
263    table.untyped_columns.clear();
264    column_merge::check_column_count(inherited.columns.len())?;
265    inherited.reject_conflicting_defaults()?;
266    let expressions = inherited.inherited_expressions();
267    table.columns = inherited.columns;
268    inherited_checks.append(&mut table.checks);
269    table.checks = inherited_checks;
270    if is_partition {
271        inherited_foreign_keys.append(&mut table.foreign_keys);
272        inherited_keys.append(&mut table.key_constraints);
273        table.foreign_keys = inherited_foreign_keys;
274        table.key_constraints = inherited_keys;
275    }
276    table.hierarchy.parents = canonical_parents;
277    Ok(MergedParents {
278        expressions,
279        not_nulls: inherited_not_nulls,
280    })
281}
282
283/// What `MergeAttributes` hands the steps that follow it: the columns whose default or generation expression a parent gives, and the NOT NULL constraints the parents give.
284#[derive(Debug, Default)]
285pub struct MergedParents {
286    pub expressions: Vec<String>,
287    pub not_nulls: Vec<InheritedNotNull>,
288}
289
290/// `MergeAttributes` keeps a temporary relation out of a permanent hierarchy: a temporary partition of a permanent table, and a permanent child or partition of a temporary one.
291fn check_parent_persistence(
292    parent: &str,
293    parent_persistence: crate::ast::RelationPersistence,
294    persistence: crate::ast::RelationPersistence,
295    is_partition: bool,
296) -> Result<(), SQLError> {
297    use crate::ast::RelationPersistence::Temporary;
298    let message = if is_partition && parent_persistence != Temporary && persistence == Temporary {
299        format!(
300            "cannot create a temporary relation as partition of permanent relation \"{parent}\""
301        )
302    } else if persistence != Temporary && parent_persistence == Temporary {
303        if is_partition {
304            format!("cannot create a permanent relation as partition of temporary relation \"{parent}\"")
305        } else {
306            format!("cannot inherit from temporary relation \"{parent}\"")
307        }
308    } else {
309        return Ok(());
310    };
311    Err(SQLError::Routine {
312        sqlstate: "42809".into(),
313        message,
314    })
315}
316
317/// The bound of a new partition and the partition key of a new partitioned table, in `DefineRelation` order: `transformPartitionBound` and `check_new_partition_bound` before `ComputePartitionAttrs`.
318pub fn bind_create_table_partitioning(
319    context: &InheritanceContext<'_>,
320    table: &mut CreateTable,
321) -> Result<(), SQLError> {
322    if let (Some(parent), Some(bound)) = (
323        table.hierarchy.parents.first(),
324        table.hierarchy.partition_bound.as_ref(),
325    ) {
326        // `DefineRelation` requires a partitioned parent once it stores the defaults.
327        let partitioned = context
328            .partitions
329            .catalog
330            .try_table_hierarchy(parent)
331            .map_err(|error| SQLError::Internal(format!("read parent hierarchy: {error}")))?
332            .partition_spec
333            .is_some();
334        if !partitioned {
335            return Err(SQLError::Routine {
336                sqlstate: "42P17".into(),
337                message: format!("\"{}\" is not partitioned", local_relation_name(parent)),
338            });
339        }
340        // The stored bound holds the values coerced to the parent's key types, as PostgreSQL stores Const nodes.
341        let bound = transform_partition_bound(&context.partitions, parent, bound)?;
342        validate_new_partition_bound(&context.partitions, parent, &table.name, &bound)?;
343        table.hierarchy.partition_bound = Some(bound);
344    }
345    validate_partition_keys(context, table)
346}
347
348pub fn merge_same_column(
349    inherited: &mut crate::ast::ColumnDef,
350    declared: crate::ast::ColumnDef,
351) -> Result<(), SQLError> {
352    if inherited.ty != declared.ty {
353        return Err(SQLError::Routine {
354            sqlstate: "42804".into(),
355            message: format!(
356                "inherited column \"{}\" has a type conflict",
357                inherited.name
358            ),
359        });
360    }
361    if inherited.generated.is_some() != declared.generated.is_some() {
362        return Err(SQLError::Routine {
363            sqlstate: "42P17".into(),
364            message: format!(
365                "inherited column \"{}\" has a generation conflict",
366                inherited.name
367            ),
368        });
369    }
370    // Recursive ADD merges into the child's existing column. It cannot adopt the parent's identity or attribute number.
371    inherited.object_id = declared.object_id;
372    inherited.attribute_number = declared.attribute_number;
373    let not_null_is_local = (inherited.not_null && inherited.not_null_is_local)
374        || (declared.not_null && declared.not_null_is_local);
375    if declared.not_null && (!inherited.not_null || declared.not_null_is_local) {
376        inherited.not_null_name.clone_from(&declared.not_null_name);
377        inherited.not_null_identity = declared.not_null_identity;
378        inherited.not_null_validated = declared.not_null_validated;
379        inherited.not_null_no_inherit = declared.not_null_no_inherit;
380    }
381    inherited.not_null |= declared.not_null;
382    inherited.not_null_is_local = !inherited.not_null || not_null_is_local;
383    inherited.not_null_explicit |= declared.not_null_explicit;
384    inherited.primary_key |= declared.primary_key;
385    inherited.unique |= declared.unique;
386    if declared.auto_increment.is_some() {
387        inherited.auto_increment = declared.auto_increment;
388    }
389    if declared.default.is_some() {
390        inherited.default = declared.default;
391    }
392    if declared.generated.is_some() {
393        inherited.generated = declared.generated;
394    }
395    if declared.check.is_some() {
396        inherited.check = declared.check;
397        inherited.check_name = declared.check_name;
398        inherited.check_enforced = declared.check_enforced;
399        inherited.check_validated = declared.check_validated;
400        inherited.check_no_inherit = declared.check_no_inherit;
401        inherited.check_is_local = declared.check_is_local;
402        inherited.check_object_id = declared.check_object_id;
403    }
404    if declared.references.is_some() {
405        inherited.references = declared.references;
406    }
407    Ok(())
408}
409
410mod column_merge;
411mod partition_keys;
412use partition_keys::validate_partition_keys;
413
414pub mod alter;
415
416pub mod detachment;
417pub mod foreign_keys;
418pub mod origins;
419pub mod restoration;