Skip to content

fix: operator precedence, outline substitution, xfail precision#359

Merged
DecisionNerd merged 3 commits into
mainfrom
fix/358-phase1-precedence-outline-substitution
Apr 27, 2026
Merged

fix: operator precedence, outline substitution, xfail precision#359
DecisionNerd merged 3 commits into
mainfrom
fix/358-phase1-precedence-outline-substitution

Conversation

@DecisionNerd
Copy link
Copy Markdown
Owner

@DecisionNerd DecisionNerd commented Apr 27, 2026

Closes #358

Summary

  • ParenthesizedExpr AST node: New node to preserve explicit parentheses through all four layers, fixing column name fidelity for expressions like (12 / 4 * (3 - 2 * 4)). Added to grammar (named rule), transformer, evaluator (pass-through), executor (_expression_to_string), and planner (aggregate detection/extraction).
  • Outline substitution fix: pytest-bdd's render_string used <(.+?)> which consumed across <> (not-equal operator), causing KeyError in all operator-precedence Scenario Outlines. Fixed with a word-char-only _fix_outline_placeholders helper called from all query-executing step functions.
  • xfail precision: Replaced 8 broad _XFAIL_REASONS_AND_PATTERNS with 7 precise ones. Eliminates all 339 false XPASS results (stale patterns matching tests our impl passes fine) while retaining 20 genuine platform-limitation xfails.

Test plan

  • uv run pytest tests/tck/test_features.py --tb=no -q → 3419 passed, 429 failed, 20 xfailed, 0 xpassed
  • uv run ruff check src/ tests/ — no issues
  • uv run ruff format --check src/ tests/ — no issues
  • uv run mypy src/graphforge --strict-optional --show-error-codes — no issues

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for explicitly parenthesized expressions in queries, preserving parentheses in output column naming.
  • Bug Fixes

    • Fixed detection of aggregate functions when wrapped in parentheses.
    • Enhanced test framework to correctly handle placeholder parsing in query scenarios.

- Add ParenthesizedExpr AST node to preserve explicit parentheses through
  all four layers (parser → planner → executor → evaluator), fixing column
  name fidelity for queries like `(12 / 4 * (3 - 2 * 4))`
- Fix pytest-bdd Scenario Outline substitution: replace broad `<(.+?)>`
  regex (which consumed across `<>` not-equal operators) with a word-char-
  only alternative, unblocking all operator-precedence TCK scenarios
- Refactor `_XFAIL_REASONS_AND_PATTERNS` from 8 broad patterns to 7 precise
  ones, eliminating all 339 false XPASS results while retaining 20 genuine
  platform-limitation xfails (CPython nanosecond truncation, clock-twice
  non-determinism, extreme-year overflow)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@DecisionNerd has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 39 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 039dac53-6a38-4797-b442-289f9c5fab43

📥 Commits

Reviewing files that changed from the base of the PR and between 9922e69 and d542b1c.

📒 Files selected for processing (5)
  • src/graphforge/executor/evaluator.py
  • src/graphforge/executor/executor.py
  • src/graphforge/planner/planner.py
  • tests/unit/executor/test_expressions_match.py
  • tests/unit/executor/test_parenthesized_expr_coverage.py

Walkthrough

This PR introduces a ParenthesizedExpr AST node to preserve explicit parentheses from source code, preventing precedence ambiguity in column names. The parser creates these nodes, the evaluator unwraps them, the executor preserves parentheses in output strings, and the planner traverses them for aggregate detection. Additionally, TCK test infrastructure is improved to handle Scenario Outline placeholder substitution correctly in presence of Cypher's <> operator.

Changes

Cohort / File(s) Summary
AST Node Definition
src/graphforge/ast/expression.py, src/graphforge/ast/__init__.py
New ParenthesizedExpr model added to represent explicitly parenthesized subexpressions; exported in module __all__.
Parser (Grammar & Transformer)
src/graphforge/parser/cypher.lark, src/graphforge/parser/parser.py
Grammar updated to produce labeled parenthesized_expr parse tree nodes; transformer method added to construct ParenthesizedExpr AST nodes from parsed syntax.
Expression Evaluation & Stringification
src/graphforge/executor/evaluator.py, src/graphforge/executor/executor.py
Evaluator now unwraps ParenthesizedExpr nodes to evaluate inner expression; executor's _expression_to_string preserves explicit parentheses and removes spurious parenthesization previously added for BinaryOp children.
Aggregate Detection
src/graphforge/planner/planner.py
_contains_aggregate and _extract_aggregates updated to transparently traverse and unwrap ParenthesizedExpr nodes when checking for or extracting aggregate functions.
TCK Test Infrastructure
tests/tck/conftest.py
Added _fix_outline_placeholders helper to correctly substitute Scenario Outline placeholders using identifier-style regex (avoiding misinterpretation of <> operator); updated step function signatures to accept request fixture and apply placeholder fixing before parameter substitution; refined xfail catalog for platform-limitation scenarios.

