Skip to main content

uqa_sql/binding/portals/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Collect table, graph and routine dependencies without evaluating cursor expressions.
8
9use super::{PortalBindingContext, SessionPortalTableDependencies};
10use crate::{
11    plan::{QueryPlan, RelationalPlan, SourcePlan},
12    registry::FunctionKind,
13    SQLError,
14};
15
16fn collect_graph_function_dependency(
17    name: &str,
18    binding: Option<&crate::ast::FunctionBinding>,
19    args: &[crate::ScalarExpr],
20    dependencies: &mut SessionPortalTableDependencies,
21) {
22    if binding.is_some_and(|binding| !binding.builtin) {
23        return;
24    }
25    let name = crate::semantics::builtin_function_dispatch_name(name);
26    let argument = match crate::registry::lookup(&name) {
27        Some(
28            FunctionKind::GraphPagerank
29            | FunctionKind::GraphHits
30            | FunctionKind::GraphBetweenness
31            | FunctionKind::GraphTraverse
32            | FunctionKind::GraphNeighbors
33            | FunctionKind::TraverseMatch
34            | FunctionKind::TemporalTraverse
35            | FunctionKind::GraphEdges,
36        ) => Some(0),
37        Some(FunctionKind::RPQ) => Some(2),
38        Some(FunctionKind::DeepPredict | FunctionKind::DeepLearn) => {
39            // Model programs can select graph inputs dynamically.
40            dependencies.graphs = None;
41            dependencies.graph_catalog = true;
42            return;
43        }
44        Some(
45            FunctionKind::GraphCreate
46            | FunctionKind::GraphDrop
47            | FunctionKind::GraphExists
48            | FunctionKind::GraphLabelCreate
49            | FunctionKind::GraphLabelDrop
50            | FunctionKind::GraphAlter,
51        ) => {
52            dependencies.graph_catalog = true;
53            return;
54        }
55        _ if name == "cypher" => Some(0),
56        _ if name == "graph_join" => {
57            dependencies.graphs = None;
58            dependencies.graph_catalog = true;
59            return;
60        }
61        _ => None,
62    };
63    let Some(argument) = argument else {
64        return;
65    };
66    dependencies.graph_catalog = true;
67    if let Some(crate::ScalarExpr::Literal(uqa_core::Value::Str(graph))) = args.get(argument) {
68        dependencies.insert_graph(graph.clone());
69    } else {
70        // Do not evaluate expressions or volatile functions at DECLARE.
71        // A parameter, expression, or default graph can select any graph.
72        dependencies.graphs = None;
73    }
74}
75
76pub fn session_portal_table_dependencies(
77    inputs: &PortalBindingContext<'_>,
78    query: &QueryPlan,
79) -> Result<SessionPortalTableDependencies, SQLError> {
80    let mut dependencies = SessionPortalTableDependencies::empty();
81    collect_session_portal_query_dependencies(
82        inputs,
83        query,
84        &mut dependencies,
85        &mut std::collections::BTreeSet::new(),
86        &mut std::collections::BTreeSet::new(),
87    )?;
88    Ok(dependencies)
89}
90
91pub fn collect_session_portal_query_dependencies(
92    inputs: &PortalBindingContext<'_>,
93    query: &QueryPlan,
94    dependencies: &mut SessionPortalTableDependencies,
95    visiting_views: &mut std::collections::BTreeSet<String>,
96    visiting_routines: &mut std::collections::BTreeSet<String>,
97) -> Result<(), SQLError> {
98    if dependencies.is_all() {
99        return Ok(());
100    }
101    for cte in &query.ctes {
102        collect_session_portal_cte_dependencies(
103            inputs,
104            &cte.body,
105            dependencies,
106            visiting_views,
107            visiting_routines,
108        )?;
109    }
110    collect_session_portal_relational_dependencies(
111        inputs,
112        &query.root,
113        dependencies,
114        visiting_views,
115        visiting_routines,
116    )?;
117
118    let mut plan = crate::plan::UnifiedPlan::Query(Box::new(query.clone()));
119    let mut routines = Vec::new();
120    plan.rewrite_scalar_expressions(&mut |expression| {
121        if let crate::ScalarExpr::Func {
122            name,
123            binding,
124            args,
125            ..
126        } = expression
127        {
128            collect_graph_function_dependency(name, binding.as_ref(), args, dependencies);
129            routines.push((name.clone(), binding.clone()));
130        }
131    });
132    for (name, binding) in routines {
133        collect_session_portal_routine_dependencies(
134            inputs,
135            &name,
136            binding.as_ref(),
137            dependencies,
138            visiting_views,
139            visiting_routines,
140        )?;
141        if dependencies.is_all() {
142            break;
143        }
144    }
145    Ok(())
146}
147
148fn collect_session_portal_cte_dependencies(
149    inputs: &PortalBindingContext<'_>,
150    body: &crate::plan::CtePlanBody,
151    dependencies: &mut SessionPortalTableDependencies,
152    visiting_views: &mut std::collections::BTreeSet<String>,
153    visiting_routines: &mut std::collections::BTreeSet<String>,
154) -> Result<(), SQLError> {
155    match body {
156        crate::plan::CtePlanBody::Query(query) => collect_session_portal_query_dependencies(
157            inputs,
158            query,
159            dependencies,
160            visiting_views,
161            visiting_routines,
162        ),
163        crate::plan::CtePlanBody::Command(command) => {
164            if let Some(target) = command.mutation_target() {
165                collect_session_portal_relation_dependencies(
166                    inputs,
167                    target,
168                    true,
169                    dependencies,
170                    visiting_views,
171                    visiting_routines,
172                )?;
173            }
174            for cte in command.ctes() {
175                collect_session_portal_cte_dependencies(
176                    inputs,
177                    &cte.body,
178                    dependencies,
179                    visiting_views,
180                    visiting_routines,
181                )?;
182            }
183            for query in command.query_inputs() {
184                collect_session_portal_query_dependencies(
185                    inputs,
186                    query,
187                    dependencies,
188                    visiting_views,
189                    visiting_routines,
190                )?;
191            }
192            if let Some(source) = command.source_input() {
193                collect_session_portal_source_dependencies(
194                    inputs,
195                    source,
196                    dependencies,
197                    visiting_views,
198                    visiting_routines,
199                )?;
200            }
201            Ok(())
202        }
203    }
204}
205
206pub fn collect_session_portal_relational_dependencies(
207    inputs: &PortalBindingContext<'_>,
208    plan: &RelationalPlan,
209    dependencies: &mut SessionPortalTableDependencies,
210    visiting_views: &mut std::collections::BTreeSet<String>,
211    visiting_routines: &mut std::collections::BTreeSet<String>,
212) -> Result<(), SQLError> {
213    match plan {
214        RelationalPlan::QueryBlock(block) => {
215            if let Some(source) = block.from.as_ref() {
216                collect_session_portal_source_dependencies(
217                    inputs,
218                    source,
219                    dependencies,
220                    visiting_views,
221                    visiting_routines,
222                )?;
223            }
224            for subquery in &block.subqueries {
225                collect_session_portal_query_dependencies(
226                    inputs,
227                    subquery,
228                    dependencies,
229                    visiting_views,
230                    visiting_routines,
231                )?;
232            }
233        }
234        RelationalPlan::SetOp {
235            left,
236            right,
237            subqueries,
238            ..
239        } => {
240            collect_session_portal_query_dependencies(
241                inputs,
242                left,
243                dependencies,
244                visiting_views,
245                visiting_routines,
246            )?;
247            collect_session_portal_query_dependencies(
248                inputs,
249                right,
250                dependencies,
251                visiting_views,
252                visiting_routines,
253            )?;
254            for subquery in subqueries {
255                collect_session_portal_query_dependencies(
256                    inputs,
257                    subquery,
258                    dependencies,
259                    visiting_views,
260                    visiting_routines,
261                )?;
262            }
263        }
264        RelationalPlan::Values { subqueries, .. } => {
265            for subquery in subqueries {
266                collect_session_portal_query_dependencies(
267                    inputs,
268                    subquery,
269                    dependencies,
270                    visiting_views,
271                    visiting_routines,
272                )?;
273            }
274        }
275    }
276    Ok(())
277}
278
279fn collect_session_portal_relation_dependencies(
280    inputs: &PortalBindingContext<'_>,
281    name: &str,
282    include_descendants: bool,
283    dependencies: &mut SessionPortalTableDependencies,
284    visiting_views: &mut std::collections::BTreeSet<String>,
285    visiting_routines: &mut std::collections::BTreeSet<String>,
286) -> Result<(), SQLError> {
287    if let Some(table) = inputs
288        .catalog
289        .try_resolve_table_name(name)
290        .map_err(|error| {
291            SQLError::Internal(format!(
292                "resolve cursor dependency relation `{name}`: {error}"
293            ))
294        })?
295    {
296        for table in inputs
297            .catalog
298            .hierarchy_scan_tables(&table, include_descendants)?
299        {
300            dependencies.insert(
301                uqa_core::RelationIdentity::from_legacy_name(&table).map_err(|error| {
302                    SQLError::Internal(format!(
303                        "resolve cursor dependency identity `{table}`: {error}"
304                    ))
305                })?,
306            );
307        }
308        return Ok(());
309    }
310    if crate::binding::view_dependencies::canonical_virtual_relation_reference(name).is_some() {
311        dependencies.tables = None;
312        dependencies.graph_catalog = true;
313        return Ok(());
314    }
315    if let Some(relation) = inputs.catalog.resolve_age_label_relation_name(name)? {
316        let relation =
317            uqa_core::RelationIdentity::from_legacy_name(&relation).map_err(SQLError::Internal)?;
318        dependencies.insert_graph(relation.schema);
319        return Ok(());
320    }
321    let key = name.to_ascii_lowercase();
322    if !visiting_views.insert(key.clone()) {
323        return Ok(());
324    }
325    if let Some(view) = inputs.catalog.view_plan(name)? {
326        collect_session_portal_query_dependencies(
327            inputs,
328            &view,
329            dependencies,
330            visiting_views,
331            visiting_routines,
332        )?;
333    }
334    visiting_views.remove(&key);
335    Ok(())
336}
337
338pub fn collect_session_portal_source_dependencies(
339    inputs: &PortalBindingContext<'_>,
340    source: &SourcePlan,
341    dependencies: &mut SessionPortalTableDependencies,
342    visiting_views: &mut std::collections::BTreeSet<String>,
343    visiting_routines: &mut std::collections::BTreeSet<String>,
344) -> Result<(), SQLError> {
345    match source {
346        SourcePlan::Table {
347            name,
348            include_descendants,
349            ..
350        } => collect_session_portal_relation_dependencies(
351            inputs,
352            name,
353            *include_descendants,
354            dependencies,
355            visiting_views,
356            visiting_routines,
357        ),
358        SourcePlan::Join { left, right, .. } => {
359            collect_session_portal_source_dependencies(
360                inputs,
361                left,
362                dependencies,
363                visiting_views,
364                visiting_routines,
365            )?;
366            collect_session_portal_source_dependencies(
367                inputs,
368                right,
369                dependencies,
370                visiting_views,
371                visiting_routines,
372            )
373        }
374        SourcePlan::Subquery { body, .. } => collect_session_portal_query_dependencies(
375            inputs,
376            body,
377            dependencies,
378            visiting_views,
379            visiting_routines,
380        ),
381        SourcePlan::Function {
382            name,
383            binding,
384            relations,
385            args,
386            ..
387        } => {
388            collect_graph_function_dependency(name, binding.as_ref(), args, dependencies);
389            collect_session_portal_function_dependencies(
390                inputs,
391                name,
392                binding.as_ref(),
393                relations.as_ref(),
394                dependencies,
395                visiting_views,
396                visiting_routines,
397            )
398        }
399        SourcePlan::FunctionGroup { functions, .. } => {
400            for function in functions {
401                collect_graph_function_dependency(
402                    &function.name,
403                    function.binding.as_ref(),
404                    &function.args,
405                    dependencies,
406                );
407                collect_session_portal_function_dependencies(
408                    inputs,
409                    &function.name,
410                    function.binding.as_ref(),
411                    function.relations.as_ref(),
412                    dependencies,
413                    visiting_views,
414                    visiting_routines,
415                )?;
416            }
417            Ok(())
418        }
419        SourcePlan::Values { .. } => Ok(()),
420    }
421}
422
423pub fn collect_session_portal_function_dependencies(
424    inputs: &PortalBindingContext<'_>,
425    name: &str,
426    binding: Option<&crate::ast::FunctionBinding>,
427    relations: Option<&crate::ast::OperatorJoinRelations>,
428    dependencies: &mut SessionPortalTableDependencies,
429    visiting_views: &mut std::collections::BTreeSet<String>,
430    visiting_routines: &mut std::collections::BTreeSet<String>,
431) -> Result<(), SQLError> {
432    if let Some(relations) = relations {
433        for relation in [&relations.left, &relations.right] {
434            collect_session_portal_function_relation_dependency(inputs, relation, dependencies)?;
435        }
436    }
437    collect_session_portal_routine_dependencies(
438        inputs,
439        name,
440        binding,
441        dependencies,
442        visiting_views,
443        visiting_routines,
444    )
445}
446
447pub fn collect_session_portal_function_relation_dependency(
448    inputs: &PortalBindingContext<'_>,
449    name: &str,
450    dependencies: &mut SessionPortalTableDependencies,
451) -> Result<(), SQLError> {
452    let Some(table) = inputs
453        .catalog
454        .try_resolve_table_name(name)
455        .map_err(|error| {
456            SQLError::Internal(format!(
457                "resolve cursor table-function relation `{name}`: {error}"
458            ))
459        })?
460    else {
461        return Ok(());
462    };
463    dependencies.insert(
464        uqa_core::RelationIdentity::from_legacy_name(&table).map_err(|error| {
465            SQLError::Internal(format!(
466                "resolve cursor table-function relation identity `{table}`: {error}"
467            ))
468        })?,
469    );
470    Ok(())
471}
472
473pub fn collect_session_portal_routine_dependencies(
474    inputs: &PortalBindingContext<'_>,
475    name: &str,
476    binding: Option<&crate::ast::FunctionBinding>,
477    dependencies: &mut SessionPortalTableDependencies,
478    visiting_views: &mut std::collections::BTreeSet<String>,
479    visiting_routines: &mut std::collections::BTreeSet<String>,
480) -> Result<(), SQLError> {
481    if binding.is_some_and(|binding| binding.builtin) {
482        return Ok(());
483    }
484    let overloads = match binding {
485        Some(binding) => inputs
486            .routines
487            .lookup_bound_sql_functions_by_binding(binding),
488        None => inputs
489            .routines
490            .lookup_visible_sql_functions_for_analysis(name)?,
491    };
492    let Some(overloads) = overloads else {
493        return Ok(());
494    };
495    for function in overloads {
496        if function.def.is_procedure
497            || binding.is_some_and(|binding| {
498                crate::routines::routine_signature_types(&function.def) != binding.argument_types
499            })
500        {
501            continue;
502        }
503        let signature = crate::routines::routine_signature_types(&function.def).join(",");
504        let key = format!("{}({signature})", function.def.name);
505        if !visiting_routines.insert(key.clone()) {
506            continue;
507        }
508        match &function.compiled {
509            crate::routines::CompiledFunctionBody::SQL(plans) => {
510                for plan in plans {
511                    match plan {
512                        crate::plan::UnifiedPlan::Query(query) => {
513                            collect_session_portal_query_dependencies(
514                                inputs,
515                                query,
516                                dependencies,
517                                visiting_views,
518                                visiting_routines,
519                            )?;
520                        }
521                        crate::plan::UnifiedPlan::Command(_) => {
522                            *dependencies = SessionPortalTableDependencies::all();
523                        }
524                    }
525                    if dependencies.is_all() {
526                        break;
527                    }
528                }
529            }
530            crate::routines::CompiledFunctionBody::PLpgSQL(_) => {
531                *dependencies = SessionPortalTableDependencies::all();
532            }
533        }
534        visiting_routines.remove(&key);
535        if dependencies.is_all() {
536            break;
537        }
538    }
539    Ok(())
540}