Skip to content

Commit f1c577a

Browse files
committed
fix(deparser): preserve parentheses around binary expressions in unary 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).
1 parent f5e33fe commit f1c577a

File tree

4 files changed

+23
-6
lines changed

4 files changed

+23
-6
lines changed

__fixtures__/generated/generated.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21330,7 +21330,9 @@
2133021330
"misc/issues-15.sql": "select \"A\" from \"table_name\"",
2133121331
"misc/issues-16.sql": "select \"AA\" from \"table_name\"",
2133221332
"misc/issues-17.sql": "SELECT CAST(t.date AT TIME ZONE $$America/New_York$$ AS text)::date FROM tbl t",
21333-
"misc/issues-18.sql": "CREATE TABLE test_exclude_where (\n id uuid PRIMARY KEY,\n database_id uuid NOT NULL,\n status text NOT NULL DEFAULT 'pending',\n EXCLUDE USING btree (database_id WITH =)\n WHERE (status = 'pending')\n)",
21333+
"misc/issues-18.sql": "SELECT (- (-10 - -12))::numeric AS delta",
21334+
"misc/issues-19.sql": "SELECT (- (a.actual_eur - a.budget_eur))::numeric AS delta_eur FROM accounts a",
21335+
"misc/issues-20.sql": "CREATE TABLE test_exclude_where (\n id uuid PRIMARY KEY,\n database_id uuid NOT NULL,\n status text NOT NULL DEFAULT 'pending',\n EXCLUDE USING btree (database_id WITH =)\n WHERE (status = 'pending')\n)",
2133421336
"misc/inflection-1.sql": "CREATE SCHEMA inflection",
2133521337
"misc/inflection-2.sql": "GRANT USAGE ON SCHEMA inflection TO PUBLIC",
2133621338
"misc/inflection-3.sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA inflection \n GRANT EXECUTE ON FUNCTIONS TO PUBLIC",

__fixtures__/kitchen-sink/misc/issues.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ select "AA" from "table_name";
7676
-- https://github.com/constructive-io/pgsql-parser/issues/217
7777
SELECT CAST(t.date AT TIME ZONE $$America/New_York$$ AS text)::date FROM tbl t;
7878

79+
-- https://github.com/constructive-io/pgsql-parser/issues/285
80+
-- TypeCast with unary minus loses parentheses around inner expression
81+
-- (- (a - b))::numeric becomes CAST(- a - b AS numeric) which changes the math
82+
SELECT (- (-10 - -12))::numeric AS delta;
83+
SELECT (- (a.actual_eur - a.budget_eur))::numeric AS delta_eur FROM accounts a;
84+
7985
-- https://github.com/constructive-io/pgsql-parser/issues/287
8086
-- EXCLUDE constraint with WHERE clause (partial exclusion constraint)
8187
-- The deparser drops the WHERE clause from EXCLUDE USING ... WHERE (...)

packages/deparser/__tests__/kitchen-sink/misc-issues.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ it('misc-issues', async () => {
2121
"misc/issues-15.sql",
2222
"misc/issues-16.sql",
2323
"misc/issues-17.sql",
24-
"misc/issues-18.sql"
24+
"misc/issues-18.sql",
25+
"misc/issues-19.sql",
26+
"misc/issues-20.sql"
2527
]);
2628
});

packages/deparser/src/deparser.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,17 @@ export class Deparser implements DeparserVisitor {
720720

721721
return context.format([leftExpr, operator, rightExpr]);
722722
}else if (rexpr) {
723-
return context.format([
724-
this.deparseOperatorName(name, context),
725-
this.visit(rexpr, context)
726-
]);
723+
// Unary operator (e.g., unary minus: - expr)
724+
const operator = this.deparseOperatorName(name, context);
725+
let rightExpr = this.visit(rexpr, context);
726+
727+
// Wrap in parentheses if rexpr is a binary A_Expr to preserve semantics
728+
// e.g., -(a - b) must stay -(a - b), not become -a - b
729+
if (rexpr && 'A_Expr' in rexpr && rexpr.A_Expr?.kind === 'AEXPR_OP' && rexpr.A_Expr?.lexpr) {
730+
rightExpr = context.parens(rightExpr);
731+
}
732+
733+
return context.format([operator, rightExpr]);
727734
}
728735
break;
729736
case 'AEXPR_OP_ANY':

0 commit comments

Comments
 (0)