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)
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}