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