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)
252        | ScalarExpr::Cast { expr: inner, .. }
253        | ScalarExpr::Collated { expr: inner, .. } => {
254            expression_is_null_with_side(routines, inner, side, other, params)
255        }
256        ScalarExpr::Between { expr, low, high } => {
257            Ok(
258                expression_is_null_with_side(routines, expr, side, other, params)?
259                    || (expression_is_null_with_side(routines, low, side, other, params)?
260                        && expression_is_null_with_side(routines, high, side, other, params)?),
261            )
262        }
263        ScalarExpr::InList { expr, .. } => {
264            expression_is_null_with_side(routines, expr, side, other, params)
265        }
266        ScalarExpr::Func {
267            name,
268            binding,
269            args,
270            ..
271        } => {
272            if !scalar_function_is_strict(
273                routines,
274                name,
275                binding.as_ref(),
276                args,
277                side,
278                other,
279                params,
280            )? {
281                return Ok(false);
282            }
283            for argument in crate::scalar_call_arguments(args)? {
284                if expression_is_null_with_side(routines, argument.value, side, other, params)? {
285                    return Ok(true);
286                }
287            }
288            Ok(false)
289        }
290        _ => Ok(false),
291    }
292}
293
294fn scalar_function_is_strict(
295    routines: &dyn RoutineResolution,
296    name: &str,
297    binding: Option<&crate::ast::FunctionBinding>,
298    args: &[ScalarExpr],
299    side: &RowSchema,
300    other: &RowSchema,
301    params: &[SQLParam],
302) -> Result<bool, SQLError> {
303    let runtime_name = name
304        .strip_prefix("pg_catalog.")
305        .unwrap_or(name)
306        .to_ascii_lowercase();
307    if routines.has_registered_scalar_function(&runtime_name) {
308        return Ok(false);
309    }
310    if let Some(strict) = crate::expr::bound_scalar_function_strictness(name, binding, args.len()) {
311        return Ok(strict);
312    }
313    let schema = RowSchema::join(side, other, std::iter::empty::<String>());
314    let (argument_names, argument_types, explicit_variadic) =
315        crate::type_resolution::function_call_argument_signature(
316            args,
317            &schema,
318            params,
319            Some(routines),
320        )?;
321    if binding.is_none() && routines.lookup_visible_sql_functions(name)?.is_none() {
322        return Ok(false);
323    }
324    Ok(routines
325        .resolve_static_sql_function(
326            name,
327            binding,
328            &argument_names,
329            &argument_types,
330            explicit_variadic,
331        )?
332        .is_some_and(|function| function.def.strict))
333}
334
335fn negate_truth_values(values: u8) -> u8 {
336    (u8::from(values & TRUTH_FALSE != 0) * TRUTH_TRUE)
337        | (u8::from(values & TRUTH_TRUE != 0) * TRUTH_FALSE)
338        | (values & TRUTH_NULL)
339}
340
341fn combine_truth_values(left: u8, right: u8, and: bool) -> u8 {
342    let mut output = 0;
343    for lhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
344        if left & lhs == 0 {
345            continue;
346        }
347        for rhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
348            if right & rhs == 0 {
349                continue;
350            }
351            output |= if and {
352                match (lhs, rhs) {
353                    (TRUTH_FALSE, _) | (_, TRUTH_FALSE) => TRUTH_FALSE,
354                    (TRUTH_TRUE, TRUTH_TRUE) => TRUTH_TRUE,
355                    _ => TRUTH_NULL,
356                }
357            } else {
358                match (lhs, rhs) {
359                    (TRUTH_TRUE, _) | (_, TRUTH_TRUE) => TRUTH_TRUE,
360                    (TRUTH_FALSE, TRUTH_FALSE) => TRUTH_FALSE,
361                    _ => TRUTH_NULL,
362                }
363            };
364        }
365    }
366    output
367}