Skip to main content

uqa_sql/retrieval/
joins.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Relation-producing operator join validation and logical operands.
8
9use super::{
10    const_f64, const_optional_string, const_string, lower_where_bound, BindingResult,
11    RetrievalArguments, RetrievalConstants, RetrievalExpr, SQLError, ScalarExpr,
12};
13
14fn lower_join_operand(
15    source: &dyn RetrievalArguments,
16    expression: &ScalarExpr,
17    constants: &RetrievalConstants<'_>,
18    function_name: &str,
19) -> BindingResult<RetrievalExpr> {
20    lower_where_bound(source, expression, constants)?.ok_or_else(|| {
21        SQLError::TypeMismatch(format!(
22            "{function_name} operand cannot be represented by the operator IR"
23        ))
24    })
25}
26
27fn const_join_threshold(
28    expression: &ScalarExpr,
29    constants: &RetrievalConstants<'_>,
30    function_name: &str,
31    minimum: f64,
32    maximum: f64,
33) -> BindingResult<f64> {
34    let threshold = const_f64(expression, constants).ok_or_else(|| {
35        SQLError::TypeMismatch(format!(
36            "{function_name}.threshold must be a constant number"
37        ))
38    })?;
39    if !threshold.is_finite() || !(minimum..=maximum).contains(&threshold) {
40        return Err(SQLError::TypeMismatch(format!(
41            "{function_name}.threshold must be finite and in [{minimum}, {maximum}], got {threshold}"
42        )));
43    }
44    Ok(threshold)
45}
46
47pub fn lower_operator_join_table_function(
48    source: &dyn RetrievalArguments,
49    name: &str,
50    relations: Option<&crate::ast::OperatorJoinRelations>,
51    args: &[ScalarExpr],
52    constants: &RetrievalConstants<'_>,
53) -> BindingResult<(crate::ast::OperatorJoinRelations, RetrievalExpr)> {
54    let expected = match name {
55        "text_similarity_join" | "vector_similarity_join" => 5,
56        "graph_join" => 6,
57        "hybrid_join" | "cross_paradigm_join" => 4,
58        _ => {
59            return Err(SQLError::Unsupported(format!(
60                "operator join table function `{name}`"
61            )))
62        }
63    };
64    let relations = relations.ok_or_else(|| {
65        SQLError::TypeMismatch(format!("{name} requires left and right table identifiers"))
66    })?;
67    let actual = args.len() + 2;
68    if actual != expected {
69        return Err(SQLError::BadArity {
70            name: name.to_string(),
71            expected: expected.to_string(),
72            actual,
73        });
74    }
75    let left = lower_join_operand(source, &args[0], constants, name)?;
76    let right = lower_join_operand(source, &args[1], constants, name)?;
77    let tree = match name {
78        "text_similarity_join" => RetrievalExpr::TextSimilarityJoin {
79            left: Box::new(left),
80            right: Box::new(right),
81            threshold: const_join_threshold(&args[2], constants, "text_similarity_join", 0.0, 1.0)?,
82        },
83        "vector_similarity_join" => RetrievalExpr::VectorSimilarityJoin {
84            left: Box::new(left),
85            right: Box::new(right),
86            threshold: const_join_threshold(
87                &args[2],
88                constants,
89                "vector_similarity_join",
90                -1.0,
91                1.0,
92            )?,
93        },
94        "graph_join" => RetrievalExpr::GraphJoin {
95            left: Box::new(left),
96            right: Box::new(right),
97            label: const_optional_string(&args[2], constants)
98                .ok_or_else(|| {
99                    SQLError::TypeMismatch(
100                        "graph_join.label must be a constant string or NULL".into(),
101                    )
102                })?
103                .into_option(),
104            graph: const_string(&args[3], constants).ok_or_else(|| {
105                SQLError::TypeMismatch("graph_join.graph must be a constant string".into())
106            })?,
107        },
108        "hybrid_join" => RetrievalExpr::HybridJoin {
109            left: Box::new(left),
110            right: Box::new(right),
111        },
112        "cross_paradigm_join" => RetrievalExpr::CrossParadigmJoin {
113            left: Box::new(left),
114            right: Box::new(right),
115        },
116        _ => unreachable!("operator join name validated above"),
117    };
118    Ok((relations.clone(), tree))
119}