1use uqa_core::Value;
10use uqa_sql::ast::BinaryOp;
11use uqa_sql::expr::IntegerWidth;
12use uqa_sql::{SQLError, SQLParam};
13
14use crate::{RowSchema, ScalarExpr};
15
16mod compile;
17mod evaluate;
18
19pub struct ProjectedPredicate {
23 expression: ProjectedExpr,
24}
25
26pub(super) enum ProjectedIntPredicate {
27 Comparison {
28 field: usize,
29 op: BinaryOp,
30 literal: i64,
31 field_on_left: bool,
32 },
33 Between {
34 field: usize,
35 low: i64,
36 high: i64,
37 },
38}
39
40pub(super) enum ProjectedExpr {
41 Field(usize),
42 Literal(Value),
43 Binary {
44 op: BinaryOp,
45 lhs: Box<Self>,
46 rhs: Box<Self>,
47 integer_width: Option<IntegerWidth>,
48 },
49 UnaryMinus(Box<Self>),
50 IntFieldComparison {
51 field: usize,
52 op: BinaryOp,
53 literal: i64,
54 field_on_left: bool,
55 },
56 Not(Box<Self>),
57 And(Vec<Self>),
58 IntFieldConjunction(Vec<ProjectedIntPredicate>),
59 Or(Vec<Self>),
60 IsNull {
61 expression: Box<Self>,
62 negated: bool,
63 },
64 Between {
65 expression: Box<Self>,
66 low: Box<Self>,
67 high: Box<Self>,
68 },
69 IntFieldBetween {
70 field: usize,
71 low: i64,
72 high: i64,
73 },
74 InList {
75 expression: Box<Self>,
76 list: Vec<Self>,
77 negated: bool,
78 },
79 Like {
80 expression: Box<Self>,
81 pattern: uqa_sql::expr::CompiledLikePattern,
82 },
83 Cast {
84 expression: Box<Self>,
85 ty: String,
86 },
87}
88
89impl ProjectedPredicate {
90 pub fn compile(
91 expression: &ScalarExpr,
92 fields: &[String],
93 params: &[SQLParam],
94 ) -> Result<Option<Self>, SQLError> {
95 Self::compile_with_schema(expression, &RowSchema::new(fields.to_vec()), params)
96 }
97
98 pub fn compile_with_schema(
100 expression: &ScalarExpr,
101 schema: &RowSchema,
102 params: &[SQLParam],
103 ) -> Result<Option<Self>, SQLError> {
104 match compile::compile(expression, schema, params) {
105 Ok(expression) => Ok(expression.map(|expression| Self { expression })),
106 Err(SQLError::Unsupported(_)) => Ok(None),
107 Err(error) => Err(error),
108 }
109 }
110
111 #[inline]
112 pub fn keep(&self, values: &[&Value]) -> Result<bool, SQLError> {
113 evaluate::keep(&self.expression, values)
114 }
115
116 #[inline]
118 pub fn keep_indexed(&self, values: &[Value], projection: &[usize]) -> Result<bool, SQLError> {
119 evaluate::keep_indexed(&self.expression, values, projection)
120 }
121
122 #[inline]
126 pub fn keep_row(&self, row: &crate::PhysicalRowView<'_>) -> Result<bool, SQLError> {
127 evaluate::keep_row(&self.expression, row)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::{eval_scalar, ScalarEvalContext};
135 use uqa_sql::expr::truthy;
136
137 #[test]
138 fn positional_predicate_preserves_null_and_short_circuit_semantics() {
139 let expression = ScalarExpr::And(vec![
140 ScalarExpr::Between {
141 expr: Box::new(ScalarExpr::Column("x".into())),
142 low: Box::new(ScalarExpr::Literal(Value::Int(2))),
143 high: Box::new(ScalarExpr::Literal(Value::Int(4))),
144 },
145 ScalarExpr::IsNull {
146 expr: Box::new(ScalarExpr::Column("y".into())),
147 negated: false,
148 },
149 ]);
150 let predicate = ProjectedPredicate::compile(&expression, &["x".into(), "y".into()], &[])
151 .unwrap()
152 .unwrap();
153 assert!(predicate.keep(&[&Value::Int(3), &Value::Null]).unwrap());
154 assert!(!predicate.keep(&[&Value::Int(5), &Value::Null]).unwrap());
155 assert!(!predicate.keep(&[&Value::Null, &Value::Null]).unwrap());
156
157 let stored = [Value::Null, Value::Int(3)];
158 assert!(predicate.keep_indexed(&stored, &[1, 0]).unwrap());
159 assert!(!predicate.keep_indexed(&stored, &[usize::MAX, 0]).unwrap());
160 }
161
162 #[test]
163 fn projected_q1_and_q6_predicates_match_the_canonical_evaluator() {
164 let fields = vec!["discount".into(), "quantity".into(), "ship_day".into()];
165 let q6 = ScalarExpr::And(vec![
166 ScalarExpr::Between {
167 expr: Box::new(ScalarExpr::Column("ship_day".into())),
168 low: Box::new(ScalarExpr::Param(1)),
169 high: Box::new(ScalarExpr::Literal(Value::Int(2_190))),
170 },
171 ScalarExpr::Between {
172 expr: Box::new(ScalarExpr::Column("discount".into())),
173 low: Box::new(ScalarExpr::Literal(Value::Int(2))),
174 high: Box::new(ScalarExpr::Literal(Value::Int(8))),
175 },
176 ScalarExpr::Binary {
177 op: BinaryOp::Greater,
178 lhs: Box::new(ScalarExpr::Literal(Value::Int(40))),
179 rhs: Box::new(ScalarExpr::Column("quantity".into())),
180 },
181 ]);
182 let params = vec![SQLParam::Scalar(Value::Int(365))];
183 let predicate = ProjectedPredicate::compile(&q6, &fields, ¶ms)
184 .unwrap()
185 .unwrap();
186 assert!(matches!(
187 &predicate.expression,
188 ProjectedExpr::IntFieldConjunction(items) if items.len() == 3
189 ));
190 let discounts = [Value::Null, Value::Int(1), Value::Int(2), Value::Int(8)];
191 let quantities = [
192 Value::Null,
193 Value::Int(39),
194 Value::Int(40),
195 Value::Float(39.5),
196 ];
197 let ship_days = [
198 Value::Null,
199 Value::Int(364),
200 Value::Int(365),
201 Value::Int(2_190),
202 Value::Int(2_191),
203 ];
204 for discount in &discounts {
205 for quantity in &quantities {
206 for ship_day in &ship_days {
207 assert_projected_parity(
208 &q6,
209 &predicate,
210 &fields,
211 &[discount.clone(), quantity.clone(), ship_day.clone()],
212 ¶ms,
213 );
214 }
215 }
216 }
217
218 let q1 = ScalarExpr::Binary {
219 op: BinaryOp::LessEqual,
220 lhs: Box::new(ScalarExpr::Column("ship_day".into())),
221 rhs: Box::new(ScalarExpr::Literal(Value::Int(2_449))),
222 };
223 let predicate = ProjectedPredicate::compile(&q1, &fields, &[])
224 .unwrap()
225 .unwrap();
226 for ship_day in [Value::Null, Value::Int(2_449), Value::Int(2_450)] {
227 assert_projected_parity(
228 &q1,
229 &predicate,
230 &fields,
231 &[Value::Int(0), Value::Int(0), ship_day],
232 &[],
233 );
234 }
235 }
236
237 #[test]
238 fn projected_integer_comparisons_match_every_canonical_operator_and_operand_order() {
239 let fields = vec!["value".into()];
240 for op in [
241 BinaryOp::Equal,
242 BinaryOp::NotEqual,
243 BinaryOp::Less,
244 BinaryOp::LessEqual,
245 BinaryOp::Greater,
246 BinaryOp::GreaterEqual,
247 ] {
248 for field_on_left in [true, false] {
249 let field = ScalarExpr::Column("value".into());
250 let literal = ScalarExpr::Literal(Value::Int(7));
251 let expression = ScalarExpr::Binary {
252 op,
253 lhs: Box::new(if field_on_left {
254 field.clone()
255 } else {
256 literal.clone()
257 }),
258 rhs: Box::new(if field_on_left {
259 literal.clone()
260 } else {
261 field.clone()
262 }),
263 };
264 let predicate = ProjectedPredicate::compile(&expression, &fields, &[])
265 .unwrap()
266 .unwrap();
267 for value in [
268 Value::Null,
269 Value::Int(6),
270 Value::Int(7),
271 Value::Int(8),
272 Value::Float(7.0),
273 ] {
274 assert_projected_parity(&expression, &predicate, &fields, &[value], &[]);
275 }
276 }
277 }
278 }
279
280 #[test]
281 fn projected_like_predicates_match_the_canonical_evaluator() {
282 for (name, pattern) in [
283 ("like", "%"),
284 ("like", "%green%"),
285 ("like", "%special%requests%"),
286 ("like", "a_c"),
287 ("ilike", "%GREEN%"),
288 ] {
289 let expression = ScalarExpr::Func {
290 name: name.into(),
291 binding: None,
292 args: vec![
293 ScalarExpr::Column("text".into()),
294 ScalarExpr::Literal(Value::Str(pattern.into())),
295 ],
296 distinct: false,
297 order_by: Vec::new(),
298 filter: None,
299 };
300 let fields = vec!["text".into()];
301 let predicate = ProjectedPredicate::compile(&expression, &fields, &[])
302 .unwrap()
303 .unwrap();
304 for value in [
305 Value::Str("forest green part".into()),
306 Value::FixedChar("GREEN ".into()),
307 Value::Str("a-c".into()),
308 Value::Str("special pending requests".into()),
309 Value::Null,
310 ] {
311 assert_projected_parity(&expression, &predicate, &fields, &[value], &[]);
312 }
313 }
314 }
315
316 #[test]
317 fn qualified_like_runs_directly_on_a_composite_physical_row() {
318 let expression = ScalarExpr::Not(Box::new(ScalarExpr::Func {
319 name: "like".into(),
320 binding: None,
321 args: vec![
322 ScalarExpr::qualified_column("o", "comment"),
323 ScalarExpr::Literal(Value::Str("%special%requests%".into())),
324 ],
325 distinct: false,
326 order_by: Vec::new(),
327 filter: None,
328 }));
329 let left_schema =
330 crate::RowSchema::with_qualified_types("c", vec!["id".into()], vec![None]);
331 let right_schema =
332 crate::RowSchema::with_qualified_types("o", vec!["comment".into()], vec![None]);
333 let schema = crate::RowSchema::join(&left_schema, &right_schema, std::iter::empty());
334 let predicate = ProjectedPredicate::compile_with_schema(&expression, &schema, &[])
335 .unwrap()
336 .unwrap();
337
338 let accepted = crate::PhysicalRow::concat(
339 &crate::PhysicalRow::from_values(vec![Value::Int(1)]),
340 &crate::PhysicalRow::from_values(vec![Value::Str("ordinary order".into())]),
341 );
342 let rejected = crate::PhysicalRow::concat(
343 &crate::PhysicalRow::from_values(vec![Value::Int(1)]),
344 &crate::PhysicalRow::from_values(vec![Value::Str("special pending requests".into())]),
345 );
346 assert!(predicate.keep_row(&schema.view(&accepted)).unwrap());
347 assert!(!predicate.keep_row(&schema.view(&rejected)).unwrap());
348 }
349
350 #[test]
351 fn projected_predicate_folds_typed_literals_once() {
352 let expression = ScalarExpr::Binary {
353 op: BinaryOp::Less,
354 lhs: Box::new(ScalarExpr::Column("day".into())),
355 rhs: Box::new(ScalarExpr::Cast {
356 expr: Box::new(ScalarExpr::Literal(Value::Str("1995-03-15".into()))),
357 ty: "date".into(),
358 }),
359 };
360 let predicate = ProjectedPredicate::compile(&expression, &["day".into()], &[])
361 .unwrap()
362 .unwrap();
363
364 let ProjectedExpr::Binary { rhs, .. } = &predicate.expression else {
365 panic!("expected a compiled comparison");
366 };
367 assert!(matches!(
368 rhs.as_ref(),
369 ProjectedExpr::Literal(Value::Temporal(_))
370 ));
371 }
372
373 #[test]
374 fn projected_predicate_preserves_unary_minus_integer_width() {
375 let expression = ScalarExpr::Binary {
376 op: BinaryOp::Equal,
377 lhs: Box::new(ScalarExpr::UnaryMinus(Box::new(ScalarExpr::Cast {
378 expr: Box::new(ScalarExpr::Column("x".into())),
379 ty: "smallint".into(),
380 }))),
381 rhs: Box::new(ScalarExpr::Literal(Value::Int(-1))),
382 };
383 let predicate = ProjectedPredicate::compile(&expression, &["x".into()], &[])
384 .unwrap()
385 .unwrap();
386
387 assert!(predicate.keep(&[&Value::Int(1)]).unwrap());
388 let error = predicate
389 .keep(&[&Value::Int(i64::from(i16::MIN))])
390 .expect_err("negating smallint minimum must overflow");
391 assert_eq!(error.sqlstate(), Some("22003"));
392 }
393
394 fn assert_projected_parity(
395 expression: &ScalarExpr,
396 predicate: &ProjectedPredicate,
397 fields: &[String],
398 values: &[Value],
399 params: &[SQLParam],
400 ) {
401 let row = fields
402 .iter()
403 .cloned()
404 .zip(values.iter().cloned())
405 .collect::<uqa_sql::ResultRow>();
406 let expected = eval_scalar(expression, &ScalarEvalContext::new(Some(&row), params))
407 .map(|value| truthy(&value));
408 let references = values.iter().collect::<Vec<_>>();
409 let actual = predicate.keep(&references);
410 match (expected, actual) {
411 (Ok(expected), Ok(actual)) => assert_eq!(actual, expected, "row: {row:?}"),
412 (Err(expected), Err(actual)) => assert_eq!(actual.to_string(), expected.to_string()),
413 (expected, actual) => panic!("projected result {actual:?} != canonical {expected:?}"),
414 }
415 }
416}