Sequence Diagram

sequenceDiagram
    participant Parser as Parser (Cypher.lark)
    participant Transformer as ASTTransformer
    participant AST as ParenthesizedExpr Node
    participant Evaluator as Evaluator
    participant Executor as Executor
    participant Planner as Planner

    Parser->>Parser: Parse `(a + b)` as parenthesized_expr
    Parser->>Transformer: Invoke parenthesized_expr transformer
    Transformer->>AST: Create ParenthesizedExpr(expr=inner_expr)
    Evaluator->>AST: Evaluate ParenthesizedExpr
    AST->>Evaluator: Return evaluation of inner expr
    Executor->>AST: Generate column name
    AST->>Executor: Return "(a + b)" preserving parentheses
    Planner->>AST: Check for aggregates
    AST->>Planner: Unwrap and delegate to inner expr
    Planner->>Planner: Detect aggregates transparently
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the three main changes: operator precedence handling via ParenthesizedExpr, Scenario Outline placeholder substitution fix, and xfail test precision improvements.
Description check ✅ Passed The description covers objectives, implementation details across all affected files, test plan with specific results, and CI check outcomes. However, the standard template sections (Type of Change, Changes Made, Testing checklist) are not used.
Linked Issues check ✅ Passed All primary objectives from #358 are met: ParenthesizedExpr support added throughout (grammar, parser, evaluator, executor, planner); outline placeholder substitution fixed with word-char-only regex; executor parenthesization logic updated; spurious parentheses removed.
Out of Scope Changes check ✅ Passed Changes are scoped to requirements from #358. Parser/executor/planner updates for ParenthesizedExpr, conftest placeholder fix, and xfail precision refinement all directly address stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/358-phase1-precedence-outline-substitution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/graphforge/executor/evaluator.py (1)

130-149: ⚠️ Potential issue | 🔴 Critical

Forward executor and recurse ParenthesizedExpr in literal containers.

At Line 192, evaluate_expression is called without executor, which breaks parenthesized expressions that depend on executor context (e.g., subqueries/custom functions).
Also, ParenthesizedExpr is missing from the literal list/map recursive evaluation checks, so parenthesized elements can be treated as raw Python objects instead of AST expressions.

🐛 Proposed fix
@@
-    if isinstance(expr, ParenthesizedExpr):
-        return evaluate_expression(expr.expr, ctx)
+    if isinstance(expr, ParenthesizedExpr):
+        return evaluate_expression(expr.expr, ctx, executor)
@@
                 if isinstance(
                     item,
                     (
                         Literal,
                         Variable,
                         PropertyAccess,
                         Subscript,
                         BinaryOp,
                         UnaryOp,
                         FunctionCall,
                         CaseExpression,
                         ListComprehension,
                         PatternComprehension,
                         QuantifierExpression,
                         SubqueryExpression,
+                        ParenthesizedExpr,
                     ),
                 )
@@
                 if isinstance(
                     val,
                     (
                         Literal,
                         Variable,
                         PropertyAccess,
                         Subscript,
                         BinaryOp,
                         UnaryOp,
                         FunctionCall,
                         CaseExpression,
                         ListComprehension,
                         PatternComprehension,
                         QuantifierExpression,
                         SubqueryExpression,
+                        ParenthesizedExpr,
                     ),
                 ):

Also applies to: 159-174, 190-193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/graphforge/executor/evaluator.py` around lines 130 - 149, The recursive
evaluation of literal containers misses ParenthesizedExpr and fails to forward
the executor to evaluate_expression, so update the type-check lists used when
iterating list/map items to include ParenthesizedExpr (alongside Literal,
Variable, PropertyAccess, Subscript, BinaryOp, UnaryOp, FunctionCall,
CaseExpression, ListComprehension, PatternComprehension, QuantifierExpression,
SubqueryExpression) and ensure every call to evaluate_expression inside those
container-handling blocks passes the executor argument (e.g.,
evaluate_expression(item, ctx, executor)); apply the same changes to the other
similar blocks handling maps/sets where evaluate_expression is invoked.
🧹 Nitpick comments (2)
tests/tck/conftest.py (2)

333-380: Step signature/refactor LGTM — minor DRY suggestion.

All four step functions now do the same _fix_outline_placeholders → _substitute_parameters → execute → capture-error pipeline. The duplication is fine for now, but if you add more execution variants in the future this is a natural place to factor out a small private helper:

♻️ Optional consolidation
def _run_query(tck_context, docstring, request, *, use_params: bool):
    params = tck_context.get("parameters", {})
    query = _fix_outline_placeholders(docstring, request)
    query = _substitute_parameters(query, params)
    try:
        kwargs = {"parameters": params} if use_params and params else {}
        tck_context["result"] = tck_context["graph"].execute(query, **kwargs)
    except Exception as e:
        tck_context["result"] = {"error": str(e)}

Each step function then becomes a 1-liner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/tck/conftest.py` around lines 333 - 380, The four step handlers
(execute_query_colon, execute_query, execute_control_query_colon,
execute_control_query) duplicate the same pipeline; refactor by adding a private
helper (e.g. _run_query) that accepts (tck_context, docstring, request,
use_params: bool) to perform _fix_outline_placeholders → _substitute_parameters
→ execute and error capture, then replace each handler with a one-line call to
_run_query passing use_params=True for the parameterized query variants and
False for control-query variants so behavior is preserved.

