Skip to main content

uqa_sql/semantics/
referential.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-key action discovery across partition and inheritance metadata.
8use crate::{ast::ForeignKey, SQLError};
9
10pub trait ReferentialCatalog {
11    fn session_replication_role_is_replica(&self) -> bool;
12    /// The relation followed by each partitioned table it is a partition of, nearest first, as [`crate::semantics::partition::partition_ancestor_tables`] returns them.
13    fn partition_ancestor_tables(&self, table: &str) -> Result<Vec<String>, SQLError>;
14    fn try_referrers_to(&self, table: &str) -> Result<Vec<(String, ForeignKey)>, String>;
15}
16use crate::catalog::errors::dml_storage_error;
17
18pub fn referrers_to_for_actions(
19    catalog: &dyn ReferentialCatalog,
20    table: &str,
21) -> Result<Vec<(String, ForeignKey)>, SQLError> {
22    if catalog.session_replication_role_is_replica() {
23        return Ok(Vec::new());
24    }
25    let mut output = Vec::new();
26    // A row belongs to its partitions' ancestors, which foreign keys may reference; a foreign key referencing a plain inheritance parent reads only that parent's own rows.
27    for target in catalog.partition_ancestor_tables(table)? {
28        let referrers = catalog
29            .try_referrers_to(&target)
30            .map_err(|err| dml_storage_error("foreign-key lookup", err))?;
31        for (declaring_table, foreign_key) in &referrers {
32            if declares_foreign_key(catalog, &referrers, declaring_table, foreign_key)? {
33                output.push((declaring_table.clone(), foreign_key.clone()));
34            }
35        }
36    }
37    Ok(output)
38}
39
40/// A foreign key declared on a partitioned table recurs on each of its partitions under the object identity of the declaration, and only the declaration acts on the rows of the whole subtree, as `PostgreSQL` creates action triggers for the constraint without a parent alone. A partition's own foreign key acts on that partition's rows only.
41fn declares_foreign_key(
42    catalog: &dyn ReferentialCatalog,
43    referrers: &[(String, ForeignKey)],
44    declaring_table: &str,
45    foreign_key: &ForeignKey,
46) -> Result<bool, SQLError> {
47    let Some(object_id) = foreign_key.object_id else {
48        return Ok(true);
49    };
50    for ancestor in catalog
51        .partition_ancestor_tables(declaring_table)?
52        .iter()
53        .skip(1)
54    {
55        if referrers
56            .iter()
57            .any(|(table, key)| table == ancestor && key.object_id == Some(object_id))
58        {
59            return Ok(false);
60        }
61    }
62    Ok(true)
63}