Skip to main content

uqa_sql/binding/portals/
relations.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind cursor relation names while preserving CTE and transition-relation visibility.
8
9use super::PortalBindingContext;
10use crate::{
11    ast::OperatorJoinRelations,
12    plan::{QueryPlan, RelationalPlan, SourcePlan},
13    SQLError,
14};
15
16pub fn bind_session_portal_query_relations(
17    inputs: &PortalBindingContext<'_>,
18    query: &mut QueryPlan,
19    inherited_ctes: &std::collections::BTreeSet<String>,
20) -> Result<(), SQLError> {
21    let mut visible_ctes = inherited_ctes.clone();
22    for cte in &mut query.ctes {
23        let mut definition_scope = visible_ctes.clone();
24        if cte.recursive {
25            definition_scope.insert(cte.name.clone());
26        }
27        crate::binding::view_dependencies::bind_cte_plan_relations(
28            &mut cte.body,
29            &definition_scope,
30            &mut |name| {
31                let mut name = name.to_string();
32                bind_session_portal_relation_reference(
33                    inputs,
34                    &mut name,
35                    &std::collections::BTreeSet::new(),
36                )?;
37                Ok::<_, SQLError>(name)
38            },
39        )?;
40        visible_ctes.insert(cte.name.clone());
41    }
42    bind_session_portal_relational_plan(inputs, &mut query.root, &visible_ctes)?;
43    query.relations_bound = true;
44    Ok(())
45}
46
47pub fn bind_session_portal_relational_plan(
48    inputs: &PortalBindingContext<'_>,
49    plan: &mut RelationalPlan,
50    visible_ctes: &std::collections::BTreeSet<String>,
51) -> Result<(), SQLError> {
52    match plan {
53        RelationalPlan::QueryBlock(block) => {
54            if let Some(source) = block.from.as_mut() {
55                bind_session_portal_source_plan(inputs, source, visible_ctes)?;
56            }
57            for subquery in &mut block.subqueries {
58                bind_session_portal_query_relations(inputs, subquery, visible_ctes)?;
59            }
60            Ok(())
61        }
62        RelationalPlan::SetOp {
63            left,
64            right,
65            subqueries,
66            ..
67        } => {
68            bind_session_portal_query_relations(inputs, left, visible_ctes)?;
69            bind_session_portal_query_relations(inputs, right, visible_ctes)?;
70            for subquery in subqueries {
71                bind_session_portal_query_relations(inputs, subquery, visible_ctes)?;
72            }
73            Ok(())
74        }
75        RelationalPlan::Values { subqueries, .. } => {
76            for subquery in subqueries {
77                bind_session_portal_query_relations(inputs, subquery, visible_ctes)?;
78            }
79            Ok(())
80        }
81    }
82}
83
84pub fn bind_session_portal_source_plan(
85    inputs: &PortalBindingContext<'_>,
86    source: &mut SourcePlan,
87    visible_ctes: &std::collections::BTreeSet<String>,
88) -> Result<(), SQLError> {
89    match source {
90        SourcePlan::Table { name, .. } => {
91            bind_session_portal_relation_reference(inputs, name, visible_ctes)
92        }
93        SourcePlan::Join { left, right, .. } => {
94            bind_session_portal_source_plan(inputs, left, visible_ctes)?;
95            bind_session_portal_source_plan(inputs, right, visible_ctes)
96        }
97        SourcePlan::Subquery { body, .. } => {
98            bind_session_portal_query_relations(inputs, body, visible_ctes)
99        }
100        SourcePlan::Function { relations, .. } => {
101            bind_session_portal_function_relations(inputs, relations)
102        }
103        SourcePlan::FunctionGroup { functions, .. } => {
104            for function in functions {
105                bind_session_portal_function_relations(inputs, &mut function.relations)?;
106            }
107            Ok(())
108        }
109        SourcePlan::Values { .. } => Ok(()),
110    }
111}
112
113fn bind_session_portal_relation_reference(
114    inputs: &PortalBindingContext<'_>,
115    name: &mut String,
116    visible_ctes: &std::collections::BTreeSet<String>,
117) -> Result<(), SQLError> {
118    if uqa_core::RelationIdentity::parse_reference(name)
119        .ok()
120        .is_some_and(|(schema, name)| schema.is_none() && visible_ctes.contains(&name))
121    {
122        return Ok(());
123    }
124    let requested = name.clone();
125    if let Some(canonical) =
126        crate::binding::view_dependencies::canonical_virtual_relation_reference(&requested)
127    {
128        *name = canonical;
129        return Ok(());
130    }
131    if uqa_core::RelationIdentity::parse_reference(&requested)
132        .ok()
133        .is_some_and(|(schema, relation)| {
134            schema.is_none()
135                && inputs
136                    .transitions
137                    .active_transition_relation_names()
138                    .contains(&relation)
139        })
140    {
141        return Ok(());
142    }
143    if let Some(canonical) = inputs.catalog.resolve_age_label_relation_name(&requested)? {
144        *name = canonical;
145        return Ok(());
146    }
147    match inputs
148        .catalog
149        .try_resolve_visible_relation_kind(&requested)?
150    {
151        Some((canonical, _)) => *name = canonical,
152        None => return Err(SQLError::UnknownTable(requested)),
153    }
154    Ok(())
155}
156
157pub fn bind_session_portal_function_relations(
158    inputs: &PortalBindingContext<'_>,
159    relations: &mut Option<OperatorJoinRelations>,
160) -> Result<(), SQLError> {
161    let Some(relations) = relations else {
162        return Ok(());
163    };
164    for relation in [&mut relations.left, &mut relations.right] {
165        let requested = relation.clone();
166        match inputs
167            .catalog
168            .try_resolve_visible_relation_kind(&requested)?
169        {
170            Some((canonical, "table")) => *relation = canonical,
171            Some((canonical, kind)) => {
172                return Err(SQLError::Routine {
173                    sqlstate: "42809".into(),
174                    message: format!(
175                        "cursor table-function relation \"{canonical}\" is a {kind}, not a table"
176                    ),
177                });
178            }
179            None => return Err(SQLError::UnknownTable(requested)),
180        }
181    }
182    Ok(())
183}