Skip to main content

polars_sql/
sql_expr.rs

1//! Expressions that are supported by the Polars SQL interface.
2//!
3//! This is useful for syntax highlighting
4//!
5//! This module defines:
6//! - all Polars SQL keywords [`all_keywords`]
7//! - all Polars SQL functions [`all_functions`]
8
9use std::fmt::Display;
10use std::ops::Div;
11
12use polars_core::prelude::*;
13use polars_lazy::prelude::*;
14use polars_plan::plans::DynLiteralValue;
15use polars_plan::prelude::typed_lit;
16use polars_time::Duration;
17use polars_time::chunkedarray::StringMethods;
18use polars_utils::unique_column_name;
19#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21use sqlparser::ast::{
22    AccessExpr, BinaryOperator as SQLBinaryOperator, CastFormat, CastKind, DataType as SQLDataType,
23    DateTimeField, Expr as SQLExpr, Function as SQLFunction, Ident, Interval, Query as Subquery,
24    SelectItem, Subscript, TimezoneInfo, TrimWhereField, TypedString,
25    UnaryOperator as SQLUnaryOperator, Value as SQLValue, ValueWithSpan,
26};
27use sqlparser::dialect::GenericDialect;
28use sqlparser::keywords;
29use sqlparser::parser::{Parser, ParserOptions};
30use sqlparser::tokenizer::Token;
31
32use crate::SQLContext;
33use crate::functions::SQLFunctionVisitor;
34use crate::types::{
35    bitstring_to_bytes_literal, is_iso_date, is_iso_datetime, is_iso_time, map_sql_dtype_to_polars,
36    timeunit_from_precision,
37};
38
39#[inline]
40#[cold]
41#[must_use]
42/// Convert a Display-able error to PolarsError::SQLInterface
43pub fn to_sql_interface_err(err: impl Display) -> PolarsError {
44    PolarsError::SQLInterface(err.to_string().into())
45}
46
47/// Represents a boolean-typed NULL literal (aka: SQL "UNKNOWN" truth value).
48fn sql_unknown() -> Expr {
49    lit(NULL).cast(DataType::Boolean)
50}
51
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)]
54/// Categorises the type of (allowed) subquery constraint
55pub enum SubqueryRestriction {
56    /// Subquery must return a single column
57    SingleColumn,
58    // SingleRow,
59    // SingleValue,
60    // Any
61}
62
63/// Recursively walks a SQL Expr to create a polars Expr
64pub(crate) struct SQLExprVisitor<'a> {
65    ctx: &'a mut SQLContext,
66    active_schema: Option<&'a Schema>,
67}
68
69/// Cast a literal series to the target dtype; uses dedicated ops for
70/// `string → date/time/datetime` and `strict_cast` for everything else.
71fn cast_literal_series(s: &Series, dtype: &DataType) -> PolarsResult<Series> {
72    if s.dtype() == &DataType::String {
73        let ca = s.str()?;
74        match dtype {
75            DataType::Date => return Ok(ca.as_date(None, false)?.into_series()),
76            DataType::Time => return Ok(ca.as_time(None, false)?.into_series()),
77            DataType::Datetime(tu, tz) => {
78                let ambiguous = StringChunked::from_slice(PlSmallStr::EMPTY, &["latest"]);
79                return Ok(ca
80                    .as_datetime(None, *tu, false, false, tz.as_ref(), &ambiguous)?
81                    .into_series());
82            },
83            _ => {},
84        }
85    }
86    s.strict_cast(dtype)
87}
88
89/// Extract the literal value; returns `(sql_value, optional_op)`.
90fn extract_literal_with_op<'a>(
91    expr: &'a SQLExpr,
92    outer_op: Option<&'a SQLUnaryOperator>,
93) -> Option<(&'a SQLValue, Option<&'a SQLUnaryOperator>)> {
94    match expr {
95        SQLExpr::Value(ValueWithSpan { value: v, .. }) => Some((v, outer_op)),
96        SQLExpr::UnaryOp { op, expr } if outer_op.is_none() => match expr.as_ref() {
97            SQLExpr::Value(ValueWithSpan { value: v, .. }) => Some((v, Some(op))),
98            _ => None,
99        },
100        _ => None,
101    }
102}
103
104/// Ensure we have a common dtype from an input set of dtypes (must match).
105fn resolve_common_dtype(dtypes: &[DataType], desc: Option<&str>) -> PolarsResult<Option<DataType>> {
106    let Some(first) = dtypes.first() else {
107        return Ok(None);
108    };
109    let desc = desc.unwrap_or("");
110    for dtype in &dtypes[1..] {
111        polars_ensure!(
112            dtype == first,
113            SQLInterface: "{desc}expected consistent dtypes (found {first:?} and {dtype:?})",
114        );
115    }
116    Ok(Some(first.clone()))
117}
118
119impl SQLExprVisitor<'_> {
120    fn array_expr_to_series(&mut self, elements: &[SQLExpr]) -> PolarsResult<Series> {
121        let mut array_elements = Vec::with_capacity(elements.len());
122        let mut cast_dtypes = Vec::new();
123        for e in elements {
124            let val = match e {
125                SQLExpr::Value(ValueWithSpan { value: v, .. }) => self.visit_any_value(v, None),
126                SQLExpr::UnaryOp { op, expr } => match extract_literal_with_op(expr, Some(op)) {
127                    Some((v, op)) => self.visit_any_value(v, op),
128                    None => match expr.as_ref() {
129                        SQLExpr::Cast {
130                            data_type, expr: inner, format: None, ..
131                        } => {
132                            cast_dtypes.push(map_sql_dtype_to_polars(data_type)?);
133                            match extract_literal_with_op(inner, Some(op)) {
134                                Some((v, op)) => self.visit_any_value(v, op),
135                                None => Err(polars_err!(SQLInterface: "array element {:?} is not supported", e)),
136                            }
137                        },
138                        _ => Err(polars_err!(SQLInterface: "array element {:?} is not supported", e)),
139                    },
140                },
141                SQLExpr::Array(values) => {
142                    let srs = self.array_expr_to_series(&values.elem)?;
143                    Ok(AnyValue::List(srs))
144                },
145                SQLExpr::TypedString(TypedString {
146                    data_type,
147                    value: ValueWithSpan {
148                        value: SQLValue::SingleQuotedString(v), ..
149                    },
150                    ..
151                }) => {
152                    cast_dtypes.push(self.resolve_typed_literal_dtype(data_type, v)?);
153                    Ok(AnyValue::StringOwned(v.as_str().into()))
154                },
155                SQLExpr::Cast {
156                    data_type, expr, format: None, ..
157                } => {
158                    cast_dtypes.push(map_sql_dtype_to_polars(data_type)?);
159                    match extract_literal_with_op(expr, None) {
160                        Some((v, op)) => self.visit_any_value(v, op),
161                        None => Err(polars_err!(SQLInterface: "array element {:?} is not supported", e)),
162                    }
163                },
164                _ => Err(polars_err!(SQLInterface: "array element {:?} is not supported", e)),
165            }?
166            .into_static();
167            array_elements.push(val);
168        }
169        let mut s = Series::from_any_values(PlSmallStr::EMPTY, &array_elements, true)?;
170        if let Some(dtype) = resolve_common_dtype(&cast_dtypes, Some("array literal "))? {
171            s = cast_literal_series(&s, &dtype)?;
172        }
173        Ok(s)
174    }
175
176    fn visit_expr(&mut self, expr: &SQLExpr) -> PolarsResult<Expr> {
177        match expr {
178            SQLExpr::AllOp {
179                left,
180                compare_op,
181                right,
182            } => self.visit_all(left, compare_op, right),
183            SQLExpr::AnyOp {
184                left,
185                compare_op,
186                right,
187                is_some: _,
188            } => self.visit_any(left, compare_op, right),
189            SQLExpr::Array(arr) => self.visit_array_expr(&arr.elem, true, None),
190            SQLExpr::Between {
191                expr,
192                negated,
193                low,
194                high,
195            } => self.visit_between(expr, *negated, low, high),
196            SQLExpr::BinaryOp { left, op, right } => self.visit_binary_op(left, op, right),
197            SQLExpr::Cast {
198                kind,
199                expr,
200                data_type,
201                format,
202                array: _,
203            } => self.visit_cast(expr, data_type, format, kind),
204            SQLExpr::Ceil { expr, .. } => Ok(self.visit_expr(expr)?.ceil()),
205            SQLExpr::CompoundFieldAccess { root, access_chain } => {
206                // simple subscript access (eg: "array_col[1]")
207                if access_chain.len() == 1 {
208                    match &access_chain[0] {
209                        AccessExpr::Subscript(subscript) => {
210                            return self.visit_subscript(root, subscript);
211                        },
212                        AccessExpr::Dot(_) => {
213                            polars_bail!(SQLSyntax: "dot-notation field access is currently unsupported: {:?}", access_chain[0])
214                        },
215                    }
216                }
217                // chained dot/bracket notation (eg: "struct_col.field[2].foo[0].bar")
218                polars_bail!(SQLSyntax: "complex field access chains are currently unsupported: {:?}", access_chain[0])
219            },
220            SQLExpr::CompoundIdentifier(idents) => self.visit_compound_identifier(idents),
221            SQLExpr::Extract {
222                field,
223                syntax: _,
224                expr,
225            } => parse_extract_date_part(self.visit_expr(expr)?, field),
226            SQLExpr::Floor { expr, .. } => Ok(self.visit_expr(expr)?.floor()),
227            SQLExpr::Function(function) => self.visit_function(function),
228            SQLExpr::Identifier(ident) => self.visit_identifier(ident),
229            SQLExpr::InList {
230                expr,
231                list,
232                negated,
233            } => {
234                let expr = self.visit_expr(expr)?;
235                let elems = self.visit_array_expr(list, false, Some(&expr))?;
236                let set_has_null = matches!(
237                    &elems,
238                    Expr::Literal(LiteralValue::Series(s)) if s.null_count() > 0
239                );
240                let membership = expr.is_in(elems.implode(false), false);
241                let is_in = if set_has_null {
242                    // Non-match against sets containing NULL is unknown, not FALSE
243                    membership.or(sql_unknown())
244                } else {
245                    membership
246                };
247                Ok(if *negated { is_in.not() } else { is_in })
248            },
249            SQLExpr::InSubquery {
250                expr,
251                subquery,
252                negated,
253            } => self.visit_in_subquery(expr, subquery, *negated),
254            SQLExpr::Interval(interval) => Ok(lit(interval_to_duration(interval, true)?)),
255            SQLExpr::IsDistinctFrom(e1, e2) => {
256                Ok(self.visit_expr(e1)?.neq_missing(self.visit_expr(e2)?))
257            },
258            SQLExpr::IsFalse(expr) => Ok(self.visit_expr(expr)?.eq(lit(false))),
259            SQLExpr::IsNotDistinctFrom(e1, e2) => {
260                Ok(self.visit_expr(e1)?.eq_missing(self.visit_expr(e2)?))
261            },
262            SQLExpr::IsNotFalse(expr) => Ok(self.visit_expr(expr)?.eq(lit(false)).not()),
263            SQLExpr::IsNotNull(expr) => Ok(self.visit_expr(expr)?.is_not_null()),
264            SQLExpr::IsNotTrue(expr) => Ok(self.visit_expr(expr)?.eq(lit(true)).not()),
265            SQLExpr::IsNull(expr) => Ok(self.visit_expr(expr)?.is_null()),
266            SQLExpr::IsTrue(expr) => Ok(self.visit_expr(expr)?.eq(lit(true))),
267            SQLExpr::Like {
268                negated,
269                any,
270                expr,
271                pattern,
272                escape_char,
273            } => {
274                if *any {
275                    polars_bail!(SQLSyntax: "LIKE ANY is not a supported syntax")
276                }
277                let escape_str = escape_char.as_ref().and_then(|v| match &**v {
278                    SQLValue::SingleQuotedString(s) => Some(s.clone()),
279                    _ => None,
280                });
281                self.visit_like(*negated, expr, pattern, &escape_str, false)
282            },
283            SQLExpr::ILike {
284                negated,
285                any,
286                expr,
287                pattern,
288                escape_char,
289            } => {
290                if *any {
291                    polars_bail!(SQLSyntax: "ILIKE ANY is not a supported syntax")
292                }
293                let escape_str = escape_char.as_ref().and_then(|v| match &**v {
294                    SQLValue::SingleQuotedString(s) => Some(s.clone()),
295                    _ => None,
296                });
297                self.visit_like(*negated, expr, pattern, &escape_str, true)
298            },
299            SQLExpr::Nested(expr) => self.visit_expr(expr),
300            SQLExpr::Position { expr, r#in } => Ok(
301                // note: SQL is 1-indexed
302                (self
303                    .visit_expr(r#in)?
304                    .str()
305                    .find(self.visit_expr(expr)?, true)
306                    + typed_lit(1u32))
307                .fill_null(typed_lit(0u32)),
308            ),
309            SQLExpr::RLike {
310                // note: parses both RLIKE and REGEXP
311                negated,
312                expr,
313                pattern,
314                regexp: _,
315            } => {
316                let matches = self
317                    .visit_expr(expr)?
318                    .str()
319                    .contains(self.visit_expr(pattern)?, true);
320                Ok(if *negated { matches.not() } else { matches })
321            },
322            SQLExpr::Subquery(_) => polars_bail!(SQLInterface: "unexpected subquery"),
323            SQLExpr::Substring {
324                expr,
325                substring_from,
326                substring_for,
327                ..
328            } => self.visit_substring(expr, substring_from.as_deref(), substring_for.as_deref()),
329            SQLExpr::Trim {
330                expr,
331                trim_where,
332                trim_what,
333                trim_characters,
334            } => self.visit_trim(expr, trim_where, trim_what, trim_characters),
335            SQLExpr::TypedString(TypedString {
336                data_type,
337                value:
338                    ValueWithSpan {
339                        value: SQLValue::SingleQuotedString(v),
340                        ..
341                    },
342                uses_odbc_syntax: _,
343            }) => {
344                let dtype = self.resolve_typed_literal_dtype(data_type, v)?;
345                match dtype {
346                    DataType::Date => Ok(lit(v.as_str()).cast(DataType::Date)),
347                    DataType::Time => Ok(lit(v.as_str()).str().to_time(StrptimeOptions {
348                        strict: true,
349                        ..Default::default()
350                    })),
351                    DataType::Datetime(_, _) => Ok(lit(v.as_str()).str().to_datetime(
352                        None,
353                        None,
354                        StrptimeOptions {
355                            strict: true,
356                            ..Default::default()
357                        },
358                        lit("latest"),
359                    )),
360                    _ => unreachable!(),
361                }
362            },
363            SQLExpr::UnaryOp { op, expr } => self.visit_unary_op(op, expr),
364            SQLExpr::Value(ValueWithSpan { value, .. }) => self.visit_literal(value),
365            SQLExpr::Wildcard(_) => Ok(all().as_expr()),
366            e @ SQLExpr::Case { .. } => self.visit_case_when_then(e),
367            other => {
368                polars_bail!(SQLInterface: "expression {:?} is not currently supported", other)
369            },
370        }
371    }
372
373    fn visit_subquery(
374        &mut self,
375        subquery: &Subquery,
376        restriction: SubqueryRestriction,
377    ) -> PolarsResult<Expr> {
378        if subquery.with.is_some() {
379            polars_bail!(SQLSyntax: "SQL subquery cannot be a CTE 'WITH' clause");
380        }
381        // note: we have to execute subqueries in an isolated scope to prevent
382        // propagating any context/arena mutation into the rest of the query
383        let lf = self
384            .ctx
385            .execute_isolated(|ctx| ctx.execute_query_no_ctes(subquery))?;
386
387        if restriction == SubqueryRestriction::SingleColumn {
388            let new_name = unique_column_name();
389            return Ok(Expr::SubPlan(
390                SpecialEq::new(Arc::new(lf.logical_plan)),
391                // TODO: pass the implode depending on expr.
392                vec![(
393                    new_name.clone(),
394                    first().as_expr().implode(true).alias(new_name.clone()),
395                )],
396            ));
397        };
398        polars_bail!(SQLInterface: "subquery type not supported");
399    }
400
401    /// Visit a single SQL identifier.
402    ///
403    /// e.g. column
404    fn visit_identifier(&self, ident: &Ident) -> PolarsResult<Expr> {
405        Ok(col(ident.value.as_str()))
406    }
407
408    /// Visit a compound SQL identifier
409    ///
410    /// e.g. tbl.column, struct.field, tbl.struct.field (inc. nested struct fields)
411    fn visit_compound_identifier(&mut self, idents: &[Ident]) -> PolarsResult<Expr> {
412        Ok(resolve_compound_identifier(self.ctx, idents, self.active_schema)?[0].clone())
413    }
414
415    fn visit_like(
416        &mut self,
417        negated: bool,
418        expr: &SQLExpr,
419        pattern: &SQLExpr,
420        escape_char: &Option<String>,
421        case_insensitive: bool,
422    ) -> PolarsResult<Expr> {
423        if escape_char.is_some() {
424            polars_bail!(SQLInterface: "ESCAPE char for LIKE/ILIKE is not currently supported; found '{}'", escape_char.clone().unwrap());
425        }
426        let pat = match self.visit_expr(pattern) {
427            Ok(Expr::Literal(lv)) if lv.extract_str().is_some() => {
428                PlSmallStr::from_str(lv.extract_str().unwrap())
429            },
430            _ => {
431                polars_bail!(SQLSyntax: "LIKE/ILIKE pattern must be a string literal; found {}", pattern)
432            },
433        };
434        if pat.is_empty() || (!case_insensitive && pat.chars().all(|c| !matches!(c, '%' | '_'))) {
435            // empty string or other exact literal match (eg: no wildcard chars)
436            let op = if negated {
437                SQLBinaryOperator::NotEq
438            } else {
439                SQLBinaryOperator::Eq
440            };
441            self.visit_binary_op(expr, &op, pattern)
442        } else {
443            // create regex from pattern containing SQL wildcard chars ('%' => '.*', '_' => '.')
444            let mut rx = regex::escape(pat.as_str())
445                .replace('%', ".*")
446                .replace('_', ".");
447
448            rx = format!(
449                "^{}{}$",
450                if case_insensitive { "(?is)" } else { "(?s)" },
451                rx
452            );
453
454            let expr = self.visit_expr(expr)?;
455            let matches = expr.str().contains(lit(rx), true);
456            Ok(if negated { matches.not() } else { matches })
457        }
458    }
459
460    fn visit_subscript(&mut self, expr: &SQLExpr, subscript: &Subscript) -> PolarsResult<Expr> {
461        let expr = self.visit_expr(expr)?;
462        Ok(match subscript {
463            Subscript::Index { index } => {
464                let idx = adjust_one_indexed_param(self.visit_expr(index)?, true);
465                expr.list().get(idx, true)
466            },
467            Subscript::Slice { .. } => {
468                polars_bail!(SQLSyntax: "array slice syntax is not currently supported")
469            },
470        })
471    }
472
473    /// Handle implicit temporal string comparisons.
474    ///
475    /// eg: clauses such as -
476    ///   "dt >= '2024-04-30'"
477    ///   "dt = '2077-10-10'::date"
478    ///   "dtm::date = '2077-10-10'
479    fn convert_temporal_strings(&mut self, left: &Expr, right: &Expr) -> Expr {
480        if let (Some(name), Some(s), expr_dtype) = match (left, right) {
481            // identify "col <op> string" expressions
482            (Expr::Column(name), Expr::Literal(lv)) if lv.extract_str().is_some() => {
483                (Some(name.clone()), Some(lv.extract_str().unwrap()), None)
484            },
485            // identify "CAST(expr AS type) <op> string" and/or "expr::type <op> string" expressions
486            (Expr::Cast { expr, dtype, .. }, Expr::Literal(lv)) if lv.extract_str().is_some() => {
487                let s = lv.extract_str().unwrap();
488                match &**expr {
489                    Expr::Column(name) => (Some(name.clone()), Some(s), Some(dtype)),
490                    _ => (None, Some(s), Some(dtype)),
491                }
492            },
493            _ => (None, None, None),
494        } {
495            if expr_dtype.is_none() && self.active_schema.is_none() {
496                right.clone()
497            } else {
498                let left_dtype = expr_dtype.map_or_else(
499                    || {
500                        self.active_schema
501                            .as_ref()
502                            .and_then(|schema| schema.get(&name))
503                    },
504                    |dt| dt.as_literal(),
505                );
506                match left_dtype {
507                    Some(DataType::Time) if is_iso_time(s) => {
508                        right.clone().str().to_time(StrptimeOptions {
509                            strict: true,
510                            ..Default::default()
511                        })
512                    },
513                    Some(DataType::Date) if is_iso_date(s) => {
514                        right.clone().str().to_date(StrptimeOptions {
515                            strict: true,
516                            ..Default::default()
517                        })
518                    },
519                    Some(DataType::Datetime(tu, tz)) if is_iso_datetime(s) || is_iso_date(s) => {
520                        if s.len() == 10 {
521                            // handle upcast from ISO date string (10 chars) to datetime
522                            lit(format!("{s}T00:00:00"))
523                        } else {
524                            lit(s.replacen(' ', "T", 1))
525                        }
526                        .str()
527                        .to_datetime(
528                            Some(*tu),
529                            tz.clone(),
530                            StrptimeOptions {
531                                strict: true,
532                                ..Default::default()
533                            },
534                            lit("latest"),
535                        )
536                    },
537                    _ => right.clone(),
538                }
539            }
540        } else {
541            right.clone()
542        }
543    }
544
545    fn struct_field_access_expr(
546        &mut self,
547        expr: &Expr,
548        path: &str,
549        infer_index: bool,
550    ) -> PolarsResult<Expr> {
551        let path_elems = if path.starts_with('{') && path.ends_with('}') {
552            path.trim_matches(|c| c == '{' || c == '}')
553        } else {
554            path
555        }
556        .split(',');
557
558        let mut expr = expr.clone();
559        for p in path_elems {
560            let p = p.trim();
561            expr = if infer_index {
562                match p.parse::<i64>() {
563                    Ok(idx) => expr.list().get(lit(idx), true),
564                    Err(_) => expr.struct_().field_by_name(p),
565                }
566            } else {
567                expr.struct_().field_by_name(p)
568            }
569        }
570        Ok(expr)
571    }
572
573    /// Visit a SQL binary operator.
574    ///
575    /// e.g. "column + 1", "column1 <= column2"
576    fn visit_binary_op(
577        &mut self,
578        left: &SQLExpr,
579        op: &SQLBinaryOperator,
580        right: &SQLExpr,
581    ) -> PolarsResult<Expr> {
582        // check for (unsupported) scalar subquery comparisons
583        if matches!(left, SQLExpr::Subquery(_)) || matches!(right, SQLExpr::Subquery(_)) {
584            let (suggestion, str_op) = match op {
585                SQLBinaryOperator::NotEq => ("; use 'NOT IN' instead", "!=".to_string()),
586                SQLBinaryOperator::Eq => ("; use 'IN' instead", format!("{op}")),
587                _ => ("", format!("{op}")),
588            };
589            polars_bail!(
590                SQLSyntax: "subquery comparisons with '{str_op}' are not supported{suggestion}"
591            );
592        }
593
594        // need special handling for interval offsets and comparisons
595        let (lhs, mut rhs) = match (left, op, right) {
596            (_, SQLBinaryOperator::Minus, SQLExpr::Interval(v)) => {
597                let duration = interval_to_duration(v, false)?;
598                return Ok(self
599                    .visit_expr(left)?
600                    .dt()
601                    .offset_by(lit(format!("-{duration}"))));
602            },
603            (_, SQLBinaryOperator::Plus, SQLExpr::Interval(v)) => {
604                let duration = interval_to_duration(v, false)?;
605                return Ok(self
606                    .visit_expr(left)?
607                    .dt()
608                    .offset_by(lit(format!("{duration}"))));
609            },
610            (SQLExpr::Interval(v1), _, SQLExpr::Interval(v2)) => {
611                // shortcut interval comparison evaluation (-> bool)
612                let d1 = interval_to_duration(v1, false)?;
613                let d2 = interval_to_duration(v2, false)?;
614                let res = match op {
615                    SQLBinaryOperator::Gt => Ok(lit(d1 > d2)),
616                    SQLBinaryOperator::Lt => Ok(lit(d1 < d2)),
617                    SQLBinaryOperator::GtEq => Ok(lit(d1 >= d2)),
618                    SQLBinaryOperator::LtEq => Ok(lit(d1 <= d2)),
619                    SQLBinaryOperator::NotEq => Ok(lit(d1 != d2)),
620                    SQLBinaryOperator::Eq | SQLBinaryOperator::Spaceship => Ok(lit(d1 == d2)),
621                    _ => polars_bail!(SQLInterface: "invalid interval comparison operator"),
622                };
623                if res.is_ok() {
624                    return res;
625                }
626                (self.visit_expr(left)?, self.visit_expr(right)?)
627            },
628            _ => (self.visit_expr(left)?, self.visit_expr(right)?),
629        };
630        rhs = self.convert_temporal_strings(&lhs, &rhs);
631
632        Ok(match op {
633            // ----
634            // Bitwise operators
635            // ----
636            SQLBinaryOperator::BitwiseAnd => lhs.and(rhs),  // "x & y"
637            SQLBinaryOperator::BitwiseOr => lhs.or(rhs),  // "x | y"
638            SQLBinaryOperator::Xor => lhs.xor(rhs),  // "x XOR y"
639
640            // ----
641            // Comparison operators
642            // ----
643            SQLBinaryOperator::Eq => lhs.eq(rhs),  // "x = y"
644            SQLBinaryOperator::Gt => lhs.gt(rhs),  // "x > y"
645            SQLBinaryOperator::GtEq => lhs.gt_eq(rhs),  // "x >= y"
646            SQLBinaryOperator::Lt => lhs.lt(rhs),  // "x < y"
647            SQLBinaryOperator::LtEq => lhs.lt_eq(rhs),  // "x <= y"
648            SQLBinaryOperator::NotEq => lhs.eq(rhs).not(),  // "x != y"
649            SQLBinaryOperator::Spaceship => lhs.eq_missing(rhs),  // "x <=> y"
650
651            // ----
652            // Logical operators
653            // ----
654            SQLBinaryOperator::And => lhs.and(rhs),  // "x AND y"
655            SQLBinaryOperator::Or => lhs.or(rhs),  // "x OR y"
656
657            // ----
658            // Mathematical operators
659            // ----
660            SQLBinaryOperator::Divide => lhs.true_div(rhs),  // "x / y"
661            SQLBinaryOperator::DuckIntegerDivide => lhs.floor_div(rhs).cast(DataType::Int64),  // "x // y"
662            SQLBinaryOperator::Minus => lhs - rhs,  // "x - y"
663            SQLBinaryOperator::Modulo => lhs % rhs,  // "x % y"
664            SQLBinaryOperator::Multiply => lhs * rhs,  // "x * y"
665            SQLBinaryOperator::Plus => lhs + rhs,  // "x + y"
666
667            // ----
668            // Regular expression operators
669            // ----
670            SQLBinaryOperator::PGRegexMatch => match rhs {  // "x ~ y"
671                Expr::Literal(ref lv) if lv.extract_str().is_some() => lhs.str().contains(rhs, true),
672                _ => polars_bail!(SQLSyntax: "invalid pattern for '~' operator: {:?}", rhs),
673            },
674            SQLBinaryOperator::PGRegexNotMatch => match rhs {  // "x !~ y"
675                Expr::Literal(ref lv) if lv.extract_str().is_some() => lhs.str().contains(rhs, true).not(),
676                _ => polars_bail!(SQLSyntax: "invalid pattern for '!~' operator: {:?}", rhs),
677            },
678            SQLBinaryOperator::PGRegexIMatch => match rhs {  // "x ~* y"
679                Expr::Literal(ref lv) if lv.extract_str().is_some() => {
680                    let pat = lv.extract_str().unwrap();
681                    lhs.str().contains(lit(format!("(?i){pat}")), true)
682                },
683                _ => polars_bail!(SQLSyntax: "invalid pattern for '~*' operator: {:?}", rhs),
684            },
685            SQLBinaryOperator::PGRegexNotIMatch => match rhs {  // "x !~* y"
686                Expr::Literal(ref lv) if lv.extract_str().is_some() => {
687                    let pat = lv.extract_str().unwrap();
688                    lhs.str().contains(lit(format!("(?i){pat}")), true).not()
689                },
690                _ => {
691                    polars_bail!(SQLSyntax: "invalid pattern for '!~*' operator: {:?}", rhs)
692                },
693            },
694            // ----
695            // LIKE/ILIKE operators
696            // ----
697            SQLBinaryOperator::PGLikeMatch  // "x ~~ y"
698            | SQLBinaryOperator::PGNotLikeMatch  // "x !~~ y"
699            | SQLBinaryOperator::PGILikeMatch  // "x ~~* y"
700            | SQLBinaryOperator::PGNotILikeMatch => {  // "x !~~* y"
701                let expr = if matches!(
702                    op,
703                    SQLBinaryOperator::PGLikeMatch | SQLBinaryOperator::PGNotLikeMatch
704                ) {
705                    SQLExpr::Like {
706                        negated: matches!(op, SQLBinaryOperator::PGNotLikeMatch),
707                        any: false,
708                        expr: Box::new(left.clone()),
709                        pattern: Box::new(right.clone()),
710                        escape_char: None,
711                    }
712                } else {
713                    SQLExpr::ILike {
714                        negated: matches!(op, SQLBinaryOperator::PGNotILikeMatch),
715                        any: false,
716                        expr: Box::new(left.clone()),
717                        pattern: Box::new(right.clone()),
718                        escape_char: None,
719                    }
720                };
721                self.visit_expr(&expr)?
722            },
723            // ----
724            // String operators
725            // ----
726            SQLBinaryOperator::PGStartsWith => lhs.str().starts_with(rhs),  // "x ^@ y"
727            SQLBinaryOperator::StringConcat => {  // "x || y"
728                lhs.cast(DataType::String) + rhs.cast(DataType::String)
729            },
730            // ----
731            // Nested (JSON) field access operators
732            // ----
733            SQLBinaryOperator::Arrow | SQLBinaryOperator::LongArrow => match rhs {  // "x -> y", "x ->> y"
734                Expr::Literal(lv) if lv.extract_str().is_some() => {
735                    let path = lv.extract_str().unwrap();
736                    let mut expr = self.struct_field_access_expr(&lhs, path, false)?;
737                    if let SQLBinaryOperator::LongArrow = op {
738                        expr = expr.cast(DataType::String);
739                    }
740                    expr
741                },
742                Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(idx))) => {
743                    let mut expr = self.struct_field_access_expr(&lhs, &idx.to_string(), true)?;
744                    if let SQLBinaryOperator::LongArrow = op {
745                        expr = expr.cast(DataType::String);
746                    }
747                    expr
748                },
749                _ => {
750                    polars_bail!(SQLSyntax: "invalid json/struct path-extract definition: {:?}", right)
751                },
752            },
753            SQLBinaryOperator::HashArrow | SQLBinaryOperator::HashLongArrow => {  // "x #> y", "x #>> y"
754                match rhs {
755                    Expr::Literal(lv) if lv.extract_str().is_some() => {
756                        let path = lv.extract_str().unwrap();
757                        let mut expr = self.struct_field_access_expr(&lhs, path, true)?;
758                        if let SQLBinaryOperator::HashLongArrow = op {
759                            expr = expr.cast(DataType::String);
760                        }
761                        expr
762                    },
763                    _ => {
764                        polars_bail!(SQLSyntax: "invalid json/struct path-extract definition: {:?}", rhs)
765                    }
766                }
767            },
768            other => {
769                polars_bail!(SQLInterface: "operator {:?} is not currently supported", other)
770            },
771        })
772    }
773
774    /// Visit a SQL unary operator.
775    ///
776    /// e.g. +column or -column
777    fn visit_unary_op(&mut self, op: &SQLUnaryOperator, expr: &SQLExpr) -> PolarsResult<Expr> {
778        let expr = self.visit_expr(expr)?;
779        Ok(match (op, expr.clone()) {
780            // simplify the parse tree by special-casing common unary +/- ops
781            (SQLUnaryOperator::Plus, Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n)))) => {
782                lit(n)
783            },
784            (
785                SQLUnaryOperator::Plus,
786                Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Float(n))),
787            ) => lit(n),
788            (
789                SQLUnaryOperator::Minus,
790                Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))),
791            ) => lit(-n),
792            (
793                SQLUnaryOperator::Minus,
794                Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Float(n))),
795            ) => lit(-n),
796            // general case
797            (SQLUnaryOperator::Plus, _) => lit(0) + expr,
798            (SQLUnaryOperator::Minus, _) => lit(0) - expr,
799            (SQLUnaryOperator::Not, _) => match &expr {
800                Expr::Column(name)
801                    if self
802                        .active_schema
803                        .and_then(|schema| schema.get(name))
804                        .is_some_and(|dtype| matches!(dtype, DataType::Boolean)) =>
805                {
806                    // if already boolean, can operate bitwise
807                    expr.not()
808                },
809                // otherwise SQL "NOT" expects logical, not bitwise, behaviour (eg: on integers)
810                _ => expr.strict_cast(DataType::Boolean).not(),
811            },
812            other => polars_bail!(SQLInterface: "unary operator {:?} is not supported", other),
813        })
814    }
815
816    /// Visit a SQL function.
817    ///
818    /// e.g. SUM(column) or COUNT(*)
819    ///
820    /// See [SQLFunctionVisitor] for more details
821    fn visit_function(&mut self, function: &SQLFunction) -> PolarsResult<Expr> {
822        let mut visitor = SQLFunctionVisitor {
823            func: function,
824            ctx: self.ctx,
825            active_schema: self.active_schema,
826            filter: None,
827        };
828        visitor.visit_function()
829    }
830
831    /// Visit a SQL `ALL` expression.
832    ///
833    /// e.g. `a > ALL(y)`
834    fn visit_all(
835        &mut self,
836        left: &SQLExpr,
837        compare_op: &SQLBinaryOperator,
838        right: &SQLExpr,
839    ) -> PolarsResult<Expr> {
840        let left = self.visit_expr(left)?;
841        let right = self.visit_expr(right)?;
842
843        match compare_op {
844            SQLBinaryOperator::Gt => Ok(left.gt(right.max())),
845            SQLBinaryOperator::Lt => Ok(left.lt(right.min())),
846            SQLBinaryOperator::GtEq => Ok(left.gt_eq(right.max())),
847            SQLBinaryOperator::LtEq => Ok(left.lt_eq(right.min())),
848            SQLBinaryOperator::Eq => polars_bail!(SQLSyntax: "ALL cannot be used with ="),
849            SQLBinaryOperator::NotEq => polars_bail!(SQLSyntax: "ALL cannot be used with !="),
850            _ => polars_bail!(SQLInterface: "invalid comparison operator"),
851        }
852    }
853
854    /// Visit a SQL `ANY` expression.
855    ///
856    /// e.g. `a != ANY(y)`
857    fn visit_any(
858        &mut self,
859        left: &SQLExpr,
860        compare_op: &SQLBinaryOperator,
861        right: &SQLExpr,
862    ) -> PolarsResult<Expr> {
863        let left = self.visit_expr(left)?;
864        let right = self.visit_expr(right)?;
865
866        match compare_op {
867            SQLBinaryOperator::Gt => Ok(left.gt(right.min())),
868            SQLBinaryOperator::Lt => Ok(left.lt(right.max())),
869            SQLBinaryOperator::GtEq => Ok(left.gt_eq(right.min())),
870            SQLBinaryOperator::LtEq => Ok(left.lt_eq(right.max())),
871            SQLBinaryOperator::Eq => Ok(left.is_in(right, false)),
872            SQLBinaryOperator::NotEq => Ok(left.is_in(right, false).not()),
873            _ => polars_bail!(SQLInterface: "invalid comparison operator"),
874        }
875    }
876
877    /// Visit a SQL `ARRAY` list (including `IN` values).
878    fn visit_array_expr(
879        &mut self,
880        elements: &[SQLExpr],
881        result_as_element: bool,
882        dtype_expr_match: Option<&Expr>,
883    ) -> PolarsResult<Expr> {
884        let mut elems = self.array_expr_to_series(elements)?;
885
886        // handle implicit temporal strings, eg: "dt IN ('2024-04-30','2024-05-01')".
887        // (not yet as versatile as the temporal string conversions in visit_binary_op)
888        if let (Some(Expr::Column(name)), Some(schema)) =
889            (dtype_expr_match, self.active_schema.as_ref())
890        {
891            if elems.dtype() == &DataType::String {
892                if let Some(dtype) = schema.get(name) {
893                    if matches!(
894                        dtype,
895                        DataType::Date | DataType::Time | DataType::Datetime(_, _)
896                    ) {
897                        elems = elems.strict_cast(dtype)?;
898                    }
899                }
900            }
901        }
902
903        // if we are parsing the list as an element in a series, implode.
904        // otherwise, return the series as-is.
905        let res = if result_as_element {
906            elems.implode()?.into_series()
907        } else {
908            elems
909        };
910        Ok(lit(res))
911    }
912
913    /// Visit a SQL `CAST` or `TRY_CAST` expression.
914    ///
915    /// e.g. `CAST(col AS INT)`, `col::int4`, or `TRY_CAST(col AS VARCHAR)`,
916    fn visit_cast(
917        &mut self,
918        expr: &SQLExpr,
919        dtype: &SQLDataType,
920        format: &Option<CastFormat>,
921        cast_kind: &CastKind,
922    ) -> PolarsResult<Expr> {
923        if format.is_some() {
924            return Err(
925                polars_err!(SQLInterface: "use of FORMAT is not currently supported in CAST"),
926            );
927        }
928        let expr = self.visit_expr(expr)?;
929
930        #[cfg(feature = "json")]
931        if dtype == &SQLDataType::JSON {
932            // @BROKEN: we cannot handle this.
933            return Ok(expr.str().json_decode(DataType::Struct(Vec::new())));
934        }
935        let polars_type = map_sql_dtype_to_polars(dtype)?;
936        Ok(match cast_kind {
937            CastKind::Cast | CastKind::DoubleColon => expr.strict_cast(polars_type),
938            CastKind::TryCast | CastKind::SafeCast => expr.cast(polars_type),
939        })
940    }
941
942    /// Visit a SQL literal.
943    ///
944    /// e.g. 1, 'foo', 1.0, NULL
945    ///
946    /// See [SQLValue] and [LiteralValue] for more details
947    fn visit_literal(&self, value: &SQLValue) -> PolarsResult<Expr> {
948        // note: double-quoted strings will be parsed as identifiers, not literals
949        Ok(match value {
950            SQLValue::Boolean(b) => lit(*b),
951            SQLValue::DollarQuotedString(s) => lit(s.value.clone()),
952            #[cfg(feature = "binary_encoding")]
953            SQLValue::HexStringLiteral(x) => {
954                if x.len() % 2 != 0 {
955                    polars_bail!(SQLSyntax: "hex string literal must have an even number of digits; found '{}'", x)
956                };
957                lit(hex::decode(x.clone())
958                    .map_err(|_| polars_err!(SQLSyntax: "invalid hex string literal: '{}'", x))?)
959            },
960            SQLValue::Null => Expr::Literal(LiteralValue::untyped_null()),
961            SQLValue::Number(s, _) => {
962                // Check for existence of decimal separator dot
963                if s.contains('.') {
964                    s.parse::<f64>().map(lit).map_err(|_| ())
965                } else {
966                    s.parse::<i64>().map(lit).map_err(|_| ())
967                }
968                .map_err(|_| polars_err!(SQLInterface: "cannot parse literal: {:?}", s))?
969            },
970            SQLValue::SingleQuotedByteStringLiteral(b) => {
971                // note: for PostgreSQL this represents a BIT string literal (eg: b'10101') not a BYTE string
972                // literal (see https://www.postgresql.org/docs/current/datatype-bit.html), but sqlparser-rs
973                // patterned the token name after BigQuery (where b'str' really IS a byte string)
974                bitstring_to_bytes_literal(b)?
975            },
976            SQLValue::SingleQuotedString(s) => lit(s.clone()),
977            other => {
978                polars_bail!(SQLInterface: "value {:?} is not a supported literal type", other)
979            },
980        })
981    }
982
983    /// Visit a SQL literal (like [visit_literal]), but return AnyValue instead of Expr.
984    fn visit_any_value(
985        &self,
986        value: &SQLValue,
987        op: Option<&SQLUnaryOperator>,
988    ) -> PolarsResult<AnyValue<'_>> {
989        Ok(match value {
990            SQLValue::Boolean(b) => AnyValue::Boolean(*b),
991            SQLValue::DollarQuotedString(s) => AnyValue::StringOwned(s.clone().value.into()),
992            #[cfg(feature = "binary_encoding")]
993            SQLValue::HexStringLiteral(x) => {
994                if x.len() % 2 != 0 {
995                    polars_bail!(SQLSyntax: "hex string literal must have an even number of digits; found '{}'", x)
996                };
997                AnyValue::BinaryOwned(
998                    hex::decode(x.clone()).map_err(
999                        |_| polars_err!(SQLSyntax: "invalid hex string literal: '{}'", x),
1000                    )?,
1001                )
1002            },
1003            SQLValue::Null => AnyValue::Null,
1004            SQLValue::Number(s, _) => {
1005                let negate = match op {
1006                    Some(SQLUnaryOperator::Minus) => true,
1007                    // no op should be taken as plus.
1008                    Some(SQLUnaryOperator::Plus) | None => false,
1009                    Some(op) => {
1010                        polars_bail!(SQLInterface: "unary op {:?} not supported for numeric SQL value", op)
1011                    },
1012                };
1013                // Check for existence of decimal separator dot
1014                if s.contains('.') {
1015                    s.parse::<f64>()
1016                        .map(|n: f64| AnyValue::Float64(if negate { -n } else { n }))
1017                        .map_err(|_| ())
1018                } else {
1019                    s.parse::<i64>()
1020                        .map(|n: i64| AnyValue::Int64(if negate { -n } else { n }))
1021                        .map_err(|_| ())
1022                }
1023                .map_err(|_| polars_err!(SQLInterface: "cannot parse literal: {:?}", s))?
1024            },
1025            SQLValue::SingleQuotedByteStringLiteral(b) => {
1026                // note: for PostgreSQL this represents a BIT literal (eg: b'10101') not BYTE
1027                let bytes_literal = bitstring_to_bytes_literal(b)?;
1028                match bytes_literal {
1029                    Expr::Literal(lv) if lv.extract_binary().is_some() => {
1030                        AnyValue::BinaryOwned(lv.extract_binary().unwrap().to_vec())
1031                    },
1032                    _ => {
1033                        polars_bail!(SQLInterface: "failed to parse bitstring literal: {:?}", b)
1034                    },
1035                }
1036            },
1037            SQLValue::SingleQuotedString(s) => AnyValue::StringOwned(s.as_str().into()),
1038            other => polars_bail!(SQLInterface: "value {:?} is not currently supported", other),
1039        })
1040    }
1041
1042    /// Validate a typed string literal (e.g. DATE '2024-01-01'), returning its dtype.
1043    fn resolve_typed_literal_dtype(
1044        &self,
1045        data_type: &SQLDataType,
1046        value: &str,
1047    ) -> PolarsResult<DataType> {
1048        match data_type {
1049            SQLDataType::Date => {
1050                polars_ensure!(is_iso_date(value), SQLSyntax: "invalid DATE literal '{}'", value);
1051                Ok(DataType::Date)
1052            },
1053            SQLDataType::Time(None, TimezoneInfo::None) => {
1054                polars_ensure!(is_iso_time(value), SQLSyntax: "invalid TIME literal '{}'", value);
1055                Ok(DataType::Time)
1056            },
1057            SQLDataType::Timestamp(prec, TimezoneInfo::None) | SQLDataType::Datetime(prec) => {
1058                let fn_name = match data_type {
1059                    SQLDataType::Timestamp(_, _) => "TIMESTAMP",
1060                    _ => "DATETIME",
1061                };
1062                polars_ensure!(
1063                    is_iso_datetime(value),
1064                    SQLSyntax: "invalid {} literal '{}'", fn_name, value,
1065                );
1066                Ok(DataType::Datetime(timeunit_from_precision(prec)?, None))
1067            },
1068            _ => {
1069                polars_bail!(SQLInterface: "typed literal should be one of DATE, DATETIME, TIME, or TIMESTAMP (found {})", data_type)
1070            },
1071        }
1072    }
1073
1074    /// Visit a SQL `BETWEEN` expression.
1075    /// See [sqlparser::ast::Expr::Between] for more details
1076    fn visit_between(
1077        &mut self,
1078        expr: &SQLExpr,
1079        negated: bool,
1080        low: &SQLExpr,
1081        high: &SQLExpr,
1082    ) -> PolarsResult<Expr> {
1083        let expr = self.visit_expr(expr)?;
1084        let low = self.visit_expr(low)?;
1085        let high = self.visit_expr(high)?;
1086
1087        let low = self.convert_temporal_strings(&expr, &low);
1088        let high = self.convert_temporal_strings(&expr, &high);
1089        Ok(if negated {
1090            expr.clone().lt(low).or(expr.gt(high))
1091        } else {
1092            expr.clone().gt_eq(low).and(expr.lt_eq(high))
1093        })
1094    }
1095
1096    /// Visit a SQL `TRIM` function.
1097    /// See [sqlparser::ast::Expr::Trim] for more details
1098    fn visit_trim(
1099        &mut self,
1100        expr: &SQLExpr,
1101        trim_where: &Option<TrimWhereField>,
1102        trim_what: &Option<Box<SQLExpr>>,
1103        trim_characters: &Option<Vec<SQLExpr>>,
1104    ) -> PolarsResult<Expr> {
1105        if trim_characters.is_some() {
1106            // TODO: allow compact snowflake/bigquery syntax?
1107            return Err(polars_err!(SQLSyntax: "unsupported TRIM syntax (custom chars)"));
1108        };
1109        let expr = self.visit_expr(expr)?;
1110        let trim_what = trim_what.as_ref().map(|e| self.visit_expr(e)).transpose()?;
1111        let trim_what = match trim_what {
1112            Some(Expr::Literal(lv)) if lv.extract_str().is_some() => {
1113                Some(PlSmallStr::from_str(lv.extract_str().unwrap()))
1114            },
1115            None => None,
1116            _ => return self.err(&expr),
1117        };
1118        Ok(match (trim_where, trim_what) {
1119            (None | Some(TrimWhereField::Both), None) => {
1120                expr.str().strip_chars(lit(LiteralValue::untyped_null()))
1121            },
1122            (None | Some(TrimWhereField::Both), Some(val)) => expr.str().strip_chars(lit(val)),
1123            (Some(TrimWhereField::Leading), None) => expr
1124                .str()
1125                .strip_chars_start(lit(LiteralValue::untyped_null())),
1126            (Some(TrimWhereField::Leading), Some(val)) => expr.str().strip_chars_start(lit(val)),
1127            (Some(TrimWhereField::Trailing), None) => expr
1128                .str()
1129                .strip_chars_end(lit(LiteralValue::untyped_null())),
1130            (Some(TrimWhereField::Trailing), Some(val)) => expr.str().strip_chars_end(lit(val)),
1131        })
1132    }
1133
1134    fn visit_substring(
1135        &mut self,
1136        expr: &SQLExpr,
1137        substring_from: Option<&SQLExpr>,
1138        substring_for: Option<&SQLExpr>,
1139    ) -> PolarsResult<Expr> {
1140        let e = self.visit_expr(expr)?;
1141
1142        match (substring_from, substring_for) {
1143            // SUBSTRING(expr FROM start FOR length)
1144            (Some(from_expr), Some(for_expr)) => {
1145                let start = self.visit_expr(from_expr)?;
1146                let length = self.visit_expr(for_expr)?;
1147
1148                // note: SQL is 1-indexed, so we need to adjust the offsets accordingly
1149                Ok(match (start.clone(), length.clone()) {
1150                    (Expr::Literal(lv), _) | (_, Expr::Literal(lv)) if lv.is_null() => lit(lv),
1151                    (_, Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n)))) if n < 0 => {
1152                        polars_bail!(SQLSyntax: "SUBSTR does not support negative length ({})", n)
1153                    },
1154                    (Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))), _) if n > 0 => {
1155                        e.str().slice(lit(n - 1), length)
1156                    },
1157                    (Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))), _) => e
1158                        .str()
1159                        .slice(lit(0), (length + lit(n - 1)).clip_min(lit(0))),
1160                    (Expr::Literal(_), _) => {
1161                        polars_bail!(SQLSyntax: "invalid 'start' for SUBSTRING")
1162                    },
1163                    (_, Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Float(_)))) => {
1164                        polars_bail!(SQLSyntax: "invalid 'length' for SUBSTRING")
1165                    },
1166                    _ => {
1167                        let adjusted_start = start - lit(1);
1168                        when(adjusted_start.clone().lt(lit(0)))
1169                            .then(e.clone().str().slice(
1170                                lit(0),
1171                                (length.clone() + adjusted_start.clone()).clip_min(lit(0)),
1172                            ))
1173                            .otherwise(e.str().slice(adjusted_start, length))
1174                    },
1175                })
1176            },
1177            // SUBSTRING(expr FROM start)
1178            (Some(from_expr), None) => {
1179                let start = self.visit_expr(from_expr)?;
1180
1181                Ok(match start {
1182                    Expr::Literal(lv) if lv.is_null() => lit(lv),
1183                    Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))) if n <= 0 => e,
1184                    Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))) => {
1185                        e.str().slice(lit(n - 1), lit(LiteralValue::untyped_null()))
1186                    },
1187                    Expr::Literal(_) => {
1188                        polars_bail!(SQLSyntax: "invalid 'start' for SUBSTRING")
1189                    },
1190                    _ => e
1191                        .str()
1192                        .slice(start - lit(1), lit(LiteralValue::untyped_null())),
1193                })
1194            },
1195            // SUBSTRING(expr) - not valid, but handle gracefully
1196            (None, _) => {
1197                polars_bail!(SQLSyntax: "SUBSTR expects 2-3 arguments (found 1)")
1198            },
1199        }
1200    }
1201
1202    /// Visit a SQL subquery inside an `IN` expression.
1203    fn visit_in_subquery(
1204        &mut self,
1205        expr: &SQLExpr,
1206        subquery: &Subquery,
1207        negated: bool,
1208    ) -> PolarsResult<Expr> {
1209        let subquery_result = self.visit_subquery(subquery, SubqueryRestriction::SingleColumn)?;
1210        let expr = self.visit_expr(expr)?;
1211        let Expr::SubPlan(_, cols) = &subquery_result else {
1212            unreachable!("SingleColumn subquery must lower to a SubPlan");
1213        };
1214        let value_set = col(cols[0].0.clone()).first();
1215        let membership = expr.is_in(subquery_result, false);
1216        let set_has_null = value_set.clone().list().contains(lit(NULL), true);
1217        let set_is_empty = value_set.list().len().eq(lit(0u32));
1218        let is_in = when(set_is_empty)
1219            .then(lit(false))
1220            .otherwise(membership.or(set_has_null.and(sql_unknown())));
1221
1222        Ok(if negated { is_in.not() } else { is_in })
1223    }
1224
1225    /// Visit `CASE` control flow expression.
1226    fn visit_case_when_then(&mut self, expr: &SQLExpr) -> PolarsResult<Expr> {
1227        if let SQLExpr::Case {
1228            case_token: _,
1229            end_token: _,
1230            operand,
1231            conditions,
1232            else_result,
1233        } = expr
1234        {
1235            polars_ensure!(
1236                !conditions.is_empty(),
1237                SQLSyntax: "WHEN and THEN expressions must have at least one element"
1238            );
1239
1240            let mut when_thens = conditions.iter();
1241            let first = when_thens.next();
1242            if first.is_none() {
1243                polars_bail!(SQLSyntax: "WHEN and THEN expressions must have at least one element");
1244            }
1245            let else_res = match else_result {
1246                Some(else_res) => self.visit_expr(else_res)?,
1247                None => lit(LiteralValue::untyped_null()), // ELSE clause is optional; when omitted, it is implicitly NULL
1248            };
1249            if let Some(operand_expr) = operand {
1250                let first_operand_expr = self.visit_expr(operand_expr)?;
1251
1252                let first = first.unwrap();
1253                let first_cond = first_operand_expr.eq(self.visit_expr(&first.condition)?);
1254                let first_then = self.visit_expr(&first.result)?;
1255                let expr = when(first_cond).then(first_then);
1256                let next = when_thens.next();
1257
1258                let mut when_then = if let Some(case_when) = next {
1259                    let second_operand_expr = self.visit_expr(operand_expr)?;
1260                    let cond = second_operand_expr.eq(self.visit_expr(&case_when.condition)?);
1261                    let res = self.visit_expr(&case_when.result)?;
1262                    expr.when(cond).then(res)
1263                } else {
1264                    return Ok(expr.otherwise(else_res));
1265                };
1266                for case_when in when_thens {
1267                    let new_operand_expr = self.visit_expr(operand_expr)?;
1268                    let cond = new_operand_expr.eq(self.visit_expr(&case_when.condition)?);
1269                    let res = self.visit_expr(&case_when.result)?;
1270                    when_then = when_then.when(cond).then(res);
1271                }
1272                return Ok(when_then.otherwise(else_res));
1273            }
1274
1275            let first = first.unwrap();
1276            let first_cond = self.visit_expr(&first.condition)?;
1277            let first_then = self.visit_expr(&first.result)?;
1278            let expr = when(first_cond).then(first_then);
1279            let next = when_thens.next();
1280
1281            let mut when_then = if let Some(case_when) = next {
1282                let cond = self.visit_expr(&case_when.condition)?;
1283                let res = self.visit_expr(&case_when.result)?;
1284                expr.when(cond).then(res)
1285            } else {
1286                return Ok(expr.otherwise(else_res));
1287            };
1288            for case_when in when_thens {
1289                let cond = self.visit_expr(&case_when.condition)?;
1290                let res = self.visit_expr(&case_when.result)?;
1291                when_then = when_then.when(cond).then(res);
1292            }
1293            Ok(when_then.otherwise(else_res))
1294        } else {
1295            unreachable!()
1296        }
1297    }
1298
1299    fn err(&self, expr: &Expr) -> PolarsResult<Expr> {
1300        polars_bail!(SQLInterface: "expression {:?} is not currently supported", expr);
1301    }
1302}
1303
1304/// parse a SQL expression to a polars expression
1305/// # Example
1306/// ```rust
1307/// # use polars_sql::{SQLContext, sql_expr};
1308/// # use polars_core::prelude::*;
1309/// # use polars_lazy::prelude::*;
1310/// # fn main() {
1311///
1312/// let mut ctx = SQLContext::new();
1313/// let df = df! {
1314///    "a" =>  [1, 2, 3],
1315/// }
1316/// .unwrap();
1317/// let expr = sql_expr("MAX(a)").unwrap();
1318/// df.lazy().select(vec![expr]).collect().unwrap();
1319/// # }
1320/// ```
1321pub fn sql_expr<S: AsRef<str>>(s: S) -> PolarsResult<Expr> {
1322    let mut ctx = SQLContext::new();
1323    let s = s.as_ref();
1324
1325    let mut parser = Parser::new(&GenericDialect);
1326    parser = parser.with_options(ParserOptions {
1327        trailing_commas: true,
1328        ..Default::default()
1329    });
1330
1331    // `sql_expr` should only translate expressions, not statements or clauses
1332    let mut ast = parser.try_with_sql(s).map_err(to_sql_interface_err)?;
1333    if let Token::Word(word) = &ast.peek_token().token {
1334        if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&word.keyword) {
1335            polars_bail!(SQLInterface: "expected an expression (found '{}' clause)", word.value)
1336        }
1337    }
1338    let expr = ast
1339        .parse_select_item()
1340        .map_err(|_| polars_err!(SQLInterface: "unable to parse '{}' as Expr", s))?;
1341
1342    // ensure all input was consumed; remaining tokens indicate invalid trailing SQL
1343    match &ast.peek_token().token {
1344        Token::EOF => {},
1345        Token::Word(word) if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&word.keyword) => {
1346            polars_bail!(SQLInterface: "expected an expression (found '{}' clause)", word.value)
1347        },
1348        token => {
1349            polars_bail!(SQLInterface: "invalid expression (found unexpected token '{}')", token)
1350        },
1351    }
1352    Ok(match &expr {
1353        SelectItem::ExprWithAlias { expr, alias } => {
1354            let expr = parse_sql_expr(expr, &mut ctx, None)?;
1355            expr.alias(alias.value.as_str())
1356        },
1357        SelectItem::UnnamedExpr(expr) => parse_sql_expr(expr, &mut ctx, None)?,
1358        _ => polars_bail!(SQLInterface: "unable to parse '{}' as Expr", s),
1359    })
1360}
1361
1362pub(crate) fn interval_to_duration(interval: &Interval, fixed: bool) -> PolarsResult<Duration> {
1363    if interval.last_field.is_some()
1364        || interval.leading_field.is_some()
1365        || interval.leading_precision.is_some()
1366        || interval.fractional_seconds_precision.is_some()
1367    {
1368        polars_bail!(SQLSyntax: "unsupported interval syntax ('{}')", interval)
1369    }
1370    let s = match &*interval.value {
1371        SQLExpr::UnaryOp { .. } => {
1372            polars_bail!(SQLSyntax: "unary ops are not valid on interval strings; found {}", interval.value)
1373        },
1374        SQLExpr::Value(ValueWithSpan {
1375            value: SQLValue::SingleQuotedString(s),
1376            ..
1377        }) => Some(s),
1378        _ => None,
1379    };
1380    match s {
1381        Some(s) if s.contains('-') => {
1382            polars_bail!(SQLInterface: "minus signs are not yet supported in interval strings; found '{}'", s)
1383        },
1384        Some(s) => {
1385            // years, quarters, and months do not have a fixed duration; these
1386            // interval parts can only be used with respect to a reference point
1387            let duration = Duration::parse_interval(s);
1388            if fixed && duration.months() != 0 {
1389                polars_bail!(SQLSyntax: "fixed-duration interval cannot contain years, quarters, or months; found {}", s)
1390            };
1391            Ok(duration)
1392        },
1393        None => polars_bail!(SQLSyntax: "invalid interval {:?}", interval),
1394    }
1395}
1396
1397pub(crate) fn parse_sql_expr(
1398    expr: &SQLExpr,
1399    ctx: &mut SQLContext,
1400    active_schema: Option<&Schema>,
1401) -> PolarsResult<Expr> {
1402    let mut visitor = SQLExprVisitor { ctx, active_schema };
1403    visitor.visit_expr(expr)
1404}
1405
1406pub(crate) fn parse_sql_array(expr: &SQLExpr, ctx: &mut SQLContext) -> PolarsResult<Series> {
1407    match expr {
1408        SQLExpr::Array(arr) => {
1409            let mut visitor = SQLExprVisitor {
1410                ctx,
1411                active_schema: None,
1412            };
1413            visitor.array_expr_to_series(arr.elem.as_slice())
1414        },
1415        _ => polars_bail!(SQLSyntax: "Expected array expression, found {:?}", expr),
1416    }
1417}
1418
1419pub(crate) fn parse_extract_date_part(expr: Expr, field: &DateTimeField) -> PolarsResult<Expr> {
1420    let field = match field {
1421        // handle 'DATE_PART' and all valid abbreviations/alternates
1422        DateTimeField::Custom(Ident { value, .. }) => {
1423            let value = value.to_ascii_lowercase();
1424            match value.as_str() {
1425                "millennium" | "millennia" => &DateTimeField::Millennium,
1426                "century" | "centuries" | "c" => &DateTimeField::Century,
1427                "decade" | "decades" => &DateTimeField::Decade,
1428                "isoyear" => &DateTimeField::Isoyear,
1429                "year" | "years" | "y" => &DateTimeField::Year,
1430                "quarter" | "quarters" => &DateTimeField::Quarter,
1431                "month" | "months" | "mon" | "mons" => &DateTimeField::Month,
1432                "dayofyear" | "doy" => &DateTimeField::DayOfYear,
1433                "dayofweek" | "dow" => &DateTimeField::DayOfWeek,
1434                "isoweek" | "week" | "weeks" => &DateTimeField::IsoWeek,
1435                "isodow" => &DateTimeField::Isodow,
1436                "day" | "days" | "dayofmonth" | "d" => &DateTimeField::Day,
1437                "hour" | "hours" | "h" => &DateTimeField::Hour,
1438                "minute" | "minutes" | "mins" | "min" | "m" => &DateTimeField::Minute,
1439                "second" | "seconds" | "sec" | "secs" | "s" => &DateTimeField::Second,
1440                "millisecond" | "milliseconds" | "ms" => &DateTimeField::Millisecond,
1441                "microsecond" | "microseconds" | "us" => &DateTimeField::Microsecond,
1442                "nanosecond" | "nanoseconds" | "ns" => &DateTimeField::Nanosecond,
1443                #[cfg(feature = "timezones")]
1444                "timezone" => &DateTimeField::Timezone,
1445                "time" => &DateTimeField::Time,
1446                "epoch" => &DateTimeField::Epoch,
1447                _ => {
1448                    polars_bail!(SQLSyntax: "EXTRACT/DATE_PART does not support '{}' part", value)
1449                },
1450            }
1451        },
1452        _ => field,
1453    };
1454    Ok(match field {
1455        DateTimeField::Millennium => expr.dt().millennium(),
1456        DateTimeField::Century => expr.dt().century(),
1457        DateTimeField::Decade => expr.dt().year() / typed_lit(10i32),
1458        DateTimeField::Isoyear => expr.dt().iso_year(),
1459        DateTimeField::Year | DateTimeField::Years => expr.dt().year(),
1460        DateTimeField::Quarter => expr.dt().quarter(),
1461        DateTimeField::Month | DateTimeField::Months => expr.dt().month(),
1462        DateTimeField::Week(weekday) => {
1463            if weekday.is_some() {
1464                polars_bail!(SQLSyntax: "EXTRACT/DATE_PART does not support '{}' part", field)
1465            }
1466            expr.dt().week()
1467        },
1468        DateTimeField::IsoWeek | DateTimeField::Weeks => expr.dt().week(),
1469        DateTimeField::DayOfYear | DateTimeField::Doy => expr.dt().ordinal_day(),
1470        DateTimeField::DayOfWeek | DateTimeField::Dow => {
1471            let w = expr.dt().weekday();
1472            when(w.clone().eq(typed_lit(7i8)))
1473                .then(typed_lit(0i8))
1474                .otherwise(w)
1475        },
1476        DateTimeField::Isodow => expr.dt().weekday(),
1477        DateTimeField::Day | DateTimeField::Days => expr.dt().day(),
1478        DateTimeField::Hour | DateTimeField::Hours => expr.dt().hour(),
1479        DateTimeField::Minute | DateTimeField::Minutes => expr.dt().minute(),
1480        DateTimeField::Second | DateTimeField::Seconds => expr.dt().second(),
1481        DateTimeField::Millisecond | DateTimeField::Milliseconds => {
1482            (expr.clone().dt().second() * typed_lit(1_000f64))
1483                + expr.dt().nanosecond().div(typed_lit(1_000_000f64))
1484        },
1485        DateTimeField::Microsecond | DateTimeField::Microseconds => {
1486            (expr.clone().dt().second() * typed_lit(1_000_000f64))
1487                + expr.dt().nanosecond().div(typed_lit(1_000f64))
1488        },
1489        DateTimeField::Nanosecond | DateTimeField::Nanoseconds => {
1490            (expr.clone().dt().second() * typed_lit(1_000_000_000f64)) + expr.dt().nanosecond()
1491        },
1492        DateTimeField::Time => expr.dt().time(),
1493        #[cfg(feature = "timezones")]
1494        DateTimeField::Timezone => expr.dt().base_utc_offset().dt().total_seconds(false),
1495        DateTimeField::Epoch => {
1496            expr.clone()
1497                .dt()
1498                .timestamp(TimeUnit::Nanoseconds)
1499                .div(typed_lit(1_000_000_000i64))
1500                + expr.dt().nanosecond().div(typed_lit(1_000_000_000f64))
1501        },
1502        _ => {
1503            polars_bail!(SQLSyntax: "EXTRACT/DATE_PART does not support '{}' part", field)
1504        },
1505    })
1506}
1507
1508/// Allow an expression that represents a 1-indexed parameter to
1509/// be adjusted from 1-indexed (SQL) to 0-indexed (Rust/Polars)
1510pub(crate) fn adjust_one_indexed_param(idx: Expr, null_if_zero: bool) -> Expr {
1511    match idx {
1512        Expr::Literal(sc) if sc.is_null() => lit(LiteralValue::untyped_null()),
1513        Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(0))) => {
1514            if null_if_zero {
1515                lit(LiteralValue::untyped_null())
1516            } else {
1517                idx
1518            }
1519        },
1520        Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))) if n < 0 => idx,
1521        Expr::Literal(LiteralValue::Dyn(DynLiteralValue::Int(n))) => lit(n - 1),
1522        // TODO: when 'saturating_sub' is available, should be able
1523        //  to streamline the when/then/otherwise block below -
1524        _ => when(idx.clone().gt(lit(0)))
1525            .then(idx.clone() - lit(1))
1526            .otherwise(if null_if_zero {
1527                when(idx.clone().eq(lit(0)))
1528                    .then(lit(LiteralValue::untyped_null()))
1529                    .otherwise(idx.clone())
1530            } else {
1531                idx.clone()
1532            }),
1533    }
1534}
1535
1536fn resolve_column<'a>(
1537    ctx: &'a mut SQLContext,
1538    ident_root: &'a Ident,
1539    name: &'a str,
1540    dtype: &'a DataType,
1541) -> PolarsResult<(Expr, Option<&'a DataType>)> {
1542    let resolved = ctx.resolve_name(&ident_root.value, name);
1543    let resolved = resolved.as_str();
1544    Ok((
1545        if name != resolved {
1546            col(resolved).alias(name)
1547        } else {
1548            col(name)
1549        },
1550        Some(dtype),
1551    ))
1552}
1553
1554pub(crate) fn resolve_compound_identifier(
1555    ctx: &mut SQLContext,
1556    idents: &[Ident],
1557    active_schema: Option<&Schema>,
1558) -> PolarsResult<Vec<Expr>> {
1559    // inference priority: table > struct > column
1560    let ident_root = &idents[0];
1561    let mut remaining_idents = idents.iter().skip(1);
1562    let mut lf = ctx.get_table_from_current_scope(&ident_root.value);
1563
1564    // get schema from table (or the active/default schema)
1565    let schema = if let Some(ref mut lf) = lf {
1566        lf.schema_with_arenas(&mut ctx.lp_arena, &mut ctx.expr_arena)?
1567    } else {
1568        Arc::new(active_schema.cloned().unwrap_or_default())
1569    };
1570
1571    // handle simple/unqualified column reference with no schema
1572    if lf.is_none() && schema.is_empty() {
1573        let (mut column, mut dtype): (Expr, Option<&DataType>) =
1574            (col(ident_root.value.as_str()), None);
1575
1576        // traverse the remaining struct field path (if any)
1577        for ident in remaining_idents {
1578            let name = ident.value.as_str();
1579            match dtype {
1580                Some(DataType::Struct(fields)) if name == "*" => {
1581                    return Ok(fields
1582                        .iter()
1583                        .map(|fld| column.clone().struct_().field_by_name(&fld.name))
1584                        .collect());
1585                },
1586                Some(DataType::Struct(fields)) => {
1587                    dtype = fields
1588                        .iter()
1589                        .find(|fld| fld.name == name)
1590                        .map(|fld| &fld.dtype);
1591                },
1592                Some(dtype) if name == "*" => {
1593                    polars_bail!(SQLSyntax: "cannot expand '*' on non-Struct dtype; found {:?}", dtype)
1594                },
1595                _ => dtype = None,
1596            }
1597            column = column.struct_().field_by_name(name);
1598        }
1599        return Ok(vec![column]);
1600    }
1601
1602    let name = &remaining_idents.next().unwrap().value;
1603
1604    // handle "table.*" wildcard expansion
1605    if lf.is_some() && name == "*" {
1606        return schema
1607            .iter_names_and_dtypes()
1608            .map(|(name, dtype)| resolve_column(ctx, ident_root, name, dtype).map(|(expr, _)| expr))
1609            .collect();
1610    }
1611
1612    // resolve column/struct reference
1613    let col_dtype: PolarsResult<(Expr, Option<&DataType>)> =
1614        match (lf.is_none(), schema.get(&ident_root.value)) {
1615            // root is a column/struct in schema (no table)
1616            (true, Some(dtype)) => {
1617                remaining_idents = idents.iter().skip(1);
1618                Ok((col(ident_root.value.as_str()), Some(dtype)))
1619            },
1620            // root is not in schema and no table found
1621            (true, None) => {
1622                polars_bail!(
1623                    SQLInterface: "no table or struct column named '{}' found",
1624                    ident_root
1625                )
1626            },
1627            // root is a table, resolve column from table schema
1628            (false, _) => {
1629                if let Some((_, col_name, dtype)) = schema.get_full(name) {
1630                    resolve_column(ctx, ident_root, col_name, dtype)
1631                } else {
1632                    polars_bail!(
1633                        SQLInterface: "no column named '{}' found in table '{}'",
1634                        name, ident_root
1635                    )
1636                }
1637            },
1638        };
1639
1640    // additional ident levels index into struct fields (eg: "df.col.field.nested_field")
1641    let (mut column, mut dtype) = col_dtype?;
1642    for ident in remaining_idents {
1643        let name = ident.value.as_str();
1644        match dtype {
1645            Some(DataType::Struct(fields)) if name == "*" => {
1646                return Ok(fields
1647                    .iter()
1648                    .map(|fld| column.clone().struct_().field_by_name(&fld.name))
1649                    .collect());
1650            },
1651            Some(DataType::Struct(fields)) => {
1652                dtype = fields
1653                    .iter()
1654                    .find(|fld| fld.name == name)
1655                    .map(|fld| &fld.dtype);
1656            },
1657            Some(dtype) if name == "*" => {
1658                polars_bail!(SQLSyntax: "cannot expand '*' on non-Struct dtype; found {:?}", dtype)
1659            },
1660            _ => {
1661                dtype = None;
1662            },
1663        }
1664        column = column.struct_().field_by_name(name);
1665    }
1666    Ok(vec![column])
1667}