iceberg_datafusion/physical_plan/
expr_to_predicate.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::vec;
19
20use datafusion::arrow::datatypes::DataType;
21use datafusion::logical_expr::{Expr, Like, Operator};
22use datafusion::scalar::ScalarValue;
23use iceberg::expr::{BinaryExpression, Predicate, PredicateOperator, Reference, UnaryExpression};
24use iceberg::spec::{Datum, PrimitiveLiteral};
25
26// A datafusion expression could be an Iceberg predicate, column, or literal.
27enum TransformedResult {
28    Predicate(Predicate),
29    Column(Reference),
30    Literal(Datum),
31    NotTransformed,
32}
33
34enum OpTransformedResult {
35    Operator(PredicateOperator),
36    And,
37    Or,
38    NotTransformed,
39}
40
41/// Converts DataFusion filters ([`Expr`]) to an iceberg [`Predicate`].
42/// If none of the filters could be converted, return `None` which adds no predicates to the scan operation.
43/// If the conversion was successful, return the converted predicates combined with an AND operator.
44pub fn convert_filters_to_predicate(filters: &[Expr]) -> Option<Predicate> {
45    filters
46        .iter()
47        .filter_map(convert_filter_to_predicate)
48        .reduce(Predicate::and)
49}
50
51fn convert_filter_to_predicate(expr: &Expr) -> Option<Predicate> {
52    match to_iceberg_predicate(expr) {
53        TransformedResult::Predicate(predicate) => Some(predicate),
54        TransformedResult::Column(column) => {
55            // A bare column in a filter context represents a boolean column check
56            // Convert it to: column = true
57            Some(Predicate::Binary(BinaryExpression::new(
58                PredicateOperator::Eq,
59                column,
60                Datum::bool(true),
61            )))
62        }
63        TransformedResult::Literal(_) => {
64            // Literal values in filter context cannot be pushed down
65            None
66        }
67        _ => None,
68    }
69}
70
71fn to_iceberg_predicate(expr: &Expr) -> TransformedResult {
72    match expr {
73        Expr::BinaryExpr(binary) => {
74            let left = to_iceberg_predicate(&binary.left);
75            let right = to_iceberg_predicate(&binary.right);
76            let op = to_iceberg_operation(binary.op);
77            match op {
78                OpTransformedResult::Operator(op) => to_iceberg_binary_predicate(left, right, op),
79                OpTransformedResult::And => to_iceberg_and_predicate(left, right),
80                OpTransformedResult::Or => to_iceberg_or_predicate(left, right),
81                OpTransformedResult::NotTransformed => TransformedResult::NotTransformed,
82            }
83        }
84        Expr::Not(exp) => {
85            let expr = to_iceberg_predicate(exp);
86            match expr {
87                TransformedResult::Predicate(p) => TransformedResult::Predicate(!p),
88                TransformedResult::Column(column) => {
89                    // NOT of a bare boolean column: NOT col => col = false
90                    TransformedResult::Predicate(Predicate::Binary(BinaryExpression::new(
91                        PredicateOperator::Eq,
92                        column,
93                        Datum::bool(false),
94                    )))
95                }
96                _ => TransformedResult::NotTransformed,
97            }
98        }
99        Expr::Column(column) => TransformedResult::Column(Reference::new(column.name())),
100        Expr::Literal(literal, _) => match scalar_value_to_datum(literal) {
101            Some(data) => TransformedResult::Literal(data),
102            None => TransformedResult::NotTransformed,
103        },
104        Expr::InList(inlist) => {
105            let mut datums = vec![];
106            for expr in &inlist.list {
107                let p = to_iceberg_predicate(expr);
108                match p {
109                    TransformedResult::Literal(l) => datums.push(l),
110                    _ => return TransformedResult::NotTransformed,
111                }
112            }
113
114            let expr = to_iceberg_predicate(&inlist.expr);
115            match expr {
116                TransformedResult::Column(r) => match inlist.negated {
117                    false => TransformedResult::Predicate(r.is_in(datums)),
118                    true => TransformedResult::Predicate(r.is_not_in(datums)),
119                },
120                _ => TransformedResult::NotTransformed,
121            }
122        }
123        Expr::IsNull(expr) => {
124            let p = to_iceberg_predicate(expr);
125            match p {
126                TransformedResult::Column(r) => TransformedResult::Predicate(Predicate::Unary(
127                    UnaryExpression::new(PredicateOperator::IsNull, r),
128                )),
129                _ => TransformedResult::NotTransformed,
130            }
131        }
132        Expr::IsNotNull(expr) => {
133            let p = to_iceberg_predicate(expr);
134            match p {
135                TransformedResult::Column(r) => TransformedResult::Predicate(Predicate::Unary(
136                    UnaryExpression::new(PredicateOperator::NotNull, r),
137                )),
138                _ => TransformedResult::NotTransformed,
139            }
140        }
141        Expr::Cast(c) => {
142            if c.data_type == DataType::Date32 || c.data_type == DataType::Date64 {
143                // Casts to date truncate the expression, we cannot simply extract it as it
144                // can create erroneous predicates.
145                return TransformedResult::NotTransformed;
146            }
147            to_iceberg_predicate(&c.expr)
148        }
149        Expr::Like(Like {
150            negated,
151            expr,
152            pattern,
153            escape_char,
154            case_insensitive,
155        }) => {
156            // Only support simple prefix patterns (e.g., 'prefix%')
157            // Note: Iceberg's StartsWith operator is case-sensitive, so we cannot
158            // push down case-insensitive LIKE (ILIKE) patterns
159            // Escape characters are also not supported for pushdown
160            if escape_char.is_some() || *case_insensitive {
161                return TransformedResult::NotTransformed;
162            }
163
164            // Extract the pattern string
165            let pattern_str = match to_iceberg_predicate(pattern) {
166                TransformedResult::Literal(d) => match d.literal() {
167                    PrimitiveLiteral::String(s) => s.clone(),
168                    _ => return TransformedResult::NotTransformed,
169                },
170                _ => return TransformedResult::NotTransformed,
171            };
172
173            // Check if it's a simple prefix pattern (ends with % and no other wildcards)
174            if pattern_str.ends_with('%')
175                && !pattern_str[..pattern_str.len() - 1].contains(['%', '_'])
176            {
177                // Extract the prefix (remove trailing %)
178                let prefix = pattern_str[..pattern_str.len() - 1].to_string();
179
180                // Get the column reference
181                let column = match to_iceberg_predicate(expr) {
182                    TransformedResult::Column(r) => r,
183                    _ => return TransformedResult::NotTransformed,
184                };
185
186                // Create the appropriate predicate
187                let predicate = if *negated {
188                    column.not_starts_with(Datum::string(prefix))
189                } else {
190                    column.starts_with(Datum::string(prefix))
191                };
192
193                TransformedResult::Predicate(predicate)
194            } else {
195                // Complex LIKE patterns cannot be pushed down
196                TransformedResult::NotTransformed
197            }
198        }
199        _ => TransformedResult::NotTransformed,
200    }
201}
202
203fn to_iceberg_operation(op: Operator) -> OpTransformedResult {
204    match op {
205        Operator::Eq => OpTransformedResult::Operator(PredicateOperator::Eq),
206        Operator::NotEq => OpTransformedResult::Operator(PredicateOperator::NotEq),
207        Operator::Lt => OpTransformedResult::Operator(PredicateOperator::LessThan),
208        Operator::LtEq => OpTransformedResult::Operator(PredicateOperator::LessThanOrEq),
209        Operator::Gt => OpTransformedResult::Operator(PredicateOperator::GreaterThan),
210        Operator::GtEq => OpTransformedResult::Operator(PredicateOperator::GreaterThanOrEq),
211        // AND OR
212        Operator::And => OpTransformedResult::And,
213        Operator::Or => OpTransformedResult::Or,
214        // Others not supported
215        _ => OpTransformedResult::NotTransformed,
216    }
217}
218
219fn to_iceberg_and_predicate(
220    left: TransformedResult,
221    right: TransformedResult,
222) -> TransformedResult {
223    match (left, right) {
224        (TransformedResult::Predicate(left), TransformedResult::Predicate(right)) => {
225            TransformedResult::Predicate(left.and(right))
226        }
227        (TransformedResult::Predicate(left), _) => TransformedResult::Predicate(left),
228        (_, TransformedResult::Predicate(right)) => TransformedResult::Predicate(right),
229        _ => TransformedResult::NotTransformed,
230    }
231}
232
233fn to_iceberg_or_predicate(left: TransformedResult, right: TransformedResult) -> TransformedResult {
234    match (left, right) {
235        (TransformedResult::Predicate(left), TransformedResult::Predicate(right)) => {
236            TransformedResult::Predicate(left.or(right))
237        }
238        _ => TransformedResult::NotTransformed,
239    }
240}
241
242fn to_iceberg_binary_predicate(
243    left: TransformedResult,
244    right: TransformedResult,
245    op: PredicateOperator,
246) -> TransformedResult {
247    let (r, d, op) = match (left, right) {
248        (TransformedResult::NotTransformed, _) => return TransformedResult::NotTransformed,
249        (_, TransformedResult::NotTransformed) => return TransformedResult::NotTransformed,
250        (TransformedResult::Column(r), TransformedResult::Literal(d)) => (r, d, op),
251        (TransformedResult::Literal(d), TransformedResult::Column(r)) => {
252            (r, d, reverse_predicate_operator(op))
253        }
254        _ => return TransformedResult::NotTransformed,
255    };
256    TransformedResult::Predicate(Predicate::Binary(BinaryExpression::new(op, r, d)))
257}
258
259fn reverse_predicate_operator(op: PredicateOperator) -> PredicateOperator {
260    match op {
261        PredicateOperator::Eq => PredicateOperator::Eq,
262        PredicateOperator::NotEq => PredicateOperator::NotEq,
263        PredicateOperator::GreaterThan => PredicateOperator::LessThan,
264        PredicateOperator::GreaterThanOrEq => PredicateOperator::LessThanOrEq,
265        PredicateOperator::LessThan => PredicateOperator::GreaterThan,
266        PredicateOperator::LessThanOrEq => PredicateOperator::GreaterThanOrEq,
267        _ => unreachable!("Reverse {}", op),
268    }
269}
270
271const MILLIS_PER_DAY: i64 = 24 * 60 * 60 * 1000;
272
273/// Convert a scalar value to an iceberg datum.
274fn scalar_value_to_datum(value: &ScalarValue) -> Option<Datum> {
275    match value {
276        ScalarValue::Boolean(Some(v)) => Some(Datum::bool(*v)),
277        ScalarValue::Int8(Some(v)) => Some(Datum::int(*v as i32)),
278        ScalarValue::Int16(Some(v)) => Some(Datum::int(*v as i32)),
279        ScalarValue::Int32(Some(v)) => Some(Datum::int(*v)),
280        ScalarValue::Int64(Some(v)) => Some(Datum::long(*v)),
281        ScalarValue::Float32(Some(v)) => Some(Datum::double(*v as f64)),
282        ScalarValue::Float64(Some(v)) => Some(Datum::double(*v)),
283        ScalarValue::Utf8(Some(v)) => Some(Datum::string(v.clone())),
284        ScalarValue::LargeUtf8(Some(v)) => Some(Datum::string(v.clone())),
285        ScalarValue::Binary(Some(v)) => Some(Datum::binary(v.clone())),
286        ScalarValue::LargeBinary(Some(v)) => Some(Datum::binary(v.clone())),
287        ScalarValue::Date32(Some(v)) => Some(Datum::date(*v)),
288        ScalarValue::Date64(Some(v)) => Some(Datum::date((*v / MILLIS_PER_DAY) as i32)),
289        // Timestamp conversions
290        // Note: TimestampSecond and TimestampMillisecond are not handled here because
291        // DataFusion's type coercion always converts them to match the column type
292        // (either TimestampMicrosecond or TimestampNanosecond) before predicate pushdown.
293        // See unit tests for how those conversions would work if needed.
294        ScalarValue::TimestampMicrosecond(Some(v), _) => Some(Datum::timestamp_micros(*v)),
295        ScalarValue::TimestampNanosecond(Some(v), _) => Some(Datum::timestamp_nanos(*v)),
296        _ => None,
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use std::collections::HashMap;
303
304    use datafusion::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
305    use datafusion::common::DFSchema;
306    use datafusion::logical_expr::utils::split_conjunction;
307    use datafusion::prelude::{Expr, SessionContext};
308    use iceberg::expr::{Predicate, Reference};
309    use iceberg::spec::Datum;
310    use parquet::arrow::PARQUET_FIELD_ID_META_KEY;
311
312    use super::convert_filters_to_predicate;
313
314    fn create_test_schema() -> DFSchema {
315        let arrow_schema = Schema::new(vec![
316            Field::new("foo", DataType::Int32, true).with_metadata(HashMap::from([(
317                PARQUET_FIELD_ID_META_KEY.to_string(),
318                "1".to_string(),
319            )])),
320            Field::new("bar", DataType::Utf8, true).with_metadata(HashMap::from([(
321                PARQUET_FIELD_ID_META_KEY.to_string(),
322                "2".to_string(),
323            )])),
324            Field::new("ts", DataType::Timestamp(TimeUnit::Second, None), true).with_metadata(
325                HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), "3".to_string())]),
326            ),
327        ]);
328        DFSchema::try_from_qualified_schema("my_table", &arrow_schema).unwrap()
329    }
330
331    fn convert_to_iceberg_predicate(sql: &str) -> Option<Predicate> {
332        let df_schema = create_test_schema();
333        let expr = SessionContext::new()
334            .parse_sql_expr(sql, &df_schema)
335            .unwrap();
336        let exprs: Vec<Expr> = split_conjunction(&expr).into_iter().cloned().collect();
337        convert_filters_to_predicate(&exprs[..])
338    }
339
340    #[test]
341    fn test_predicate_conversion_with_single_condition() {
342        let predicate = convert_to_iceberg_predicate("foo = 1").unwrap();
343        assert_eq!(predicate, Reference::new("foo").equal_to(Datum::long(1)));
344
345        let predicate = convert_to_iceberg_predicate("foo != 1").unwrap();
346        assert_eq!(
347            predicate,
348            Reference::new("foo").not_equal_to(Datum::long(1))
349        );
350
351        let predicate = convert_to_iceberg_predicate("foo > 1").unwrap();
352        assert_eq!(
353            predicate,
354            Reference::new("foo").greater_than(Datum::long(1))
355        );
356
357        let predicate = convert_to_iceberg_predicate("foo >= 1").unwrap();
358        assert_eq!(
359            predicate,
360            Reference::new("foo").greater_than_or_equal_to(Datum::long(1))
361        );
362
363        let predicate = convert_to_iceberg_predicate("foo < 1").unwrap();
364        assert_eq!(predicate, Reference::new("foo").less_than(Datum::long(1)));
365
366        let predicate = convert_to_iceberg_predicate("foo <= 1").unwrap();
367        assert_eq!(
368            predicate,
369            Reference::new("foo").less_than_or_equal_to(Datum::long(1))
370        );
371
372        let predicate = convert_to_iceberg_predicate("foo is null").unwrap();
373        assert_eq!(predicate, Reference::new("foo").is_null());
374
375        let predicate = convert_to_iceberg_predicate("foo is not null").unwrap();
376        assert_eq!(predicate, Reference::new("foo").is_not_null());
377
378        let predicate = convert_to_iceberg_predicate("foo in (5, 6)").unwrap();
379        assert_eq!(
380            predicate,
381            Reference::new("foo").is_in([Datum::long(5), Datum::long(6)])
382        );
383
384        let predicate = convert_to_iceberg_predicate("foo not in (5, 6)").unwrap();
385        assert_eq!(
386            predicate,
387            Reference::new("foo").is_not_in([Datum::long(5), Datum::long(6)])
388        );
389
390        let predicate = convert_to_iceberg_predicate("not foo = 1").unwrap();
391        assert_eq!(predicate, !Reference::new("foo").equal_to(Datum::long(1)));
392    }
393
394    #[test]
395    fn test_predicate_conversion_with_single_unsupported_condition() {
396        let predicate = convert_to_iceberg_predicate("foo + 1 = 1");
397        assert_eq!(predicate, None);
398
399        let predicate = convert_to_iceberg_predicate("length(bar) = 1");
400        assert_eq!(predicate, None);
401
402        let predicate = convert_to_iceberg_predicate("foo in (1, 2, foo)");
403        assert_eq!(predicate, None);
404    }
405
406    #[test]
407    fn test_predicate_conversion_with_single_condition_rev() {
408        let predicate = convert_to_iceberg_predicate("1 < foo").unwrap();
409        assert_eq!(
410            predicate,
411            Reference::new("foo").greater_than(Datum::long(1))
412        );
413    }
414
415    #[test]
416    fn test_predicate_conversion_with_and_condition() {
417        let sql = "foo > 1 and bar = 'test'";
418        let predicate = convert_to_iceberg_predicate(sql).unwrap();
419        let expected_predicate = Predicate::and(
420            Reference::new("foo").greater_than(Datum::long(1)),
421            Reference::new("bar").equal_to(Datum::string("test")),
422        );
423        assert_eq!(predicate, expected_predicate);
424    }
425
426    #[test]
427    fn test_predicate_conversion_with_and_condition_unsupported() {
428        let sql = "foo > 1 and length(bar) = 1";
429        let predicate = convert_to_iceberg_predicate(sql).unwrap();
430        let expected_predicate = Reference::new("foo").greater_than(Datum::long(1));
431        assert_eq!(predicate, expected_predicate);
432    }
433
434    #[test]
435    fn test_predicate_conversion_with_and_condition_both_unsupported() {
436        let sql = "foo in (1, 2, foo) and length(bar) = 1";
437        let predicate = convert_to_iceberg_predicate(sql);
438        assert_eq!(predicate, None);
439    }
440
441    #[test]
442    fn test_predicate_conversion_with_or_condition_unsupported() {
443        let sql = "foo > 1 or length(bar) = 1";
444        let predicate = convert_to_iceberg_predicate(sql);
445        assert_eq!(predicate, None);
446    }
447
448    #[test]
449    fn test_predicate_conversion_with_or_condition_supported() {
450        let sql = "foo > 1 or bar = 'test'";
451        let predicate = convert_to_iceberg_predicate(sql).unwrap();
452        let expected_predicate = Predicate::or(
453            Reference::new("foo").greater_than(Datum::long(1)),
454            Reference::new("bar").equal_to(Datum::string("test")),
455        );
456        assert_eq!(predicate, expected_predicate);
457    }
458
459    #[test]
460    fn test_predicate_conversion_with_complex_binary_expr() {
461        let sql = "(foo > 1 and bar = 'test') or foo < 0 ";
462        let predicate = convert_to_iceberg_predicate(sql).unwrap();
463
464        let inner_predicate = Predicate::and(
465            Reference::new("foo").greater_than(Datum::long(1)),
466            Reference::new("bar").equal_to(Datum::string("test")),
467        );
468        let expected_predicate = Predicate::or(
469            inner_predicate,
470            Reference::new("foo").less_than(Datum::long(0)),
471        );
472        assert_eq!(predicate, expected_predicate);
473    }
474
475    #[test]
476    fn test_predicate_conversion_with_one_and_expr_supported() {
477        let sql = "(foo > 1 and length(bar) = 1 ) or foo < 0 ";
478        let predicate = convert_to_iceberg_predicate(sql).unwrap();
479
480        let inner_predicate = Reference::new("foo").greater_than(Datum::long(1));
481        let expected_predicate = Predicate::or(
482            inner_predicate,
483            Reference::new("foo").less_than(Datum::long(0)),
484        );
485        assert_eq!(predicate, expected_predicate);
486    }
487
488    #[test]
489    fn test_predicate_conversion_with_complex_binary_expr_unsupported() {
490        let sql = "(foo > 1 or length(bar) = 1 ) and foo < 0 ";
491        let predicate = convert_to_iceberg_predicate(sql).unwrap();
492        let expected_predicate = Reference::new("foo").less_than(Datum::long(0));
493        assert_eq!(predicate, expected_predicate);
494    }
495
496    #[test]
497    fn test_predicate_conversion_with_cast() {
498        let sql = "ts >= timestamp '2023-01-05T00:00:00'";
499        let predicate = convert_to_iceberg_predicate(sql).unwrap();
500        let expected_predicate =
501            Reference::new("ts").greater_than_or_equal_to(Datum::string("2023-01-05T00:00:00"));
502        assert_eq!(predicate, expected_predicate);
503    }
504
505    #[test]
506    fn test_predicate_conversion_with_date_cast() {
507        let sql = "ts >= date '2023-01-05T11:00:00'";
508        let predicate = convert_to_iceberg_predicate(sql);
509        assert_eq!(predicate, None);
510    }
511
512    #[test]
513    fn test_scalar_value_to_datum_timestamp() {
514        use datafusion::common::ScalarValue;
515
516        // Test TimestampMicrosecond - maps directly to Datum::timestamp_micros
517        let ts_micros = 1672876800000000i64; // 2023-01-05 00:00:00 UTC in microseconds
518        let datum =
519            super::scalar_value_to_datum(&ScalarValue::TimestampMicrosecond(Some(ts_micros), None));
520        assert_eq!(datum, Some(Datum::timestamp_micros(ts_micros)));
521
522        // Test TimestampNanosecond - maps to Datum::timestamp_nanos to preserve precision
523        let ts_nanos = 1672876800000000500i64; // 2023-01-05 00:00:00.000000500 UTC in nanoseconds
524        let datum =
525            super::scalar_value_to_datum(&ScalarValue::TimestampNanosecond(Some(ts_nanos), None));
526        assert_eq!(datum, Some(Datum::timestamp_nanos(ts_nanos)));
527
528        // Test None timestamp
529        let datum = super::scalar_value_to_datum(&ScalarValue::TimestampMicrosecond(None, None));
530        assert_eq!(datum, None);
531
532        // Note: TimestampSecond and TimestampMillisecond are not supported because
533        // DataFusion's type coercion converts them to TimestampMicrosecond or TimestampNanosecond
534        // before they reach scalar_value_to_datum in SQL queries.
535        //
536        // These return None (not pushed down):
537        let ts_seconds = 1672876800i64; // 2023-01-05 00:00:00 UTC in seconds
538        let datum =
539            super::scalar_value_to_datum(&ScalarValue::TimestampSecond(Some(ts_seconds), None));
540        assert_eq!(datum, None);
541
542        let ts_millis = 1672876800000i64; // 2023-01-05 00:00:00 UTC in milliseconds
543        let datum =
544            super::scalar_value_to_datum(&ScalarValue::TimestampMillisecond(Some(ts_millis), None));
545        assert_eq!(datum, None);
546    }
547
548    #[test]
549    fn test_scalar_value_to_datum_binary() {
550        use datafusion::common::ScalarValue;
551
552        let bytes = vec![1u8, 2u8, 3u8];
553        let datum = super::scalar_value_to_datum(&ScalarValue::Binary(Some(bytes.clone())));
554        assert_eq!(datum, Some(Datum::binary(bytes.clone())));
555
556        let datum = super::scalar_value_to_datum(&ScalarValue::LargeBinary(Some(bytes.clone())));
557        assert_eq!(datum, Some(Datum::binary(bytes)));
558
559        let datum = super::scalar_value_to_datum(&ScalarValue::Binary(None));
560        assert_eq!(datum, None);
561    }
562
563    #[test]
564    fn test_predicate_conversion_with_binary() {
565        let sql = "foo = 1 and bar = X'0102'";
566        let predicate = convert_to_iceberg_predicate(sql).unwrap();
567        // Binary literals are converted to Datum::binary
568        // Note: SQL literal 1 is converted to Long by DataFusion
569        let expected_predicate = Reference::new("foo")
570            .equal_to(Datum::long(1))
571            .and(Reference::new("bar").equal_to(Datum::binary(vec![1u8, 2u8])));
572        assert_eq!(predicate, expected_predicate);
573    }
574
575    #[test]
576    fn test_scalar_value_to_datum_boolean() {
577        use datafusion::common::ScalarValue;
578
579        // Test boolean true
580        let datum = super::scalar_value_to_datum(&ScalarValue::Boolean(Some(true)));
581        assert_eq!(datum, Some(Datum::bool(true)));
582
583        // Test boolean false
584        let datum = super::scalar_value_to_datum(&ScalarValue::Boolean(Some(false)));
585        assert_eq!(datum, Some(Datum::bool(false)));
586
587        // Test None boolean
588        let datum = super::scalar_value_to_datum(&ScalarValue::Boolean(None));
589        assert_eq!(datum, None);
590    }
591
592    #[test]
593    fn test_predicate_conversion_with_like_starts_with() {
594        let sql = "bar LIKE 'test%'";
595        let predicate = convert_to_iceberg_predicate(sql).unwrap();
596        assert_eq!(
597            predicate,
598            Reference::new("bar").starts_with(Datum::string("test"))
599        );
600    }
601
602    #[test]
603    fn test_predicate_conversion_with_not_like_starts_with() {
604        let sql = "bar NOT LIKE 'test%'";
605        let predicate = convert_to_iceberg_predicate(sql).unwrap();
606        assert_eq!(
607            predicate,
608            Reference::new("bar").not_starts_with(Datum::string("test"))
609        );
610    }
611
612    #[test]
613    fn test_predicate_conversion_with_like_empty_prefix() {
614        let sql = "bar LIKE '%'";
615        let predicate = convert_to_iceberg_predicate(sql).unwrap();
616        assert_eq!(
617            predicate,
618            Reference::new("bar").starts_with(Datum::string(""))
619        );
620    }
621
622    #[test]
623    fn test_predicate_conversion_with_like_complex_pattern() {
624        // Patterns with wildcards in the middle cannot be pushed down
625        let sql = "bar LIKE 'te%st'";
626        let predicate = convert_to_iceberg_predicate(sql);
627        assert_eq!(predicate, None);
628    }
629
630    #[test]
631    fn test_predicate_conversion_with_like_underscore_wildcard() {
632        // Patterns with underscore wildcard cannot be pushed down
633        let sql = "bar LIKE 'test_'";
634        let predicate = convert_to_iceberg_predicate(sql);
635        assert_eq!(predicate, None);
636    }
637
638    #[test]
639    fn test_predicate_conversion_with_like_no_wildcard() {
640        // Patterns without trailing % cannot be pushed down as StartsWith
641        let sql = "bar LIKE 'test'";
642        let predicate = convert_to_iceberg_predicate(sql);
643        assert_eq!(predicate, None);
644    }
645
646    #[test]
647    fn test_predicate_conversion_with_ilike() {
648        // Case-insensitive LIKE (ILIKE) is not supported
649        let sql = "bar ILIKE 'test%'";
650        let predicate = convert_to_iceberg_predicate(sql);
651        assert_eq!(predicate, None);
652    }
653
654    #[test]
655    fn test_predicate_conversion_with_like_and_other_conditions() {
656        let sql = "bar LIKE 'test%' AND foo > 1";
657        let predicate = convert_to_iceberg_predicate(sql).unwrap();
658        let expected_predicate = Predicate::and(
659            Reference::new("bar").starts_with(Datum::string("test")),
660            Reference::new("foo").greater_than(Datum::long(1)),
661        );
662        assert_eq!(predicate, expected_predicate);
663    }
664
665    #[test]
666    fn test_predicate_conversion_with_like_special_characters() {
667        // Test LIKE with special characters in prefix
668        let sql = "bar LIKE 'test-abc_123%'";
669        let predicate = convert_to_iceberg_predicate(sql);
670        // This should not be pushed down because it contains underscore
671        assert_eq!(predicate, None);
672    }
673
674    #[test]
675    fn test_predicate_conversion_with_like_unicode() {
676        // Test LIKE with unicode characters in prefix
677        let sql = "bar LIKE '测试%'";
678        let predicate = convert_to_iceberg_predicate(sql).unwrap();
679        assert_eq!(
680            predicate,
681            Reference::new("bar").starts_with(Datum::string("测试"))
682        );
683    }
684}