Skip to main content

uqa_sql/semantics/locking/
null_rejection.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Determine nullable join sides from SQL predicates and strict function metadata.
8
9use crate::semantics::{join_alias_input_schemas, resolve_join_using};
10use crate::{
11    binding::{bind_source_plan_schema, context::BindingContext},
12    plan::SourcePlan,
13    routines::RoutineResolution,
14    RowSchema, SQLError, SQLParam, ScalarExpr,
15};
16use uqa_core::Value;
17
18pub fn reduce_null_rejected_outer_joins_to_fixpoint(
19    routines: &dyn RoutineResolution,
20    source: &mut SourcePlan,
21    predicate: Option<&ScalarExpr>,
22    params: &[SQLParam],
23    context: &BindingContext<'_>,
24) -> Result<(), SQLError> {
25    loop {
26        let mut qualifications = predicate.iter().map(|expr| (*expr).clone()).collect();
27        collect_inner_join_qualifications(source, &mut qualifications);
28        let mut changed = false;
29        for qualification in &qualifications {
30            changed |= reduce_null_rejected_outer_joins(
31                routines,
32                source,
33                qualification,
34                params,
35                context,
36                None,
37            )?;
38        }
39        if !changed {
40            return Ok(());
41        }
42    }
43}
44
45fn collect_inner_join_qualifications(source: &SourcePlan, output: &mut Vec<ScalarExpr>) {
46    let SourcePlan::Join {
47        left,
48        right,
49        kind,
50        on,
51        ..
52    } = source
53    else {
54        return;
55    };
56    if !matches!(
57        kind,
58        crate::ast::JoinKind::Inner | crate::ast::JoinKind::Cross
59    ) {
60        return;
61    }
62    if let Some(on) = on {
63        output.push(on.clone());
64    }
65    collect_inner_join_qualifications(left, output);
66    collect_inner_join_qualifications(right, output);
67}
68
69fn reduce_null_rejected_outer_joins(
70    routines: &dyn RoutineResolution,
71    source: &mut SourcePlan,
72    predicate: &ScalarExpr,
73    params: &[SQLParam],
74    context: &BindingContext<'_>,
75    outer: Option<&RowSchema>,
76) -> Result<bool, SQLError> {
77    let SourcePlan::Join {
78        left,
79        right,
80        kind,
81        using,
82        natural,
83        alias,
84        column_aliases,
85        lateral,
86        ..
87    } = source
88    else {
89        return Ok(false);
90    };
91    let left_schema = bind_source_plan_schema(routines, left, params, context, outer)?;
92    let implicit_lateral_function = matches!(
93        right.as_ref(),
94        SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
95    );
96    let right_scope =
97        (*lateral || implicit_lateral_function).then(|| overlay_outer_schema(&left_schema, outer));
98    let right_outer = right_scope.as_ref().or(outer);
99    let right_schema = bind_source_plan_schema(routines, right, params, context, right_outer)?;
100    let resolved_using = resolve_join_using(using.as_ref(), *natural, &left_schema, &right_schema)?;
101    let (left_predicate_schema, right_predicate_schema) = match alias.as_deref() {
102        Some(alias) => join_alias_input_schemas(
103            *kind,
104            &left_schema,
105            &right_schema,
106            resolved_using.as_ref(),
107            alias,
108            column_aliases,
109        )?,
110        None => (left_schema.clone(), right_schema.clone()),
111    };
112    let rejects_left = predicate_rejects_null_extended_side(
113        routines,
114        predicate,
115        &left_predicate_schema,
116        &right_predicate_schema,
117        params,
118    )?;
119    let rejects_right = predicate_rejects_null_extended_side(
120        routines,
121        predicate,
122        &right_predicate_schema,
123        &left_predicate_schema,
124        params,
125    )?;
126    let reduced = match (*kind, rejects_left, rejects_right) {
127        (crate::ast::JoinKind::Left, _, true) | (crate::ast::JoinKind::Right, true, _) => {
128            crate::ast::JoinKind::Inner
129        }
130        (crate::ast::JoinKind::Full, true, true) => crate::ast::JoinKind::Inner,
131        (crate::ast::JoinKind::Full, true, false) => crate::ast::JoinKind::Left,
132        (crate::ast::JoinKind::Full, false, true) => crate::ast::JoinKind::Right,
133        (kind, _, _) => kind,
134    };
135    let mut changed = reduced != *kind;
136    *kind = reduced;
137    changed |= reduce_null_rejected_outer_joins(routines, left, predicate, params, context, outer)?;
138    changed |=
139        reduce_null_rejected_outer_joins(routines, right, predicate, params, context, right_outer)?;
140    Ok(changed)
141}
142
143fn overlay_outer_schema(current: &RowSchema, outer: Option<&RowSchema>) -> RowSchema {
144    let Some(outer) = outer else {
145        return current.clone();
146    };
147    let columns = outer
148        .identities()
149        .iter()
150        .enumerate()
151        .map(|(position, identity)| (identity.clone(), outer.column_type(position).cloned()))
152        .collect::<Vec<_>>();
153    RowSchema::with_typed_outer_identities(current, &columns)
154}
155
156const TRUTH_FALSE: u8 = 1;
157const TRUTH_TRUE: u8 = 2;
158const TRUTH_NULL: u8 = 4;
159const TRUTH_ANY: u8 = TRUTH_FALSE | TRUTH_TRUE | TRUTH_NULL;
160
161fn predicate_rejects_null_extended_side(
162    routines: &dyn RoutineResolution,
163    expression: &ScalarExpr,
164    side: &RowSchema,
165    other: &RowSchema,
166    params: &[SQLParam],
167) -> Result<bool, SQLError> {
168    Ok(truth_values_with_null_side(routines, expression, side, other, params)? & TRUTH_TRUE == 0)
169}
170
171fn truth_values_with_null_side(
172    routines: &dyn RoutineResolution,
173    expression: &ScalarExpr,
174    side: &RowSchema,
175    other: &RowSchema,
176    params: &[SQLParam],
177) -> Result<u8, SQLError> {
178    match expression {
179        ScalarExpr::Literal(Value::Bool(value)) => {
180            if *value {
181                Ok(TRUTH_TRUE)
182            } else {
183                Ok(TRUTH_FALSE)
184            }
185        }
186        ScalarExpr::Literal(Value::Null) => Ok(TRUTH_NULL),
187        ScalarExpr::IsNull { expr, negated }
188            if expression_is_null_with_side(routines, expr, side, other, params)? =>
189        {
190            if *negated {
191                Ok(TRUTH_FALSE)
192            } else {
193                Ok(TRUTH_TRUE)
194            }
195        }
196        ScalarExpr::Between { expr, low, high } => {
197            let value_is_null = expression_is_null_with_side(routines, expr, side, other, params)?;
198            let low_is_null = expression_is_null_with_side(routines, low, side, other, params)?;
199            let high_is_null = expression_is_null_with_side(routines, high, side, other, params)?;
200            if value_is_null || (low_is_null && high_is_null) {
201                Ok(TRUTH_NULL)
202            } else if low_is_null || high_is_null {
203                Ok(TRUTH_FALSE | TRUTH_NULL)
204            } else {
205                Ok(TRUTH_ANY)
206            }
207        }
208        expression if expression_is_null_with_side(routines, expression, side, other, params)? => {
209            Ok(TRUTH_NULL)
210        }
211        ScalarExpr::Not(inner) => Ok(negate_truth_values(truth_values_with_null_side(
212            routines, inner, side, other, params,
213        )?)),
214        ScalarExpr::And(items) => items.iter().try_fold(TRUTH_TRUE, |left, right| {
215            Ok(combine_truth_values(
216                left,
217                truth_values_with_null_side(routines, right, side, other, params)?,
218                true,
219            ))
220        }),
221        ScalarExpr::Or(items) => items.iter().try_fold(TRUTH_FALSE, |left, right| {
222            Ok(combine_truth_values(
223                left,
224                truth_values_with_null_side(routines, right, side, other, params)?,
225                false,
226            ))
227        }),
228        _ => Ok(TRUTH_ANY),
229    }
230}
231
232fn expression_is_null_with_side(
233    routines: &dyn RoutineResolution,
234    expression: &ScalarExpr,
235    side: &RowSchema,
236    other: &RowSchema,
237    params: &[SQLParam],
238) -> Result<bool, SQLError> {
239    match expression {
240        ScalarExpr::Literal(Value::Null) => Ok(true),
241        ScalarExpr::Column(column) => Ok(side.unqualified_position(column).is_some()
242            && other.unqualified_position(column).is_none()),
243        ScalarExpr::QualifiedColumn { qualifier, column } => Ok(side
244            .has_qualified_column(qualifier, column)
245            && !other.has_qualified_column(qualifier, column)),
246        ScalarExpr::Binary { lhs, rhs, .. } => Ok(expression_is_null_with_side(
247            routines, lhs, side, other, params,
248        )? || expression_is_null_with_side(
249            routines, rhs, side, other, params,
250        )?),
251        ScalarExpr::UnaryMinus(inner) | ScalarExpr::Cast { expr: inner, .. } => {
252            expression_is_null_with_side(routines, inner, side, other, params)
253        }
254        ScalarExpr::Between { expr, low, high } => {
255            Ok(
256                expression_is_null_with_side(routines, expr, side, other, params)?
257                    || (expression_is_null_with_side(routines, low, side, other, params)?
258                        && expression_is_null_with_side(routines, high, side, other, params)?),
259            )
260        }
261        ScalarExpr::InList { expr, .. } => {
262            expression_is_null_with_side(routines, expr, side, other, params)
263        }
264        ScalarExpr::Func {
265            name,
266            binding,
267            args,
268            ..
269        } => {
270            if !scalar_function_is_strict(
271                routines,
272                name,
273                binding.as_ref(),
274                args,
275                side,
276                other,
277                params,
278            )? {
279                return Ok(false);
280            }
281            for argument in crate::scalar_call_arguments(args)? {
282                if expression_is_null_with_side(routines, argument.value, side, other, params)? {
283                    return Ok(true);
284                }
285            }
286            Ok(false)
287        }
288        _ => Ok(false),
289    }
290}
291
292fn scalar_function_is_strict(
293    routines: &dyn RoutineResolution,
294    name: &str,
295    binding: Option<&crate::ast::FunctionBinding>,
296    args: &[ScalarExpr],
297    side: &RowSchema,
298    other: &RowSchema,
299    params: &[SQLParam],
300) -> Result<bool, SQLError> {
301    let runtime_name = name
302        .strip_prefix("pg_catalog.")
303        .unwrap_or(name)
304        .to_ascii_lowercase();
305    if routines.has_registered_scalar_function(&runtime_name) {
306        return Ok(false);
307    }
308    if let Some(strict) = crate::expr::bound_scalar_function_strictness(name, binding, args.len()) {
309        return Ok(strict);
310    }
311    let schema = RowSchema::join(side, other, std::iter::empty::<String>());
312    let (argument_names, argument_types, explicit_variadic) =
313        crate::type_resolution::function_call_argument_signature(
314            args,
315            &schema,
316            params,
317            Some(routines),
318        )?;
319    if binding.is_none() && routines.lookup_visible_sql_functions(name)?.is_none() {
320        return Ok(false);
321    }
322    Ok(routines
323        .resolve_static_sql_function(
324            name,
325            binding,
326            &argument_names,
327            &argument_types,
328            explicit_variadic,
329        )?
330        .is_some_and(|function| function.def.strict))
331}
332
333fn negate_truth_values(values: u8) -> u8 {
334    (u8::from(values & TRUTH_FALSE != 0) * TRUTH_TRUE)
335        | (u8::from(values & TRUTH_TRUE != 0) * TRUTH_FALSE)
336        | (values & TRUTH_NULL)
337}
338
339fn combine_truth_values(left: u8, right: u8, and: bool) -> u8 {
340    let mut output = 0;
341    for lhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
342        if left & lhs == 0 {
343            continue;
344        }
345        for rhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
346            if right & rhs == 0 {
347                continue;
348            }
349            output |= if and {
350                match (lhs, rhs) {
351                    (TRUTH_FALSE, _) | (_, TRUTH_FALSE) => TRUTH_FALSE,
352                    (TRUTH_TRUE, TRUTH_TRUE) => TRUTH_TRUE,
353                    _ => TRUTH_NULL,
354                }
355            } else {
356                match (lhs, rhs) {
357                    (TRUTH_TRUE, _) | (_, TRUTH_TRUE) => TRUTH_TRUE,
358                    (TRUTH_FALSE, TRUTH_FALSE) => TRUTH_FALSE,
359                    _ => TRUTH_NULL,
360                }
361            };
362        }
363    }
364    output
365}