Context
#1200 has shipped:
The WhereClause.expr field is still load-bearing across the rest of the codebase. Two examples from cypher/lowering.py:
# lowering.py:5919 — needs surface text
expr_text = query.where.expr.text.strip()
# lowering.py:8142-8144 — needs source span for error reporting
value=where.expr.text,
line=where.expr.span.line,
column=where.expr.span.column,
This issue tracks migrating every where.expr reader to consume expr_tree (or text/span derived from it), then removing the WhereClause.expr field. ExpressionText itself stays — other AST fields use it (WithItem.expression, OrderByItem.expression, CallArg.args, WithClause.where, MatchClause.where, page values).
Reader inventory (master @ 0b1fb0c, post-#1207)
Read sites
| File |
Line |
Access pattern |
What's needed |
Migration approach |
frontends/cypher/binder.py |
1011 |
where.expr is not None → where.expr.text |
text |
drop the elif branch (expr_tree should always be populated when expr is, post-#1209) |
cypher/lowering.py |
5917 |
query.where.expr is None (gate) |
gate |
switch to expr_tree is None |
cypher/lowering.py |
5919 |
query.where.expr.text.strip() |
text |
reconstruct from expr_tree via _boolean_expr_to_text (see #1207's binder helper) |
cypher/lowering.py |
5957 |
.text for error value |
text |
same |
cypher/lowering.py |
6155-6157 |
.text |
text |
same |
cypher/lowering.py |
6163-6165 |
reentry_where.expr.text |
text |
same |
cypher/lowering.py |
6240-6242 |
.text |
text |
same |
cypher/lowering.py |
6322-6329 |
passes query.where.expr to row_where; assigns |
full ExpressionText object |
this site needs careful conversion since it forwards the value |
cypher/lowering.py |
7512-7515 |
where_clause.expr for rewrite_expr |
full ExpressionText |
the rewrite helper needs an expr_tree-aware variant |
cypher/lowering.py |
7530-7533 |
same for reentry_where |
same |
same |
cypher/lowering.py |
7997-7998 |
query.where.expr appended to pre_join_filters |
full ExpressionText |
downstream consumers of pre_join_filters need migration too |
cypher/lowering.py |
8129-8144 |
where.expr + .text + .span.line/.column |
text + span |
use expr_tree.span for span; _boolean_expr_to_text(expr_tree) for text |
cypher/ast_normalizer.py |
432-436 |
where.expr + passes to _rewrite_shortest_path_expr_text |
full ExpressionText |
rewrite helper needs to walk expr_tree instead of text |
cypher/ast_normalizer.py |
508-512 |
constructs new WhereClause(expr=..., expr_tree=...) |
both |
once expr is removed, drop the param |
(Total: ~14 distinct read sites across 3 files; some sites have multiple references.)
Write sites
| File |
Line |
Construction |
cypher/parser.py |
1170, 1206 |
structured-predicates path: WhereClause(predicates=..., expr=None, span=...) — already drops expr |
cypher/parser.py |
1209 |
generic path: WhereClause(predicates=(), expr=ExpressionText(...), expr_tree=expr_tree, span=span) — needs to drop expr arg |
cypher/parser.py |
other WhereClause(...) constructions |
check each |
cypher/ast_normalizer.py |
511 |
constructs WhereClause(expr=..., expr_tree=...) — needs to drop expr arg |
Pre-requisite: parser invariant
For this migration to be safe, the parser must guarantee that expr_tree is not None everywhere expr is not None was previously populated. Verify by:
- Auditing
_build_transformer for every WhereClause(...) construction.
- Adding an invariant test:
assert (where.expr_tree is None) == (where.expr is None) (or, after removal, assert where.expr_tree is None or has_meaning(where.expr_tree)).
This may surface edge cases — e.g. WHERE bodies that produce text but no BooleanExpr (single non-label atoms going through generic_where_clause post-#1209). If so, those cases need either a single-atom BooleanExpr synthesis or a different downstream signal.
Helper to lift to shared module
_boolean_expr_to_text currently lives in frontends/cypher/binder.py (introduced by #1207) for binder-internal use. To support lowering reading text from expr_tree, this helper should likely move to a shared location (cypher/ast.py as a BooleanExpr method, or a sibling cypher/_expr_text.py).
Done when
WhereClause.expr field is removed from cypher/ast.py:197.
- All 14+ read sites migrated to
expr_tree-derived text/span.
- All
WhereClause(...) construction sites drop the expr= argument.
_boolean_expr_to_text (or equivalent) is in a shared module so lowering can import it.
- Full GFQL test suite green (
tests/compute/gfql/).
ExpressionText class itself stays (other AST fields still use it).
Out of scope
Estimated cost
Multi-PR likely. Reasonable slicing:
- Sub-PR A: lift
_boolean_expr_to_text to shared module; add invariant assertion that (expr_tree is None) == (expr is None).
- Sub-PR B: migrate the simple text-only readers (binder L1011, lowering L5919, 5957, 6157, 6165, 6242).
- Sub-PR C: migrate the ExpressionText-passthrough readers (lowering L6322, 7515, 7533, 7998 — these need rewrite helpers updated too).
- Sub-PR D: migrate
ast_normalizer.py readers; drop expr= from constructions.
- Sub-PR E: drop the
WhereClause.expr field.
Each PR keeps the invariant assertion green; the field disappears in the last one.
Related
Context
#1200 has shipped:
WhereClause.expr_tree: Optional[BooleanExpr]exposed as additive field alongside the existingexpr: Optional[ExpressionText].expr_treewhen populated; falls back toexpr.textwhen not.generic_where_clausewalks tree; closes gfql/cypher: resolve Lark ambiguity between where_predicates and expr (grammar-level follow-up to #1125) #1194.The
WhereClause.exprfield is still load-bearing across the rest of the codebase. Two examples fromcypher/lowering.py:This issue tracks migrating every
where.exprreader to consumeexpr_tree(or text/span derived from it), then removing theWhereClause.exprfield.ExpressionTextitself stays — other AST fields use it (WithItem.expression,OrderByItem.expression,CallArg.args,WithClause.where,MatchClause.where, page values).Reader inventory (master @ 0b1fb0c, post-#1207)
Read sites
frontends/cypher/binder.pywhere.expr is not None→where.expr.textelifbranch (expr_tree should always be populated when expr is, post-#1209)cypher/lowering.pyquery.where.expr is None(gate)expr_tree is Nonecypher/lowering.pyquery.where.expr.text.strip()expr_treevia_boolean_expr_to_text(see #1207's binder helper)cypher/lowering.py.textfor error valuecypher/lowering.py.textcypher/lowering.pyreentry_where.expr.textcypher/lowering.py.textcypher/lowering.pyquery.where.exprto row_where; assignscypher/lowering.pywhere_clause.exprforrewrite_exprcypher/lowering.pycypher/lowering.pyquery.where.exprappended topre_join_filterspre_join_filtersneed migration toocypher/lowering.pywhere.expr+.text+.span.line/.columnexpr_tree.spanfor span;_boolean_expr_to_text(expr_tree)for textcypher/ast_normalizer.pywhere.expr+ passes to_rewrite_shortest_path_expr_textcypher/ast_normalizer.pyWhereClause(expr=..., expr_tree=...)(Total: ~14 distinct read sites across 3 files; some sites have multiple references.)
Write sites
cypher/parser.pyWhereClause(predicates=..., expr=None, span=...)— already drops exprcypher/parser.pyWhereClause(predicates=(), expr=ExpressionText(...), expr_tree=expr_tree, span=span)— needs to drop expr argcypher/parser.pyWhereClause(...)constructionscypher/ast_normalizer.pyWhereClause(expr=..., expr_tree=...)— needs to drop expr argPre-requisite: parser invariant
For this migration to be safe, the parser must guarantee that
expr_tree is not Noneeverywhereexpr is not Nonewas previously populated. Verify by:_build_transformerfor everyWhereClause(...)construction.assert (where.expr_tree is None) == (where.expr is None)(or, after removal,assert where.expr_tree is None or has_meaning(where.expr_tree)).This may surface edge cases — e.g. WHERE bodies that produce text but no
BooleanExpr(single non-label atoms going throughgeneric_where_clausepost-#1209). If so, those cases need either a single-atomBooleanExprsynthesis or a different downstream signal.Helper to lift to shared module
_boolean_expr_to_textcurrently lives infrontends/cypher/binder.py(introduced by #1207) for binder-internal use. To support lowering reading text fromexpr_tree, this helper should likely move to a shared location (cypher/ast.pyas aBooleanExprmethod, or a siblingcypher/_expr_text.py).Done when
WhereClause.exprfield is removed fromcypher/ast.py:197.expr_tree-derived text/span.WhereClause(...)construction sites drop theexpr=argument._boolean_expr_to_text(or equivalent) is in a shared module so lowering can import it.tests/compute/gfql/).ExpressionTextclass itself stays (other AST fields still use it).Out of scope
expr_split.pyretirement (independent, blocked on Cypher/GFQL WHERE cannot mix generic row predicates with pattern predicates #1031)._split_conjunctsin predicate_pushdown (load-bearing for IR-direct, see closed gfql/passes: predicate_pushdown drops _split_conjuncts in favor of pre-split BoundPredicates from #1207 (#1200 slice 3b) #1210)._CYPHER_BARE_LABEL_PREDICATE_RE(different operation than parser'sfullmatch, see closed gfql/cypher: retire _CYPHER_BARE_LABEL_PREDICATE_RE second copy in lowering.py (#1200 slice 3d) #1211).WhereClauseconstructions in mixed-pattern handlers (where_pattern_and_expr_clause,expr_and_where_pattern_clauseat parser.py:1046, 1058) — these may reconstructexpr_textfrom sliced source rather than walking a tree; check whether they need their own migration approach.Estimated cost
Multi-PR likely. Reasonable slicing:
_boolean_expr_to_textto shared module; add invariant assertion that(expr_tree is None) == (expr is None).ast_normalizer.pyreaders; dropexpr=from constructions.WhereClause.exprfield.Each PR keeps the invariant assertion green; the field disappears in the last one.
Related