Add cast_map to scan_db for server-side column narrowing - #28
Conversation
5a1c56c to
7d7bbf6
Compare
scan_db gains an optional cast_map={column: dtype} that casts the named
columns server-side with a SQL CAST, wrapping the query in an
auto-enumerated projecting subquery so `select *` still flows every
column. The narrowed dtype is reported in the schema, so a filter on a
cast column (for example a datetime narrowed to date) is pushed down to
the database instead of stalling above a client-side cast.
Clauses that are illegal inside a derived table (a top-level ORDER BY
without TOP/OFFSET, or OPTION hints in the T-SQL dialect) are hoisted
onto the cast SELECT so the nested subquery stays valid. Unsupported
target dtypes are rejected rather than silently emitting VARCHAR, and
decimal precision and scale are preserved.
Extract a shared polars_dtype_to_sqlglot_type helper used by both the
predicate translator and the cast wrapping.
Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
7d7bbf6 to
96e4f5f
Compare
|
One concern with casting in SQL is that filters may end up operating on the casted expression—for example: WHERE CAST(RecordDate AS date) = @dateApplying a function to the filtered column can interfere with index use. Microsoft, for example, explicitly calls out This seems similar to the problem already handled by I wonder if we could use the same pattern here. A timestamp exposed as pl.col("RecordDate") == date(2025, 5, 30)into: WHERE RecordDate >= @start
AND RecordDate < @next_dayThe matching values could then be converted locally, potentially through |
A predicate on a cast_map column pushes down over its CAST expression, which can prevent the database from using an index on the column. Note in the scan_db docstring and the wiki that the query-plan impact is backend dependent, and broaden the examples to cover non-temporal narrowing such as a float id cast to an integer. Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
|
Good point, and you're right about the SARGability risk in the general case — wrapping the column in A couple of reasons I've kept the server-side
Given that, I've gone with keeping The range-translation approach you describe (mirroring |
Makes sense, sounds great! |
Summary
Adds an optional
cast_mapargument toscan_dbthat casts selected output columns server-side, so filters on a narrowed column push down to the database instead of being evaluated client-side.Motivation
A Polars IO plugin only receives
(with_columns, predicate)from the optimizer. When downstream code casts ascan_dbcolumn to a narrower type (for example a column stored asdatetimethat is logically adate) and then filters on it, Polars will not push the predicate through the type-changing cast — the plugin seespredicate=None, so the filter runs client-side after a full table scan.cast_mapmoves the narrowing into the query itself, so the predicate stays pushable.What it does
CASTs the named columns and passes the rest through, soselect *still flows every column (including new upstream ones).Emitted SQL (T-SQL example):
Correctness details
ORDER BYwithoutTOP/OFFSET, orOPTIONhints in the T-SQL dialect) are hoisted onto the castSELECTso the nested subquery stays valid.VARCHARwhile reporting the requested dtype.polars_dtype_to_sqlglot_typehelper is extracted and used by both the predicate translator and the cast wrapping.Testing
select *preservation, unknown-column and unsupported-dtype errors, and MSSQLORDER BYhoisting.io_sourcessuite passes;ruff checkandruff format --checkare clean.