feat(python): typed filter-expression builder as an alternative to raw SQL - #9328
Closed
jonasdedden wants to merge 1 commit into
Closed
jonasdedden wants to merge 1 commit into
jonasdedden wants to merge 1 commit into
Conversation
…o raw SQL Raw SQL filters are parsed without knowledge of the caller's intent: an integer column against a fractional literal fails to plan (lance-format#9317), a double literal against Float32 casts the column and skips its index (lance-format#9318), and a boolean column against a boolean expression fails in one operand order (lance-format#9319). The Substrait path avoids some of this but cannot express nested fields, string functions, or mixed int/float comparisons. Add a minimal lance.filter builder (col/lit/to_sql plus Expr accepted by scanner/count_rows/delete/update) that renders against the dataset schema with three index-preserving rules: integer bounds for fractional comparisons, CAST AS float literals for Float32, and AND/OR/NOT expansion for boolean-vs-boolean comparisons. Anything without an exact spelling raises FilterError instead of guessing. Tests cover positive and negated filters against nulls, signed zeros, infinities, nested fields, and both index types, with explain_plan evidence that the indexed spellings stay indexed.
Contributor
Author
|
Closing in favor of #9333. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Raw SQL filters are parsed without any knowledge of the caller's intent, so three common generator patterns hit planner pitfalls:
i > 1.5\u2192could not convert to literal of type Int64, feature: accept a float literal against an integer column in filters #9317);Float32column casts the column and silently skips its scalar index (x > CAST(0.5 AS double)plansCAST(x AS Float64), perf: a double-typed literal against a Float32 column casts the column and skips its index #9318);flag != (id > 0)errors while(id > 0) != flagplans, bug: comparing a boolean column with a boolean expression fails to plan when the expression contains a literal #9319).The existing typed path,
pa.compute.Expression\u2192 Substrait, avoids the #9318/#9319 traps (verified: all spellings use the index, both boolean orders plan) but cannot express what filter generators need: pyarrow refuses to serialize the implicit safe cast ini > 1.5(Substrait is only capable of representing unsafe casts), the planner rejects nested references in Substrait filters, andExpressionhas no constructors for string functions, temporal extraction, or list/struct access.Chosen rule
A minimal client-side builder,
lance.filter(col/lit/to_sql), rendered against the dataset schema.ScannerBuilder.filter(hencescanner/to_table),count_rows,delete, andupdateaccept anExprand render it with the dataset schema. Three index-preserving rules fire only with a schema in hand:i > 1.5\u2192i > 1), with null-aware tautologies for=/!=, infinities, and out-of-range bounds;Float32\u2192CAST(<literal> AS float), so the column keeps its type and index;AND/OR/NOTexpansion, so either operand order plans.Negation is pushed through comparisons (the rewrites are filter-equivalent but not value-equivalent:
NOT (i IS NOT NULL)would keep null rows that~(i != 1.5)must drop). Anything without an exact spelling raisesFilterErrorinstead of guessing: division/modulo/power, NaN literals, timezone-aware timestamps, decimals, out-of-range integer literals.Alternatives considered
pa.compute.Expression: capped by pyarrow's serializer (safe casts, no function constructors); not something this repo controls.Compatibility and minimum version
Pure-Python SQL generation; no Rust changes, no new dependencies, no format changes. Emitted SQL was executed verbatim on 9.0.0, 12.0.0b6, and 12.0.0b7: identical row counts everywhere except float-zero equality (
f = 0.0misses-0.0before b7, as established for #6236). So float filters needpylance >= 12.0.0b7(12.0.0 stable once published); every other spelling already plans on 9.0.0.Test results
New
test_filter.pysection (9 tests): SQL snapshot spellings; positive and negated filters vs hand oracles over nulls, both zeros, infinities, nested fields, strings, dates, and microsecond timestamps;explain_planassertions that the bound rewrite, theFloat32cast spelling,INlists, andstarts_withuseScalarIndexQuerywhile the hand-written double-cast baseline does not;count_rows/delete/updateacceptance; error cases. Validated locally against the 13.0.0b4 wheel (9 passed); full CI pending.ruff checkandruff format --checkpass on all touched files. Docs: newTyped filter expressionssubsection in the read-and-write guide.Limitations (why draft)
IN,BETWEEN,+ - *,starts_with/ends_with/contains; no temporal extraction, list/map access, or decimal/binary literals.Fragment.scannerstill takes SQL strings only.Related: #9317, #9318, #9319. Closes none of them.