1use 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}