Skip to content

Add NodeTraverser-based expression parser (default) with legacy escape hatch#6

Merged
ptomecek merged 1 commit into
mainfrom
feat/node-traverser-parser
Jul 14, 2026
Merged

Add NodeTraverser-based expression parser (default) with legacy escape hatch#6
ptomecek merged 1 commit into
mainfrom
feat/node-traverser-parser

Conversation

@ptomecek

@ptomecek ptomecek commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a NodeTraverserParser built on polars' _plr API (LazyFrame._ldf.visit() + add_expressions + view_expression). It produces the same BaseExprNode tree the JSON-serialization based ExprParser does, so it plugs in behind get_parsed_expr without changing the downstream visitor surface.

The new parser is the default. POLARS_IO_TOOLS_USE_LEGACY_EXPR_PARSER=1 falls back to the legacy parser for one release as an escape hatch.

Motivation

NodeTraverser walks the post-optimization typed expression view, which is exactly the shape that arrives at a custom IO source plugin via polars.io.plugins.register_io_source. It is structurally cleaner than the JSON-based parser: typed _expr_nodes dispatch (Operator, function-category enums, Literal.value / Cast.dtype) with expr.meta.pop() only for per-child sub-expression attribution.

Several expression shapes the legacy parser handles (top-level/nested aliases, any_horizontal / all_horizontal, selectors / wildcards, list / struct / explode) are rewritten away by the polars optimizer before any IO-source predicate callback runs, so they are dead code on the predicate-pushdown path. The tests covering those shapes are marked skip_unoptimized_expression_shape and run only under the legacy escape hatch.

Known limitation

polars' NodeTraverser does not yet expose typed views for list (col.list.*) and array (col.arr.*) functions — view_expression raises NotImplementedError for them. Under the new default parser those subtrees degrade to UnknownNode, so a predicate containing them is not pushed down (the filter is still applied correctly by polars; only pushdown efficiency is lost). POLARS_IO_TOOLS_USE_LEGACY_EXPR_PARSER=1 recovers full parsing for these shapes. This is documented inline in NodeTraverserParser._parse.

Compatibility

  • Imports the parser node module via polars._plr._expr_nodes with a fallback to the older polars.polars._expr_nodes path (renamed in polars 1.32).
  • Pins polars>=1.8 to match the validated floor.

Test results

  • default: all io_sources tests pass; the dead-code-shape tests are skipped.
  • POLARS_IO_TOOLS_USE_LEGACY_EXPR_PARSER=1: the skipped tests run and pass under the legacy parser.

@ptomecek ptomecek force-pushed the feat/node-traverser-parser branch from e5d3d50 to 39b5719 Compare July 14, 2026 19:06
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Test Results

1 317 tests  +2   1 269 ✅  - 10   34s ⏱️ -1s
    2 suites ±0      48 💤 +12 
    2 files   ±0       0 ❌ ± 0 

Results for commit 954ebe6. ± Comparison against base commit 9b60777.

This pull request skips 12 tests.
polars_io_tools.tests.io_sources.test_base ‑ test_explode_operations
polars_io_tools.tests.io_sources.test_base ‑ test_multiple_string_operations
polars_io_tools.tests.io_sources.test_base ‑ test_parse_alias
polars_io_tools.tests.io_sources.test_base ‑ test_parse_list_functions
polars_io_tools.tests.io_sources.test_base ‑ test_parse_multiple_columns
polars_io_tools.tests.io_sources.test_base ‑ test_struct_operations
polars_io_tools.tests.io_sources.test_dnf_visitor ‑ test_alias_nested_expression
polars_io_tools.tests.io_sources.test_dnf_visitor ‑ test_alias_with_functions
polars_io_tools.tests.io_sources.test_dnf_visitor ‑ test_all_horizontal
polars_io_tools.tests.io_sources.test_dnf_visitor ‑ test_any_horizontal
…

♻️ This comment has been updated with latest results.

…e hatch

Adds a `NodeTraverserParser` built on polars' `_plr` API
(`LazyFrame._ldf.visit()` + `add_expressions` + `view_expression`). It
produces the same `BaseExprNode` tree the JSON-serialization based
`ExprParser` does, so it plugs in behind `get_parsed_expr` without
changing the downstream visitor surface.

The new parser is the default. Set
`POLARS_IO_TOOLS_USE_LEGACY_EXPR_PARSER=1` to fall back to the legacy
parser for one release as an escape hatch.

The new parser walks the post-optimization typed expression view, which
is exactly the shape that reaches a custom IO source predicate via
`register_io_source`. Several expression shapes the legacy parser
handles (top-level/nested aliases, any_horizontal / all_horizontal,
selectors / wildcards, list / struct / explode) are rewritten away by
the polars optimizer before any predicate callback runs, so they are
dead code on the predicate-pushdown path. The tests covering those
shapes are marked `skip_unoptimized_expression_shape` and run only under
the legacy escape hatch.

Imports the parser node module via `polars._plr._expr_nodes` with a
fallback to the older `polars.polars._expr_nodes` path (renamed in
polars 1.32), and pins `polars>=1.8` to match the validated floor.

Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
@ptomecek ptomecek force-pushed the feat/node-traverser-parser branch from 39b5719 to 954ebe6 Compare July 14, 2026 19:30
@ptomecek ptomecek merged commit e4bc8aa into main Jul 14, 2026
5 checks passed
@ptomecek ptomecek deleted the feat/node-traverser-parser branch July 14, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant