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