Skip to content

fix(deparser): preserve parentheses around binary expressions in unary operators#288

Merged
pyramation merged 1 commit intomainfrom
devin/1773545816-fix-typecast-parens
Mar 15, 2026
Merged

fix(deparser): preserve parentheses around binary expressions in unary operators#288
pyramation merged 1 commit intomainfrom
devin/1773545816-fix-typecast-parens

Conversation

@pyramation
Copy link
Collaborator

fix(deparser): preserve parentheses around binary expressions in unary operators

Summary

Fixes #285. The unary operator branch in the A_Expr handler did not wrap binary sub-expressions in parentheses, causing expressions like -(a - b) to deparse as - a - b — silently changing operator precedence and producing mathematically incorrect results.

This also affected TypeCast conversions: (-(a - b))::numeric became CAST(- a - b AS numeric) instead of CAST(- (a - b) AS numeric).

Fix: When a unary AEXPR_OP has an rexpr that is itself a binary AEXPR_OP (i.e., it has a lexpr), wrap the visited sub-expression in parentheses. Unary-inside-unary (e.g., - -x) is correctly left unwrapped since the inner expression has no lexpr.

Two test fixtures added for the exact SQL from the issue report.

Review & Testing Checklist for Human

  • Verify the parenthesization condition is complete: The fix only triggers for AEXPR_OP kind with a lexpr. Other A_Expr kinds (AEXPR_IN, AEXPR_BETWEEN, AEXPR_LIKE, etc.) inside a unary operator are not parenthesized by this change. Confirm these don't produce ambiguous output (they likely don't since they use keyword syntax, but worth a quick mental check).
  • Spot-check edge cases manually: Try parse → deparse on expressions like -(a + b * c), -(a IS NOT NULL), - -x, (-x)::int to verify no over/under-parenthesization.
  • Confirm fixture numbering didn't collide: The new fixtures shifted issue-18 from the EXCLUDE WHERE test to the new unary-minus test. The EXCLUDE WHERE test moved to issue-20. Verify no other code depends on specific fixture numbers.

Notes

  • All 290 deparser test suites pass (710 tests, 424 snapshots) with no regressions.
  • The fix is intentionally narrow — only binary AEXPR_OP inside unary AEXPR_OP — to minimize risk of over-parenthesization.

Link to Devin run: https://app.devin.ai/sessions/3f7a5f172eff49b388c44b148691911c
Requested by: @pyramation

…y operators

Fixes #285. The unary operator handler in A_Expr did not wrap a binary
A_Expr rexpr in parentheses, causing -(a - b) to deparse as - a - b,
which changes operator precedence and produces incorrect math.

Also affects TypeCast conversions: (-(a - b))::numeric became
CAST(- a - b AS numeric) instead of CAST(- (a - b) AS numeric).
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@pyramation pyramation merged commit fa0c7e6 into main Mar 15, 2026
7 checks passed
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.

Deparser removes parentheses when deparsing a :: to CAST leading to incorrect math

1 participant