Skip to main content

uqa_sql/retrieval/
mod.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL retrieval argument binding and lowering to runtime-independent expressions.
8
9mod binding;
10mod calls;
11mod constants;
12mod description;
13mod fusion;
14mod graph;
15mod ir;
16mod joins;
17mod predicates;
18use crate::semantics::graph_functions::{
19    default_graph_name as default_operator_graph, GraphNameCatalog,
20};
21use crate::{ast::BinaryOp, SQLError, SQLParam, ScalarExpr};
22pub use binding::{lower_sql_function_bound, lower_where_bound};
23pub use description::{describe_vector_call, VectorCallDescription};
24pub use ir::{AttentionSpec, MultiStageEntry, RetrievalExpr, TextScoringMode};
25pub use joins::lower_operator_join_table_function;
26use std::collections::BTreeSet;
27use uqa_core::{
28    retrieval::{Direction as DeepGraphDirection, ExternalPriorMode, GatingSpec, MultiStageCutoff},
29    Predicate, Value,
30};
31type BindingResult<T> = Result<T, SQLError>;
32
33/// Evaluates a scalar with the caller's parameter values and without runtime function hooks.
34pub type ConstantEvaluator<'a> = dyn Fn(&ScalarExpr, &[SQLParam]) -> Result<Value, SQLError> + 'a;
35pub struct RetrievalConstants<'a> {
36    pub params: &'a [SQLParam],
37    pub evaluate: &'a ConstantEvaluator<'a>,
38    /// Whether the relation a predicate filters declares a column of the given name. A predicate on an engine pseudo column such as `_doc_id` filters a stored field only where the relation declares one; elsewhere it stays relational, where the column carries each row's value.
39    pub stores: &'a dyn Fn(&str) -> bool,
40}
41impl RetrievalConstants<'_> {
42    fn without_parameters(&self) -> RetrievalConstants<'_> {
43        RetrievalConstants {
44            params: &[],
45            evaluate: self.evaluate,
46            stores: self.stores,
47        }
48    }
49}
50
51/// Whether `expression` names an engine pseudo column, whose predicates lower according to the columns the relation declares.
52#[must_use]
53pub fn names_engine_pseudo_column(expression: &ScalarExpr) -> bool {
54    let mut names = false;
55    expression.visit(&mut |node| {
56        if let ScalarExpr::Column(column) | ScalarExpr::QualifiedColumn { column, .. } = node {
57            names |= crate::semantics::is_engine_pseudo_column(column);
58        }
59    });
60    names
61}
62/// Runtime scalar evaluation and graph catalog access; SQL owns recursion and validation order.
63pub trait RetrievalArguments: GraphNameCatalog {
64    fn evaluate_argument(
65        &self,
66        expression: &ScalarExpr,
67        params: &[SQLParam],
68    ) -> Result<Value, SQLError>;
69}
70
71use calls::{
72    bind_operator_argument, checked_retrieval_call_tree_present, lower_bayesian_match_with_prior,
73    lower_calibrated_vector_match, lower_multi_field_match, lower_operator_arg, lower_signal_arg,
74    lower_staged_retrieval, try_lower_fts_match, try_lower_knn_match, try_lower_text_match,
75    validate_checked_retrieval_call_tree, validate_operator_function_arity,
76    validate_probability_signal_contract,
77};
78use constants::{
79    const_bool, const_f64, const_f64_vector, const_gating, const_optional_string, const_string,
80    const_temporal_bound, const_usize, const_value, const_vector, named_arg_expr,
81};
82use fusion::{
83    lower_bayesian_evidence_fusion, lower_learned_fusion, lower_positive_evidence_pool,
84    try_lower_attention_fusion,
85};
86use graph::lower_graph_function;
87use predicates::{column_name, lower_comparison, lower_document_boolean, lower_function};
88
89enum OptionalStringConstant {
90    Null,
91    Value(String),
92}
93impl OptionalStringConstant {
94    fn into_option(self) -> Option<String> {
95        match self {
96            Self::Null => None,
97            Self::Value(value) => Some(value),
98        }
99    }
100}
101
102/// Lower representable SQL predicates using the supplied constant evaluator. Unsupported scalar shapes remain relational predicates.
103pub fn lower_where(expr: &ScalarExpr, constants: &RetrievalConstants<'_>) -> Option<RetrievalExpr> {
104    match expr {
105        ScalarExpr::And(parts) => {
106            let mut out: Vec<RetrievalExpr> = Vec::with_capacity(parts.len());
107            for p in parts {
108                out.push(lower_where(p, constants)?);
109            }
110            Some(lower_document_boolean(out, false))
111        }
112        ScalarExpr::Or(parts) => {
113            let mut out: Vec<RetrievalExpr> = Vec::with_capacity(parts.len());
114            for p in parts {
115                out.push(lower_where(p, constants)?);
116            }
117            Some(lower_document_boolean(out, true))
118        }
119        // Complement is only sound when the inner predicate cannot be
120        // NULL for any row (search functions, IS NULL tests). Column
121        // comparisons under NOT fall through to the wildcard `None`
122        // and keep three-valued semantics through the row-evaluator
123        // relational evaluation: `NOT (col = 5)` must not match rows whose `col`
124        // is NULL.
125        ScalarExpr::Not(inner) if crate::semantics::expr_is_null_free(inner) => Some(
126            RetrievalExpr::Complement(Box::new(lower_where(inner, constants)?)),
127        ),
128        ScalarExpr::Func { name, args, .. } => lower_function(name, args, constants),
129        ScalarExpr::Binary { op, lhs, rhs } => lower_comparison(*op, lhs, rhs, constants),
130        ScalarExpr::IsNull { expr, negated } => {
131            let field = predicates::filter_field(expr, constants)?;
132            let predicate = if *negated {
133                Predicate::IsNotNull
134            } else {
135                Predicate::IsNull
136            };
137            Some(RetrievalExpr::Filter {
138                field,
139                predicate,
140                source: None,
141            })
142        }
143        ScalarExpr::Between { expr, low, high } => {
144            let field = predicates::filter_field(expr, constants)?;
145            let lo = const_value(low, constants)?;
146            let hi = const_value(high, constants)?;
147            Some(RetrievalExpr::Filter {
148                field,
149                predicate: Predicate::Between { low: lo, high: hi },
150                source: None,
151            })
152        }
153        ScalarExpr::InList {
154            expr,
155            list,
156            negated,
157        } => {
158            let field = predicates::filter_field(expr, constants)?;
159            let mut set: BTreeSet<Value> = BTreeSet::new();
160            let mut has_null = false;
161            for v in list {
162                let value = const_value(v, constants)?;
163                if matches!(value, Value::Null) {
164                    has_null = true;
165                    continue;
166                }
167                set.insert(value);
168            }
169            if *negated {
170                // `col NOT IN (...)`: a NULL in the list means no row
171                // can ever satisfy it; otherwise complement the match
172                // set but keep NULL rows excluded (three-valued NOT).
173                if has_null {
174                    return Some(RetrievalExpr::Empty);
175                }
176                let filter = RetrievalExpr::Filter {
177                    field: field.clone(),
178                    predicate: Predicate::InSet(set),
179                    source: None,
180                };
181                let not_null = RetrievalExpr::Filter {
182                    field,
183                    predicate: Predicate::IsNotNull,
184                    source: None,
185                };
186                return Some(RetrievalExpr::Intersect(vec![
187                    RetrievalExpr::Complement(Box::new(filter)),
188                    not_null,
189                ]));
190            }
191            Some(RetrievalExpr::Filter {
192                field,
193                predicate: Predicate::InSet(set),
194                source: None,
195            })
196        }
197        _ => None,
198    }
199}