305-330: Consider adding a unit test for _fix_outline_placeholders.

Per the repo's testing guidelines, new code under tests/** should still be exercised. _fix_outline_placeholders has several non-trivial branches (no < short-circuit, missing callspec, empty example dict, mixed <> + <placeholder> input) that aren't covered by indirect TCK runs.

A small tests/unit/test_outline_placeholder_fix.py parametrized over inputs like:

  • "WHERE a <> 1 AND b = <name>" with example={"name": "'x'"}"WHERE a <> 1 AND b = 'x'"
  • "<a> <> <b>" with example={"a": "1", "b": "2"}"1 <> 2"
  • query without < → unchanged
  • request without callspec → unchanged

would lock in the regex behavior and protect against future pytest-bdd internal-API changes.

As per coding guidelines: "Write comprehensive unit tests for each layer (parser, planner, executor) with 100% coverage on new code (90% minimum)" and "Use @pytest.mark.parametrize for testing the same logic with different inputs to avoid code duplication".

Want me to draft the parametrized unit test file?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/tck/conftest.py` around lines 305 - 330, Add a new parametrized unit
test module (e.g., tests/unit/test_outline_placeholder_fix.py) that directly
calls _fix_outline_placeholders and covers its branches: no-"<" short-circuit,
request without callspec (simulate a fake request object lacking node.callspec),
empty example dict, and mixed "<>"/"<placeholder>" inputs; use inputs like
"WHERE a <> 1 AND b = <name>" with example={"name":"'x'"} expecting "WHERE a <>
1 AND b = 'x'", "<a> <> <b>" with example={"a":"1","b":"2"} expecting "1 <> 2",
a query without "<" expecting unchanged output, and a request missing callspec
expecting unchanged output; construct the request fixture by creating a simple
object with node.callspec.params or omitting node/callspec to trigger the
AttributeError path, and assert outputs against expected strings to lock
behavior of _OUTLINE_PLACEHOLDER_RE and _fix_outline_placeholders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/graphforge/executor/executor.py`:
- Around line 322-324: The matching/substitution logic doesn't handle
ParenthesizedExpr even though _expression_to_string preserves parens; update
both _expressions_match and _substitute_groupby_aliases to recognize
ParenthesizedExpr and recurse into its .expr: in _expressions_match, when either
side is a ParenthesizedExpr, unwrap and call _expressions_match on the inner
expr (so parenthesized forms compare equal to their inner expression for
matching/sort-path purposes); in _substitute_groupby_aliases, when encountering
a ParenthesizedExpr, call _substitute_groupby_aliases on .expr and wrap the
result back into a ParenthesizedExpr (preserving explicit parens while enabling
alias substitution). Ensure the logic references the ParenthesizedExpr type and
uses the existing helper functions for recursion.

In `@src/graphforge/planner/planner.py`:
- Around line 1374-1375: The three expression-walking methods
QueryPlanner._collect_expr_variables, QueryPlanner._validate_non_agg_context,
and QueryPlanner._validate_agg_arg_scope currently skip ParenthesizedExpr nodes;
update each to handle ParenthesizedExpr the same way as the aggregate walker
(i.e., when encountering a ParenthesizedExpr delegate to the inner expr:
return/continue with self._collect_expr_variables(expr.expr) or
self._validate_non_agg_context(expr.expr) /
self._validate_agg_arg_scope(expr.expr) as appropriate) so parenthesized
expressions are traversed for ORDER BY/scope validation and aggregate checking
consistently.

---

