From 2b32ccec510535c50262dbb4fc4e917b1742104e Mon Sep 17 00:00:00 2001 From: ritchie46 Date: Thu, 27 Aug 2026 12:09:22 +0200 Subject: [PATCH] fix(sql): Parse a typed DATE literal instead of casting it `DATE '2019-01-01'` emitted `lit(v).cast(Date)`, which polars no longer has a kernel for, so any query using the typed-literal form failed with "casting from string to date is not supported". Its `TIME` and `TIMESTAMP` siblings already parsed the string; all three now go through `parse_string_as_temporal`. The time unit `resolve_typed_literal_dtype` derives from a declared precision was also computed and then discarded, so `TIMESTAMP(3)` produced microseconds. It is now honoured. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016uVEEL5SiJifQEYPiUxszh --- crates/polars-sql/src/sql_expr.rs | 20 ++-------- py-polars/tests/unit/sql/test_temporal.py | 48 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/crates/polars-sql/src/sql_expr.rs b/crates/polars-sql/src/sql_expr.rs index d9ab239b12c7..7c1a177c17e8 100644 --- a/crates/polars-sql/src/sql_expr.rs +++ b/crates/polars-sql/src/sql_expr.rs @@ -403,23 +403,9 @@ impl SQLExprVisitor<'_> { uses_odbc_syntax: _, }) => { let dtype = self.resolve_typed_literal_dtype(data_type, v)?; - match dtype { - DataType::Date => Ok(lit(v.as_str()).cast(DataType::Date)), - DataType::Time => Ok(lit(v.as_str()).str().to_time(StrptimeOptions { - strict: true, - ..Default::default() - })), - DataType::Datetime(_, _) => Ok(lit(v.as_str()).str().to_datetime( - None, - None, - StrptimeOptions { - strict: true, - ..Default::default() - }, - lit("latest"), - )), - _ => unreachable!(), - } + parse_string_as_temporal(lit(v.as_str()), &dtype, true).ok_or_else( + || polars_err!(SQLInterface: "invalid temporal literal type {}", dtype), + ) }, SQLExpr::UnaryOp { op, expr } => self.visit_unary_op(op, expr), SQLExpr::Value(ValueWithSpan { value, .. }) => self.visit_literal(value), diff --git a/py-polars/tests/unit/sql/test_temporal.py b/py-polars/tests/unit/sql/test_temporal.py index bbd1c06233e1..87e66725eb19 100644 --- a/py-polars/tests/unit/sql/test_temporal.py +++ b/py-polars/tests/unit/sql/test_temporal.py @@ -8,6 +8,7 @@ import polars as pl from polars.exceptions import InvalidOperationError, SQLInterfaceError, SQLSyntaxError from polars.testing import assert_frame_equal +from tests.unit.sql import assert_sql_matches def test_date_func() -> None: @@ -482,3 +483,50 @@ def test_timestamp_time_unit_errors() -> None: match="sql parser error: Expected: literal int, found: - ", ): ctx.execute("SELECT ts::timestamp(-3) FROM frame_data") + + +def test_typed_temporal_literal_comparison() -> None: + df = pl.DataFrame( + { + "d": [date(2019, 6, 1), date(2020, 1, 1), date(2021, 1, 1)], + "ts": [ + datetime(2019, 6, 1, 12), + datetime(2020, 1, 1, 8), + datetime(2021, 1, 1, 0), + ], + } + ) + for query in ( + "SELECT d FROM self WHERE d > DATE '2019-12-31' ORDER BY d", + "SELECT d FROM self WHERE d BETWEEN DATE '2019-01-01' AND DATE '2020-06-01' ORDER BY d", + "SELECT ts FROM self WHERE ts > TIMESTAMP '2019-12-31 00:00:00' ORDER BY ts", + ): + assert_sql_matches(df, query=query, compare_with="duckdb") + + +@pytest.mark.parametrize( + ("literal", "expected"), + [ + ("DATE '2020-02-29'", date(2020, 2, 29)), + ("TIME '12:30:05'", time(12, 30, 5)), + ("TIMESTAMP '2020-01-01 08:00:00'", datetime(2020, 1, 1, 8)), + ], +) +def test_typed_temporal_literal(literal: str, expected: Any) -> None: + df = pl.DataFrame({"a": [1]}) + with pl.SQLContext(frames={"tbl": df}, eager=True) as ctx: + assert ctx.execute(f"SELECT {literal} AS x FROM tbl").item() == expected + + +@pytest.mark.parametrize( + ("precision", "time_unit"), + [(3, "ms"), (6, "us"), (9, "ns")], +) +def test_typed_timestamp_literal_precision(precision: int, time_unit: str) -> None: + # the declared precision selects the time unit + df = pl.DataFrame({"a": [1]}) + with pl.SQLContext(frames={"tbl": df}, eager=True) as ctx: + res = ctx.execute( + f"SELECT TIMESTAMP({precision}) '2020-01-01 08:00:00.123' AS x FROM tbl" + ) + assert res.schema["x"] == pl.Datetime(time_unit) # type: ignore[arg-type]