Skip to content

Commit ac18319

Browse files
authored
Merge pull request #1 from Point72/fix/dt-date-pushdown
Recognize .dt.date() in temporal predicate pushdown
2 parents bf0c1dd + a48c42a commit ac18319

3 files changed

Lines changed: 35 additions & 0 deletions

File tree

polars_io_tools/io_sources/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
FunctionType,
1616
GenericFunctionType,
1717
OperatorType,
18+
TemporalFunctionType,
1819
TimeUnit,
1920
get_function_enum,
2021
)
@@ -1101,6 +1102,10 @@ def extract_column_name(node: BaseExprNode) -> Optional[str]:
11011102
return col_names[0]
11021103
elif isinstance(cur, (AliasNode, CastNode)):
11031104
cur = cur.input
1105+
elif isinstance(cur, FunctionNode) and cur.function_type == TemporalFunctionType.DATE and len(cur.inputs) == 1:
1106+
# ``col.dt.date()`` floors a Datetime to day granularity: order- and domain-preserving, hence equivalent
1107+
# to ``col.cast(pl.Date)`` (unwrapped above). Treat it identically so it resolves wherever a cast would.
1108+
cur = cur.inputs[0]
11041109
else:
11051110
return None
11061111

polars_io_tools/tests/io_sources/test_base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ def test_extract_column_name():
7979
expr = pl.col("b").cast(pl.Int64).alias("first").alias("second")
8080
assert "b" == extract_column_name(get_parsed_expr(expr))
8181

82+
# .dt.date() floors a Datetime column to day granularity (order-preserving, like cast(pl.Date)) and should
83+
# resolve to the underlying column so temporal predicates push down.
84+
expr = pl.col("ts").dt.date()
85+
assert "ts" == extract_column_name(get_parsed_expr(expr))
86+
expr = pl.col("ts").dt.date().alias("d")
87+
assert "ts" == extract_column_name(get_parsed_expr(expr))
88+
8289

8390
def test_parse_simple_column():
8491
"""Test parsing a simple column reference."""

polars_io_tools/tests/io_sources/test_multi_source.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,29 @@ def test_simple_date_filter(self):
233233
# With maintain_order="left", row order should be preserved
234234
assert_frame_equal(result, expected)
235235

236+
def test_dt_date_filter_prunes_datetime_source(self):
237+
"""A ``.dt.date()`` window on a Datetime source prunes upstream (no full scan) and stays correct.
238+
239+
``.dt.date()`` floors to day granularity (order-preserving, like ``cast(pl.Date)``), so the temporal range
240+
must still be extracted and pushed down rather than collapsing to the universe interval.
241+
"""
242+
ts = [datetime(2024, 1, 1) + timedelta(days=i) for i in range(90)]
243+
df = pl.DataFrame({"ts": ts, "val": list(range(90))})
244+
tracker = PredicateTracker(df)
245+
246+
lf = multi_source(
247+
sources={"data": (tracker.lazy_frame, {"ts": FilterSpec()})},
248+
combine=lambda s: s["data"],
249+
)
250+
251+
start, end = date(2024, 1, 1), date(2024, 1, 31)
252+
result = lf.filter((pl.col("ts").dt.date() >= start) & (pl.col("ts").dt.date() <= end)).collect()
253+
254+
# Range was extracted, so the source received a constraining predicate (not a silent full scan).
255+
assert tracker.last_predicate is not None
256+
assert result.height == 31
257+
assert result["ts"].max() == datetime(2024, 1, 31)
258+
236259
def test_simple_equality_filter(self):
237260
"""Equality filter propagates correctly."""
238261
left = pl.LazyFrame({"id": ["A", "B", "C"], "val": [1, 2, 3]})

0 commit comments

Comments
 (0)