Skip to main content

oxigdal_query/executor/
filter.rs

1//! Filter executor.
2
3use crate::error::{QueryError, Result};
4use crate::executor::scan::{ColumnData, RecordBatch};
5use crate::parser::ast::{BinaryOperator, Expr, Literal, UnaryOperator};
6use oxigdal_core::error::OxiGdalError;
7
8/// Filter operator.
9pub struct Filter {
10    /// Filter predicate.
11    pub predicate: Expr,
12}
13
14impl Filter {
15    /// Create a new filter.
16    pub fn new(predicate: Expr) -> Self {
17        Self { predicate }
18    }
19
20    /// Execute the filter on a record batch.
21    pub fn execute(&self, batch: &RecordBatch) -> Result<RecordBatch> {
22        let mut selection = vec![false; batch.num_rows];
23
24        // Evaluate predicate for each row
25        for (row_idx, sel) in selection.iter_mut().enumerate().take(batch.num_rows) {
26            let result = self.evaluate_expr(&self.predicate, batch, row_idx)?;
27            if let Value::Boolean(b) = result {
28                *sel = b;
29            } else {
30                return Err(QueryError::execution(
31                    OxiGdalError::invalid_operation_builder(
32                        "Filter predicate must evaluate to boolean type",
33                    )
34                    .with_operation("filter_evaluation")
35                    .with_parameter("row_index", row_idx.to_string())
36                    .with_parameter("actual_type", format!("{:?}", result))
37                    .with_suggestion("Ensure WHERE clause uses comparison or boolean operators")
38                    .build()
39                    .to_string(),
40                ));
41            }
42        }
43
44        // Filter columns based on selection
45        let mut filtered_columns = Vec::new();
46        for column in &batch.columns {
47            filtered_columns.push(self.filter_column(column, &selection));
48        }
49
50        let filtered_rows = selection.iter().filter(|&&b| b).count();
51
52        RecordBatch::new(batch.schema.clone(), filtered_columns, filtered_rows)
53    }
54
55    /// Filter a column based on selection.
56    fn filter_column(&self, column: &ColumnData, selection: &[bool]) -> ColumnData {
57        match column {
58            ColumnData::Boolean(data) => {
59                let filtered: Vec<Option<bool>> = data
60                    .iter()
61                    .zip(selection)
62                    .filter_map(|(v, &sel)| if sel { Some(*v) } else { None })
63                    .collect();
64                ColumnData::Boolean(filtered)
65            }
66            ColumnData::Int32(data) => {
67                let filtered: Vec<Option<i32>> = data
68                    .iter()
69                    .zip(selection)
70                    .filter_map(|(v, &sel)| if sel { Some(*v) } else { None })
71                    .collect();
72                ColumnData::Int32(filtered)
73            }
74            ColumnData::Int64(data) => {
75                let filtered: Vec<Option<i64>> = data
76                    .iter()
77                    .zip(selection)
78                    .filter_map(|(v, &sel)| if sel { Some(*v) } else { None })
79                    .collect();
80                ColumnData::Int64(filtered)
81            }
82            ColumnData::Float32(data) => {
83                let filtered: Vec<Option<f32>> = data
84                    .iter()
85                    .zip(selection)
86                    .filter_map(|(v, &sel)| if sel { Some(*v) } else { None })
87                    .collect();
88                ColumnData::Float32(filtered)
89            }
90            ColumnData::Float64(data) => {
91                let filtered: Vec<Option<f64>> = data
92                    .iter()
93                    .zip(selection)
94                    .filter_map(|(v, &sel)| if sel { Some(*v) } else { None })
95                    .collect();
96                ColumnData::Float64(filtered)
97            }
98            ColumnData::String(data) => {
99                let filtered: Vec<Option<String>> = data
100                    .iter()
101                    .zip(selection)
102                    .filter_map(|(v, &sel)| if sel { Some(v.clone()) } else { None })
103                    .collect();
104                ColumnData::String(filtered)
105            }
106            ColumnData::Binary(data) => {
107                let filtered = data
108                    .iter()
109                    .zip(selection)
110                    .filter_map(|(v, &sel)| if sel { Some(v.clone()) } else { None })
111                    .collect();
112                ColumnData::Binary(filtered)
113            }
114        }
115    }
116
117    /// Evaluate an expression for a specific row.
118    pub(crate) fn evaluate_expr(
119        &self,
120        expr: &Expr,
121        batch: &RecordBatch,
122        row_idx: usize,
123    ) -> Result<Value> {
124        match expr {
125            Expr::Column { table: _, name } => {
126                let column = batch
127                    .column_by_name(name)
128                    .ok_or_else(|| QueryError::ColumnNotFound(name.clone()))?;
129                self.get_column_value(column, row_idx)
130            }
131            Expr::Literal(lit) => Ok(Value::from_literal(lit)),
132            Expr::BinaryOp { left, op, right } => {
133                let left_val = self.evaluate_expr(left, batch, row_idx)?;
134                let right_val = self.evaluate_expr(right, batch, row_idx)?;
135                self.evaluate_binary_op(&left_val, *op, &right_val)
136            }
137            Expr::UnaryOp { op, expr } => {
138                let val = self.evaluate_expr(expr, batch, row_idx)?;
139                self.evaluate_unary_op(*op, &val)
140            }
141            Expr::IsNull(expr) => {
142                let val = self.evaluate_expr(expr, batch, row_idx)?;
143                Ok(Value::Boolean(matches!(val, Value::Null)))
144            }
145            Expr::IsNotNull(expr) => {
146                let val = self.evaluate_expr(expr, batch, row_idx)?;
147                Ok(Value::Boolean(!matches!(val, Value::Null)))
148            }
149            Expr::Function { name, args } => {
150                // Evaluate each argument first.
151                let arg_values: Vec<Value> = args
152                    .iter()
153                    .map(|a| self.evaluate_expr(a, batch, row_idx))
154                    .collect::<Result<Vec<_>>>()?;
155                // Dispatch to the spatial-function evaluator. The coordinate
156                // dimension is 2-D for the current row-based interpreter.
157                crate::executor::spatial_funcs::evaluate_spatial_function(name, &arg_values, 2)
158            }
159            _ => Err(QueryError::unsupported(
160                OxiGdalError::not_supported_builder("Unsupported expression type in filter")
161                    .with_operation("filter_evaluation")
162                    .with_parameter("expression_type", format!("{:?}", expr))
163                    .with_suggestion(
164                        "Use simpler expressions: columns, literals, binary/unary operators, IS [NOT] NULL",
165                    )
166                    .build()
167                    .to_string(),
168            )),
169        }
170    }
171
172    /// Get value from column at row index.
173    fn get_column_value(&self, column: &ColumnData, row_idx: usize) -> Result<Value> {
174        match column {
175            ColumnData::Boolean(data) => Ok(data
176                .get(row_idx)
177                .and_then(|v| v.as_ref())
178                .map(|&v| Value::Boolean(v))
179                .unwrap_or(Value::Null)),
180            ColumnData::Int32(data) => Ok(data
181                .get(row_idx)
182                .and_then(|v| v.as_ref())
183                .map(|&v| Value::Int32(v))
184                .unwrap_or(Value::Null)),
185            ColumnData::Int64(data) => Ok(data
186                .get(row_idx)
187                .and_then(|v| v.as_ref())
188                .map(|&v| Value::Int64(v))
189                .unwrap_or(Value::Null)),
190            ColumnData::Float32(data) => Ok(data
191                .get(row_idx)
192                .and_then(|v| v.as_ref())
193                .map(|&v| Value::Float32(v))
194                .unwrap_or(Value::Null)),
195            ColumnData::Float64(data) => Ok(data
196                .get(row_idx)
197                .and_then(|v| v.as_ref())
198                .map(|&v| Value::Float64(v))
199                .unwrap_or(Value::Null)),
200            ColumnData::String(data) => Ok(data
201                .get(row_idx)
202                .and_then(|v| v.as_ref())
203                .map(|v| Value::String(v.clone()))
204                .unwrap_or(Value::Null)),
205            ColumnData::Binary(_) => Err(QueryError::unsupported(
206                OxiGdalError::not_supported_builder(
207                    "Binary column type not supported in filter predicates",
208                )
209                .with_operation("column_value_extraction")
210                .with_parameter("row_index", row_idx.to_string())
211                .with_suggestion(
212                    "Cast binary columns to supported types or filter at a different stage",
213                )
214                .build()
215                .to_string(),
216            )),
217        }
218    }
219
220    /// Evaluate a binary operation.
221    fn evaluate_binary_op(&self, left: &Value, op: BinaryOperator, right: &Value) -> Result<Value> {
222        match (left, right) {
223            (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
224            // Type coercion: Int32 with Int64
225            (Value::Int32(l), Value::Int64(r)) => {
226                self.evaluate_binary_op(&Value::Int64(*l as i64), op, &Value::Int64(*r))
227            }
228            (Value::Int64(l), Value::Int32(r)) => {
229                self.evaluate_binary_op(&Value::Int64(*l), op, &Value::Int64(*r as i64))
230            }
231            (Value::Int32(l), Value::Int32(r)) => match op {
232                // Checked arithmetic: overflow yields NULL rather than panicking
233                // (debug) or silently wrapping (release), mirroring the
234                // divide/modulo-by-zero-returns-NULL convention below.
235                BinaryOperator::Plus => {
236                    Ok(l.checked_add(*r).map(Value::Int32).unwrap_or(Value::Null))
237                }
238                BinaryOperator::Minus => {
239                    Ok(l.checked_sub(*r).map(Value::Int32).unwrap_or(Value::Null))
240                }
241                BinaryOperator::Multiply => {
242                    Ok(l.checked_mul(*r).map(Value::Int32).unwrap_or(Value::Null))
243                }
244                BinaryOperator::Divide => {
245                    if *r == 0 {
246                        Ok(Value::Null)
247                    } else {
248                        Ok(Value::Int32(l / r))
249                    }
250                }
251                BinaryOperator::Modulo => {
252                    if *r == 0 {
253                        Ok(Value::Null)
254                    } else {
255                        Ok(Value::Int32(l % r))
256                    }
257                }
258                BinaryOperator::Eq => Ok(Value::Boolean(l == r)),
259                BinaryOperator::NotEq => Ok(Value::Boolean(l != r)),
260                BinaryOperator::Lt => Ok(Value::Boolean(l < r)),
261                BinaryOperator::LtEq => Ok(Value::Boolean(l <= r)),
262                BinaryOperator::Gt => Ok(Value::Boolean(l > r)),
263                BinaryOperator::GtEq => Ok(Value::Boolean(l >= r)),
264                _ => Err(QueryError::unsupported("Unsupported operator for integers")),
265            },
266            (Value::Int64(l), Value::Int64(r)) => match op {
267                // Checked arithmetic: overflow yields NULL rather than panicking
268                // (debug) or silently wrapping (release).
269                BinaryOperator::Plus => {
270                    Ok(l.checked_add(*r).map(Value::Int64).unwrap_or(Value::Null))
271                }
272                BinaryOperator::Minus => {
273                    Ok(l.checked_sub(*r).map(Value::Int64).unwrap_or(Value::Null))
274                }
275                BinaryOperator::Multiply => {
276                    Ok(l.checked_mul(*r).map(Value::Int64).unwrap_or(Value::Null))
277                }
278                BinaryOperator::Divide => {
279                    if *r == 0 {
280                        Ok(Value::Null)
281                    } else {
282                        Ok(Value::Int64(l / r))
283                    }
284                }
285                BinaryOperator::Modulo => {
286                    if *r == 0 {
287                        Ok(Value::Null)
288                    } else {
289                        Ok(Value::Int64(l % r))
290                    }
291                }
292                BinaryOperator::Eq => Ok(Value::Boolean(l == r)),
293                BinaryOperator::NotEq => Ok(Value::Boolean(l != r)),
294                BinaryOperator::Lt => Ok(Value::Boolean(l < r)),
295                BinaryOperator::LtEq => Ok(Value::Boolean(l <= r)),
296                BinaryOperator::Gt => Ok(Value::Boolean(l > r)),
297                BinaryOperator::GtEq => Ok(Value::Boolean(l >= r)),
298                _ => Err(QueryError::unsupported("Unsupported operator for integers")),
299            },
300            // Type coercion: Float32 with Float64
301            (Value::Float32(l), Value::Float64(r)) => {
302                self.evaluate_binary_op(&Value::Float64(*l as f64), op, &Value::Float64(*r))
303            }
304            (Value::Float64(l), Value::Float32(r)) => {
305                self.evaluate_binary_op(&Value::Float64(*l), op, &Value::Float64(*r as f64))
306            }
307            (Value::Float32(l), Value::Float32(r)) => match op {
308                BinaryOperator::Plus => Ok(Value::Float32(l + r)),
309                BinaryOperator::Minus => Ok(Value::Float32(l - r)),
310                BinaryOperator::Multiply => Ok(Value::Float32(l * r)),
311                BinaryOperator::Divide => Ok(Value::Float32(l / r)),
312                BinaryOperator::Eq => Ok(Value::Boolean((l - r).abs() < f32::EPSILON)),
313                BinaryOperator::NotEq => Ok(Value::Boolean((l - r).abs() >= f32::EPSILON)),
314                BinaryOperator::Lt => Ok(Value::Boolean(l < r)),
315                BinaryOperator::LtEq => Ok(Value::Boolean(l <= r)),
316                BinaryOperator::Gt => Ok(Value::Boolean(l > r)),
317                BinaryOperator::GtEq => Ok(Value::Boolean(l >= r)),
318                _ => Err(QueryError::unsupported("Unsupported operator for floats")),
319            },
320            (Value::Float64(l), Value::Float64(r)) => match op {
321                BinaryOperator::Plus => Ok(Value::Float64(l + r)),
322                BinaryOperator::Minus => Ok(Value::Float64(l - r)),
323                BinaryOperator::Multiply => Ok(Value::Float64(l * r)),
324                BinaryOperator::Divide => Ok(Value::Float64(l / r)),
325                BinaryOperator::Eq => Ok(Value::Boolean((l - r).abs() < f64::EPSILON)),
326                BinaryOperator::NotEq => Ok(Value::Boolean((l - r).abs() >= f64::EPSILON)),
327                BinaryOperator::Lt => Ok(Value::Boolean(l < r)),
328                BinaryOperator::LtEq => Ok(Value::Boolean(l <= r)),
329                BinaryOperator::Gt => Ok(Value::Boolean(l > r)),
330                BinaryOperator::GtEq => Ok(Value::Boolean(l >= r)),
331                _ => Err(QueryError::unsupported("Unsupported operator for floats")),
332            },
333            // Type coercion: Int with Float
334            (Value::Int32(l), Value::Float64(r)) => {
335                self.evaluate_binary_op(&Value::Float64(*l as f64), op, &Value::Float64(*r))
336            }
337            (Value::Int64(l), Value::Float64(r)) => {
338                self.evaluate_binary_op(&Value::Float64(*l as f64), op, &Value::Float64(*r))
339            }
340            (Value::Float64(l), Value::Int32(r)) => {
341                self.evaluate_binary_op(&Value::Float64(*l), op, &Value::Float64(*r as f64))
342            }
343            (Value::Float64(l), Value::Int64(r)) => {
344                self.evaluate_binary_op(&Value::Float64(*l), op, &Value::Float64(*r as f64))
345            }
346            (Value::Boolean(l), Value::Boolean(r)) => match op {
347                BinaryOperator::And => Ok(Value::Boolean(*l && *r)),
348                BinaryOperator::Or => Ok(Value::Boolean(*l || *r)),
349                BinaryOperator::Eq => Ok(Value::Boolean(l == r)),
350                BinaryOperator::NotEq => Ok(Value::Boolean(l != r)),
351                _ => Err(QueryError::unsupported("Unsupported operator for booleans")),
352            },
353            (Value::String(l), Value::String(r)) => match op {
354                BinaryOperator::Eq => Ok(Value::Boolean(l == r)),
355                BinaryOperator::NotEq => Ok(Value::Boolean(l != r)),
356                BinaryOperator::Concat => Ok(Value::String(format!("{}{}", l, r))),
357                // Lexicographic ordering.
358                BinaryOperator::Lt => Ok(Value::Boolean(l < r)),
359                BinaryOperator::LtEq => Ok(Value::Boolean(l <= r)),
360                BinaryOperator::Gt => Ok(Value::Boolean(l > r)),
361                BinaryOperator::GtEq => Ok(Value::Boolean(l >= r)),
362                // Pattern matching (`l` is the value, `r` is the pattern).
363                BinaryOperator::Like => Ok(Value::Boolean(crate::executor::like::like_match(
364                    l, r, false,
365                ))),
366                BinaryOperator::NotLike => Ok(Value::Boolean(!crate::executor::like::like_match(
367                    l, r, false,
368                ))),
369                BinaryOperator::ILike => Ok(Value::Boolean(crate::executor::like::like_match(
370                    l, r, true,
371                ))),
372                BinaryOperator::NotILike => Ok(Value::Boolean(!crate::executor::like::like_match(
373                    l, r, true,
374                ))),
375                _ => Err(QueryError::unsupported("Unsupported operator for strings")),
376            },
377            _ => Err(QueryError::execution(
378                OxiGdalError::invalid_operation_builder("Type mismatch in binary operation")
379                    .with_operation("binary_operator_evaluation")
380                    .with_parameter("left_type", format!("{:?}", left))
381                    .with_parameter("right_type", format!("{:?}", right))
382                    .with_parameter("operator", format!("{:?}", op))
383                    .with_suggestion(
384                        "Ensure both operands have compatible types or use explicit type casts",
385                    )
386                    .build()
387                    .to_string(),
388            )),
389        }
390    }
391
392    /// Evaluate a unary operation.
393    fn evaluate_unary_op(&self, op: UnaryOperator, val: &Value) -> Result<Value> {
394        match (op, val) {
395            (UnaryOperator::Minus, Value::Int64(i)) => Ok(Value::Int64(-i)),
396            (UnaryOperator::Minus, Value::Float64(f)) => Ok(Value::Float64(-f)),
397            (UnaryOperator::Not, Value::Boolean(b)) => Ok(Value::Boolean(!b)),
398            (_, Value::Null) => Ok(Value::Null),
399            _ => Err(QueryError::unsupported("Unsupported unary operation")),
400        }
401    }
402}
403
404/// Evaluate an expression for a single row without a pre-built [`Filter`].
405///
406/// Shared by the Sort and Aggregate operators so `ORDER BY` / `GROUP BY` can
407/// evaluate arbitrary scalar expressions (columns, arithmetic, functions)
408/// through the exact same evaluator used for WHERE-clause predicates. The
409/// dummy predicate is never inspected by [`Filter::evaluate_expr`].
410pub(crate) fn evaluate_expr_for_row(
411    expr: &Expr,
412    batch: &RecordBatch,
413    row_idx: usize,
414) -> Result<Value> {
415    Filter::new(Expr::Wildcard).evaluate_expr(expr, batch, row_idx)
416}
417
418/// Runtime value.
419#[derive(Debug, Clone, PartialEq)]
420pub enum Value {
421    /// Null value.
422    Null,
423    /// Boolean value.
424    Boolean(bool),
425    /// 32-bit integer value.
426    Int32(i32),
427    /// 64-bit integer value.
428    Int64(i64),
429    /// 32-bit float value.
430    Float32(f32),
431    /// 64-bit float value.
432    Float64(f64),
433    /// String value.
434    String(String),
435    /// Geometry value (constructed by spatial functions or parsed from WKT).
436    Geometry(geo::Geometry<f64>),
437}
438
439impl Value {
440    /// Convert from a literal.
441    fn from_literal(lit: &Literal) -> Self {
442        match lit {
443            Literal::Null => Value::Null,
444            Literal::Boolean(b) => Value::Boolean(*b),
445            Literal::Integer(i) => Value::Int64(*i),
446            Literal::Float(f) => Value::Float64(*f),
447            Literal::String(s) => Value::String(s.clone()),
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use crate::executor::scan::{Field, Schema};
456    use std::sync::Arc;
457
458    #[test]
459    fn test_filter_execution() -> Result<()> {
460        let schema = Arc::new(Schema::new(vec![
461            Field::new(
462                "id".to_string(),
463                crate::executor::scan::DataType::Int64,
464                false,
465            ),
466            Field::new(
467                "value".to_string(),
468                crate::executor::scan::DataType::Int64,
469                false,
470            ),
471        ]));
472
473        let columns = vec![
474            ColumnData::Int64(vec![Some(1), Some(2), Some(3), Some(4), Some(5)]),
475            ColumnData::Int64(vec![Some(10), Some(20), Some(30), Some(40), Some(50)]),
476        ];
477
478        let batch = RecordBatch::new(schema, columns, 5)?;
479
480        // Filter: value > 25
481        let predicate = Expr::BinaryOp {
482            left: Box::new(Expr::Column {
483                table: None,
484                name: "value".to_string(),
485            }),
486            op: BinaryOperator::Gt,
487            right: Box::new(Expr::Literal(Literal::Integer(25))),
488        };
489
490        let filter = Filter::new(predicate);
491        let filtered = filter.execute(&batch)?;
492
493        assert_eq!(filtered.num_rows, 3); // 30, 40, 50 are > 25
494
495        Ok(())
496    }
497
498    fn string_batch() -> Result<RecordBatch> {
499        let schema = Arc::new(Schema::new(vec![Field::new(
500            "name".to_string(),
501            crate::executor::scan::DataType::String,
502            true,
503        )]));
504        let columns = vec![ColumnData::String(vec![
505            Some("Apple".to_string()),
506            Some("banana".to_string()),
507            Some("Cherry".to_string()),
508            Some("foobar".to_string()),
509        ])];
510        RecordBatch::new(schema, columns, 4)
511    }
512
513    fn name_col(expr_name: &str) -> Expr {
514        Expr::Column {
515            table: None,
516            name: expr_name.to_string(),
517        }
518    }
519
520    #[test]
521    fn test_where_like_case_sensitive() -> Result<()> {
522        let batch = string_batch()?;
523        // WHERE name LIKE '%oo%' should match only "foobar".
524        let predicate = Expr::BinaryOp {
525            left: Box::new(name_col("name")),
526            op: BinaryOperator::Like,
527            right: Box::new(Expr::Literal(Literal::String("%oo%".to_string()))),
528        };
529        let filtered = Filter::new(predicate).execute(&batch)?;
530        assert_eq!(filtered.num_rows, 1);
531
532        // LIKE is case-sensitive: uppercase pattern matches nothing here.
533        let predicate = Expr::BinaryOp {
534            left: Box::new(name_col("name")),
535            op: BinaryOperator::Like,
536            right: Box::new(Expr::Literal(Literal::String("A%".to_string()))),
537        };
538        let filtered = Filter::new(predicate).execute(&batch)?;
539        assert_eq!(filtered.num_rows, 1); // only "Apple"
540
541        let predicate = Expr::BinaryOp {
542            left: Box::new(name_col("name")),
543            op: BinaryOperator::Like,
544            right: Box::new(Expr::Literal(Literal::String("a%".to_string()))),
545        };
546        let filtered = Filter::new(predicate).execute(&batch)?;
547        assert_eq!(filtered.num_rows, 0); // "Apple" is capital A, none start lowercase 'a'
548        Ok(())
549    }
550
551    #[test]
552    fn test_where_not_like() -> Result<()> {
553        let batch = string_batch()?;
554        let predicate = Expr::BinaryOp {
555            left: Box::new(name_col("name")),
556            op: BinaryOperator::NotLike,
557            right: Box::new(Expr::Literal(Literal::String("%oo%".to_string()))),
558        };
559        let filtered = Filter::new(predicate).execute(&batch)?;
560        assert_eq!(filtered.num_rows, 3); // everything except "foobar"
561        Ok(())
562    }
563
564    #[test]
565    fn test_where_ilike_case_insensitive() -> Result<()> {
566        let batch = string_batch()?;
567        let predicate = Expr::BinaryOp {
568            left: Box::new(name_col("name")),
569            op: BinaryOperator::ILike,
570            right: Box::new(Expr::Literal(Literal::String("a%".to_string()))),
571        };
572        let filtered = Filter::new(predicate).execute(&batch)?;
573        assert_eq!(filtered.num_rows, 1); // "Apple" matches case-insensitively
574        Ok(())
575    }
576
577    #[test]
578    fn test_where_string_ordering() -> Result<()> {
579        let batch = string_batch()?;
580        // WHERE name > 'C' -> lexicographic: "Cherry", "banana", "foobar"
581        // ASCII: uppercase letters sort before lowercase, so "Cherry" > "C",
582        // "banana"/"foobar" (lowercase) > "C" too; "Apple" < "C".
583        let predicate = Expr::BinaryOp {
584            left: Box::new(name_col("name")),
585            op: BinaryOperator::Gt,
586            right: Box::new(Expr::Literal(Literal::String("C".to_string()))),
587        };
588        let filtered = Filter::new(predicate).execute(&batch)?;
589        assert_eq!(filtered.num_rows, 3);
590        Ok(())
591    }
592
593    #[test]
594    fn test_where_int_overflow_returns_null_not_panic() -> Result<()> {
595        // i32::MAX + 1 must not panic; the arithmetic yields NULL, so the
596        // comparison against it is NULL (not selected) rather than crashing.
597        let schema = Arc::new(Schema::new(vec![Field::new(
598            "v".to_string(),
599            crate::executor::scan::DataType::Int32,
600            false,
601        )]));
602        let columns = vec![ColumnData::Int32(vec![Some(i32::MAX), Some(1)])];
603        let batch = RecordBatch::new(schema, columns, 2)?;
604
605        // WHERE v + 1 > 0
606        let predicate = Expr::BinaryOp {
607            left: Box::new(Expr::BinaryOp {
608                left: Box::new(name_col("v")),
609                op: BinaryOperator::Plus,
610                right: Box::new(Expr::Literal(Literal::Integer(1))),
611            }),
612            op: BinaryOperator::Gt,
613            right: Box::new(Expr::Literal(Literal::Integer(0))),
614        };
615        // Literal::Integer maps to Int64, so v(Int32) + 1(Int64) coerces to
616        // i64 and does not overflow. Test the pure-i32 overflow path directly.
617        let ovf = Filter::new(Expr::Wildcard).evaluate_binary_op(
618            &Value::Int32(i32::MAX),
619            BinaryOperator::Plus,
620            &Value::Int32(1),
621        )?;
622        assert_eq!(ovf, Value::Null);
623
624        let ovf64 = Filter::new(Expr::Wildcard).evaluate_binary_op(
625            &Value::Int64(i64::MAX),
626            BinaryOperator::Multiply,
627            &Value::Int64(2),
628        )?;
629        assert_eq!(ovf64, Value::Null);
630
631        // Sanity: the end-to-end predicate still executes without panicking.
632        let _ = Filter::new(predicate).execute(&batch)?;
633        Ok(())
634    }
635}