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(
68        crate::ScalarExpr::Literal(uqa_core::Value::Str(graph))
69        | crate::ScalarExpr::TypedLiteral {
70            value: uqa_core::Value::Str(graph),
71            parameter_index: None,
72            ..
73        },
74    ) = args.get(argument)
75    {
76        // Preparation coerces string constants to their selected SQL type.
77        // Those datums still name one graph; parameters and expressions do not.
78        dependencies.insert_graph(graph.clone());
79    } else {
80        // Do not evaluate expressions or volatile functions at DECLARE.
81        // A parameter, expression, or default graph can select any graph.
82        dependencies.graphs = None;
83    }
84}
85
86pub fn session_portal_table_dependencies(
87    inputs: &PortalBindingContext<'_>,
88    query: &QueryPlan,
89) -> Result<SessionPortalTableDependencies, SQLError> {
90    let mut dependencies = SessionPortalTableDependencies::empty();
91    collect_session_portal_query_dependencies(
92        inputs,
93        query,
94        &mut dependencies,
95        &mut std::collections::BTreeSet::new(),
96        &mut std::collections::BTreeSet::new(),
97    )?;
98    Ok(dependencies)
99}
100
101pub fn collect_session_portal_query_dependencies(
102    inputs: &PortalBindingContext<'_>,
103    query: &QueryPlan,
104    dependencies: &mut SessionPortalTableDependencies,
105    visiting_views: &mut std::collections::BTreeSet<String>,
106    visiting_routines: &mut std::collections::BTreeSet<String>,
107) -> Result<(), SQLError> {
108    if dependencies.is_all() {
109        return Ok(());
110    }
111    for cte in &query.ctes {
112        collect_session_portal_cte_dependencies(
113            inputs,
114            &cte.body,
115            dependencies,
116            visiting_views,
117            visiting_routines,
118        )?;
119    }
120    collect_session_portal_relational_dependencies(
121        inputs,
122        &query.root,
123        dependencies,
124        visiting_views,
125        visiting_routines,
126    )?;
127
128    let mut plan = crate::plan::UnifiedPlan::Query(Box::new(query.clone()));
129    let mut routines = Vec::new();
130    plan.rewrite_scalar_expressions(&mut |expression| {
131        if let crate::ScalarExpr::Func {
132            name,
133            binding,
134            args,
135            ..
136        } = expression
137        {
138            collect_graph_function_dependency(name, binding.as_ref(), args, dependencies);
139            routines.push((name.clone(), binding.clone()));
140        }
141    });
142    for (name, binding) in routines {
143        collect_session_portal_routine_dependencies(
144            inputs,
145            &name,
146            binding.as_ref(),
147            dependencies,
148            visiting_views,
149            visiting_routines,
150        )?;
151        if dependencies.is_all() {
152            break;
153        }
154    }
155    Ok(())
156}
157
158fn collect_session_portal_cte_dependencies(
159    inputs: &PortalBindingContext<'_>,
160    body: &crate::plan::CtePlanBody,
161    dependencies: &mut SessionPortalTableDependencies,
162    visiting_views: &mut std::collections::BTreeSet<String>,
163    visiting_routines: &mut std::collections::BTreeSet<String>,
164) -> Result<(), SQLError> {
165    match body {
166        crate::plan::CtePlanBody::Query(query) => collect_session_portal_query_dependencies(
167            inputs,
168            query,
169            dependencies,
170            visiting_views,
171            visiting_routines,
172        ),
173        crate::plan::CtePlanBody::Command(command) => {
174            if let Some(target) = command.mutation_target() {
175                collect_session_portal_relation_dependencies(
176                    inputs,
177                    target,
178                    true,
179                    dependencies,
180                    visiting_views,
181                    visiting_routines,
182                )?;
183            }
184            for cte in command.ctes() {
185                collect_session_portal_cte_dependencies(
186                    inputs,
187                    &cte.body,
188                    dependencies,
189                    visiting_views,
190                    visiting_routines,
191                )?;
192            }
193            for query in command.query_inputs() {
194                collect_session_portal_query_dependencies(
195                    inputs,
196                    query,
197                    dependencies,
198                    visiting_views,
199                    visiting_routines,
200                )?;
201            }
202            if let Some(source) = command.source_input() {
203                collect_session_portal_source_dependencies(
204                    inputs,
205                    source,
206                    dependencies,
207                    visiting_views,
208                    visiting_routines,
209                )?;
210            }
211            Ok(())
212        }
213    }
214}
215
216pub fn collect_session_portal_relational_dependencies(
217    inputs: &PortalBindingContext<'_>,
218    plan: &RelationalPlan,
219    dependencies: &mut SessionPortalTableDependencies,
220    visiting_views: &mut std::collections::BTreeSet<String>,
221    visiting_routines: &mut std::collections::BTreeSet<String>,
222) -> Result<(), SQLError> {
223    match plan {
224        RelationalPlan::QueryBlock(block) => {
225            if let Some(source) = block.from.as_ref() {
226                collect_session_portal_source_dependencies(
227                    inputs,
228                    source,
229                    dependencies,
230                    visiting_views,
231                    visiting_routines,
232                )?;
233            }
234            for subquery in &block.subqueries {
235                collect_session_portal_query_dependencies(
236                    inputs,
237                    subquery,
238                    dependencies,
239                    visiting_views,
240                    visiting_routines,
241                )?;
242            }
243        }
244        RelationalPlan::SetOp {
245            left,
246            right,
247            subqueries,
248            ..
249        } => {
250            collect_session_portal_query_dependencies(
251                inputs,
252                left,
253                dependencies,
254                visiting_views,
255                visiting_routines,
256            )?;
257            collect_session_portal_query_dependencies(
258                inputs,
259                right,
260                dependencies,
261                visiting_views,
262                visiting_routines,
263            )?;
264            for subquery in subqueries {
265                collect_session_portal_query_dependencies(
266                    inputs,
267                    subquery,
268                    dependencies,
269                    visiting_views,
270                    visiting_routines,
271                )?;
272            }
273        }
274        RelationalPlan::Values { subqueries, .. } => {
275            for subquery in subqueries {
276                collect_session_portal_query_dependencies(
277                    inputs,
278                    subquery,
279                    dependencies,
280                    visiting_views,
281                    visiting_routines,
282                )?;
283            }
284        }
285    }
286    Ok(())
287}
288
289fn collect_session_portal_relation_dependencies(
290    inputs: &PortalBindingContext<'_>,
291    name: &str,
292    include_descendants: bool,
293    dependencies: &mut SessionPortalTableDependencies,
294    visiting_views: &mut std::collections::BTreeSet<String>,
295    visiting_routines: &mut std::collections::BTreeSet<String>,
296) -> Result<(), SQLError> {
297    if let Some(table) = inputs
298        .catalog
299        .try_resolve_table_name(name)
300        .map_err(|error| {
301            SQLError::Internal(format!(
302                "resolve cursor dependency relation `{name}`: {error}"
303            ))
304        })?
305    {
306        for table in inputs
307            .catalog
308            .hierarchy_scan_tables(&table, include_descendants)?
309        {
310            dependencies.insert(
311                uqa_core::RelationIdentity::from_legacy_name(&table).map_err(|error| {
312                    SQLError::Internal(format!(
313                        "resolve cursor dependency identity `{table}`: {error}"
314                    ))
315                })?,
316            );
317        }
318        return Ok(());
319    }
320    if crate::binding::view_dependencies::canonical_virtual_relation_reference(name).is_some() {
321        dependencies.tables = None;
322        dependencies.graph_catalog = true;
323        return Ok(());
324    }
325    if let Some(relation) = inputs.catalog.resolve_age_label_relation_name(name)? {
326        let relation =
327            uqa_core::RelationIdentity::from_legacy_name(&relation).map_err(SQLError::Internal)?;
328        dependencies.insert_graph(relation.schema);
329        return Ok(());
330    }
331    let key = name.to_ascii_lowercase();
332    if !visiting_views.insert(key.clone()) {
333        return Ok(());
334    }
335    if let Some(view) = inputs.catalog.view_plan(name)? {
336        collect_session_portal_query_dependencies(
337            inputs,
338            &view,
339            dependencies,
340            visiting_views,
341            visiting_routines,
342        )?;
343    }
344    visiting_views.remove(&key);
345    Ok(())
346}
347
348pub fn collect_session_portal_source_dependencies(
349    inputs: &PortalBindingContext<'_>,
350    source: &SourcePlan,
351    dependencies: &mut SessionPortalTableDependencies,
352    visiting_views: &mut std::collections::BTreeSet<String>,
353    visiting_routines: &mut std::collections::BTreeSet<String>,
354) -> Result<(), SQLError> {
355    match source {
356        SourcePlan::Table {
357            name,
358            include_descendants,
359            ..
360        } => collect_session_portal_relation_dependencies(
361            inputs,
362            name,
363            *include_descendants,
364            dependencies,
365            visiting_views,
366            visiting_routines,
367        ),
368        SourcePlan::Join { left, right, .. } => {
369            collect_session_portal_source_dependencies(
370                inputs,
371                left,
372                dependencies,
373                visiting_views,
374                visiting_routines,
375            )?;
376            collect_session_portal_source_dependencies(
377                inputs,
378                right,
379                dependencies,
380                visiting_views,
381                visiting_routines,
382            )
383        }
384        SourcePlan::Subquery { body, .. } => collect_session_portal_query_dependencies(
385            inputs,
386            body,
387            dependencies,
388            visiting_views,
389            visiting_routines,
390        ),
391        SourcePlan::Function {
392            name,
393            binding,
394            relations,
395            args,
396            ..
397        } => {
398            collect_graph_function_dependency(name, binding.as_ref(), args, dependencies);
399            collect_session_portal_function_dependencies(
400                inputs,
401                name,
402                binding.as_ref(),
403                relations.as_ref(),
404                dependencies,
405                visiting_views,
406                visiting_routines,
407            )
408        }
409        SourcePlan::FunctionGroup { functions, .. } => {
410            for function in functions {
411                collect_graph_function_dependency(
412                    &function.name,
413                    function.binding.as_ref(),
414                    &function.args,
415                    dependencies,
416                );
417                collect_session_portal_function_dependencies(
418                    inputs,
419                    &function.name,
420                    function.binding.as_ref(),
421                    function.relations.as_ref(),
422                    dependencies,
423                    visiting_views,
424                    visiting_routines,
425                )?;
426            }
427            Ok(())
428        }
429        SourcePlan::Values { .. } => Ok(()),
430    }
431}
432
433pub fn collect_session_portal_function_dependencies(
434    inputs: &PortalBindingContext<'_>,
435    name: &str,
436    binding: Option<&crate::ast::FunctionBinding>,
437    relations: Option<&crate::ast::OperatorJoinRelations>,
438    dependencies: &mut SessionPortalTableDependencies,
439    visiting_views: &mut std::collections::BTreeSet<String>,
440    visiting_routines: &mut std::collections::BTreeSet<String>,
441) -> Result<(), SQLError> {
442    if let Some(relations) = relations {
443        for relation in [&relations.left, &relations.right] {
444            collect_session_portal_function_relation_dependency(inputs, relation, dependencies)?;
445        }
446    }
447    collect_session_portal_routine_dependencies(
448        inputs,
449        name,
450        binding,
451        dependencies,
452        visiting_views,
453        visiting_routines,
454    )
455}
456
457pub fn collect_session_portal_function_relation_dependency(
458    inputs: &PortalBindingContext<'_>,
459    name: &str,
460    dependencies: &mut SessionPortalTableDependencies,
461) -> Result<(), SQLError> {
462    let Some(table) = inputs
463        .catalog
464        .try_resolve_table_name(name)
465        .map_err(|error| {
466            SQLError::Internal(format!(
467                "resolve cursor table-function relation `{name}`: {error}"
468            ))
469        })?
470    else {
471        return Ok(());
472    };
473    dependencies.insert(
474        uqa_core::RelationIdentity::from_legacy_name(&table).map_err(|error| {
475            SQLError::Internal(format!(
476                "resolve cursor table-function relation identity `{table}`: {error}"
477            ))
478        })?,
479    );
480    Ok(())
481}
482
483pub fn collect_session_portal_routine_dependencies(
484    inputs: &PortalBindingContext<'_>,
485    name: &str,
486    binding: Option<&crate::ast::FunctionBinding>,
487    dependencies: &mut SessionPortalTableDependencies,
488    visiting_views: &mut std::collections::BTreeSet<String>,
489    visiting_routines: &mut std::collections::BTreeSet<String>,
490) -> Result<(), SQLError> {
491    if binding.is_some_and(|binding| binding.builtin) {
492        return Ok(());
493    }
494    let overloads = match binding {
495        Some(binding) => inputs
496            .routines
497            .lookup_bound_sql_functions_by_binding(binding),
498        None => inputs
499            .routines
500            .lookup_visible_sql_functions_for_analysis(name)?,
501    };
502    let Some(overloads) = overloads else {
503        return Ok(());
504    };
505    for function in overloads {
506        if function.def.is_procedure
507            || binding.is_some_and(|binding| {
508                crate::routines::routine_signature_types(&function.def) != binding.argument_types
509            })
510        {
511            continue;
512        }
513        let signature = crate::routines::routine_signature_types(&function.def).join(",");
514        let key = format!("{}({signature})", function.def.name);
515        if !visiting_routines.insert(key.clone()) {
516            continue;
517        }
518        match crate::routines::analyzable_routine_body(inputs.routines, &function)?.as_deref() {
519            Some(crate::routines::CompiledFunctionBody::SQL(plans)) => {
520                for plan in plans {
521                    match plan {
522                        crate::plan::UnifiedPlan::Query(query) => {
523                            collect_session_portal_query_dependencies(
524                                inputs,
525                                query,
526                                dependencies,
527                                visiting_views,
528                                visiting_routines,
529                            )?;
530                        }
531                        crate::plan::UnifiedPlan::Command(_) => {
532                            *dependencies = SessionPortalTableDependencies::all();
533                        }
534                    }
535                    if dependencies.is_all() {
536                        break;
537                    }
538                }
539            }
540            Some(crate::routines::CompiledFunctionBody::PLpgSQL(_)) => {
541                *dependencies = SessionPortalTableDependencies::all();
542            }
543            // Procedural fragments may be valid only under the settings in
544            // effect when execution reaches them. Unknown dependencies retain
545            // all tables without parsing or caching those fragments here.
546            None if function.def.language == "plpgsql" => {
547                *dependencies = SessionPortalTableDependencies::all();
548            }
549            None => {}
550        }
551        visiting_routines.remove(&key);
552        if dependencies.is_all() {
553            break;
554        }
555    }
556    Ok(())
557}