Outside diff comments:
In `@src/graphforge/executor/evaluator.py`:
- Around line 130-149: The recursive evaluation of literal containers misses
ParenthesizedExpr and fails to forward the executor to evaluate_expression, so
update the type-check lists used when iterating list/map items to include
ParenthesizedExpr (alongside Literal, Variable, PropertyAccess, Subscript,
BinaryOp, UnaryOp, FunctionCall, CaseExpression, ListComprehension,
PatternComprehension, QuantifierExpression, SubqueryExpression) and ensure every
call to evaluate_expression inside those container-handling blocks passes the
executor argument (e.g., evaluate_expression(item, ctx, executor)); apply the
same changes to the other similar blocks handling maps/sets where
evaluate_expression is invoked.

---

Nitpick comments:
In `@tests/tck/conftest.py`:
- Around line 333-380: The four step handlers (execute_query_colon,
execute_query, execute_control_query_colon, execute_control_query) duplicate the
same pipeline; refactor by adding a private helper (e.g. _run_query) that
accepts (tck_context, docstring, request, use_params: bool) to perform
_fix_outline_placeholders → _substitute_parameters → execute and error capture,
then replace each handler with a one-line call to _run_query passing
use_params=True for the parameterized query variants and False for control-query
variants so behavior is preserved.
- Around line 305-330: Add a new parametrized unit test module (e.g.,
tests/unit/test_outline_placeholder_fix.py) that directly calls
_fix_outline_placeholders and covers its branches: no-"<" short-circuit, request
without callspec (simulate a fake request object lacking node.callspec), empty
example dict, and mixed "<>"/"<placeholder>" inputs; use inputs like "WHERE a <>
1 AND b = <name>" with example={"name":"'x'"} expecting "WHERE a <> 1 AND b =
'x'", "<a> <> <b>" with example={"a":"1","b":"2"} expecting "1 <> 2", a query
without "<" expecting unchanged output, and a request missing callspec expecting
unchanged output; construct the request fixture by creating a simple object with
node.callspec.params or omitting node/callspec to trigger the AttributeError
path, and assert outputs against expected strings to lock behavior of
_OUTLINE_PLACEHOLDER_RE and _fix_outline_placeholders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1d8cd029-527b-4722-9cee-c94c39130460

📥 Commits

Reviewing files that changed from the base of the PR and between 80892cf and 9922e69.

📒 Files selected for processing (8)
  • src/graphforge/ast/__init__.py
  • src/graphforge/ast/expression.py
  • src/graphforge/executor/evaluator.py
  • src/graphforge/executor/executor.py
  • src/graphforge/parser/cypher.lark
  • src/graphforge/parser/parser.py
  • src/graphforge/planner/planner.py
  • tests/tck/conftest.py

Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/planner/planner.py
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 73.52941% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.29%. Comparing base (80892cf) to head (d542b1c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #359      +/-   ##
==========================================
+ Coverage   89.11%   89.29%   +0.18%     
==========================================
  Files          39       39              
  Lines       11683    11714      +31     
  Branches     2531     2540       +9     
==========================================
+ Hits        10411    10460      +49     
+ Misses        891      855      -36     
- Partials      381      399      +18     
Flag Coverage Δ
full-coverage 89.29% <73.52%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
parser 95.77% <100.00%> (+0.62%) ⬆️
planner 92.07% <80.00%> (+0.14%) ⬆️
executor 85.48% <63.15%> (+0.27%) ⬆️
storage 93.31% <ø> (ø)
ast 96.25% <100.00%> (+0.02%) ⬆️
types 95.63% <ø> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 80892cf...d542b1c. Read the comment docs.

DecisionNerd and others added 2 commits April 26, 2026 21:31
Address CodeRabbit review findings:
- evaluator.py: forward executor arg in ParenthesizedExpr pass-through;
  add ParenthesizedExpr to literal list/map item isinstance guards
- executor.py: handle ParenthesizedExpr in _substitute_groupby_aliases
  and unwrap in _expressions_match (both sides) for ORDER BY alias matching
- planner.py: add ParenthesizedExpr cases to _collect_expr_variables,
  _validate_non_agg_context, and _validate_agg_arg_scope

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover the code paths added in the previous two commits:
- evaluator: ParenthesizedExpr pass-through (with executor), and
  ParenthesizedExpr inside list/map literals
- executor: _expressions_match unwrapping on both left and right sides
- planner: _collect_expr_variables, _validate_non_agg_context, and
  _validate_agg_arg_scope traversal through ParenthesizedExpr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@DecisionNerd DecisionNerd merged commit d6441e8 into main Apr 27, 2026
23 checks passed
@DecisionNerd DecisionNerd deleted the fix/358-phase1-precedence-outline-substitution branch April 27, 2026 12:50
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.

fix: Phase 1 — operator precedence, outline placeholder substitution, column names

1 participant