Skip to content

Commit 9d88ad9

Browse files
authored
Merge pull request #345 from constructive-io/fix/deparse-assign-named-args
fix(plpgsql-deparser): decide assignment targets by parse mode, not a ':=' substring test
2 parents f0bb7e5 + 2f7a111 commit 9d88ad9

5 files changed

Lines changed: 193 additions & 24 deletions

File tree

__fixtures__/plpgsql-generated/generated.json

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@
163163
"plpgsql_deparser_fixes-63.sql": "CREATE FUNCTION test_alias_positional_param(integer) RETURNS integer\nLANGUAGE plpgsql AS $$\nDECLARE\n arg ALIAS FOR $1;\nBEGIN\n RETURN arg + 1;\nEND$$",
164164
"plpgsql_deparser_fixes-64.sql": "CREATE FUNCTION test_alias_named(input_value text) RETURNS text\nLANGUAGE plpgsql AS $$\nDECLARE\n val ALIAS FOR input_value;\n buffer text := 'x';\n buf ALIAS FOR buffer;\nBEGIN\n buf := buf || val;\n RETURN buf;\nEND$$",
165165
"plpgsql_deparser_fixes-65.sql": "CREATE FUNCTION test_alias_trigger() RETURNS trigger\nLANGUAGE plpgsql AS $$\nDECLARE\n prior ALIAS FOR old;\n updated ALIAS FOR new;\nBEGIN\n updated.updated_at := now();\n RETURN updated;\nEND$$",
166+
"plpgsql_deparser_fixes-66.sql": "CREATE FUNCTION test_assign_named_args() RETURNS text\nLANGUAGE plpgsql AS $$\nDECLARE\n v_body text;\nBEGIN\n v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');\n RETURN v_body;\nEND$$",
167+
"plpgsql_deparser_fixes-67.sql": "CREATE FUNCTION test_assign_targets() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n a int[] := ARRAY[1, 2];\n r record;\nBEGIN\n SELECT 1 AS f INTO r;\n a[2] := coalesce(nullif(3, 0), 4);\n r.f := greatest(1, 2);\nEND$$",
166168
"plpgsql_control-1.sql": "do $$\nbegin\n -- basic case\n for i in 1..3 loop\n raise notice '1..3: i = %', i;\n end loop;\n -- with BY, end matches exactly\n for i in 1..10 by 3 loop\n raise notice '1..10 by 3: i = %', i;\n end loop;\n -- with BY, end does not match\n for i in 1..11 by 3 loop\n raise notice '1..11 by 3: i = %', i;\n end loop;\n -- zero iterations\n for i in 1..0 by 3 loop\n raise notice '1..0 by 3: i = %', i;\n end loop;\n -- REVERSE\n for i in reverse 10..0 by 3 loop\n raise notice 'reverse 10..0 by 3: i = %', i;\n end loop;\n -- potential overflow\n for i in 2147483620..2147483647 by 10 loop\n raise notice '2147483620..2147483647 by 10: i = %', i;\n end loop;\n -- potential overflow, reverse direction\n for i in reverse -2147483620..-2147483647 by 10 loop\n raise notice 'reverse -2147483620..-2147483647 by 10: i = %', i;\n end loop;\nend$$",
167169
"plpgsql_control-2.sql": "do $$\nbegin\n for i in 1..3 by 0 loop\n raise notice '1..3 by 0: i = %', i;\n end loop;\nend$$",
168170
"plpgsql_control-3.sql": "do $$\nbegin\n for i in 1..3 by -1 loop\n raise notice '1..3 by -1: i = %', i;\n end loop;\nend$$",
@@ -207,24 +209,25 @@
207209
"plpgsql_call-23.sql": "DO $$\nDECLARE _a int; _b int;\nBEGIN\n _a := 10; _b := 30;\n CALL test_proc9(_a, _b);\n RAISE NOTICE '_a: %, _b: %', _a, _b;\nEND\n$$",
208210
"plpgsql_call-24.sql": "CREATE PROCEDURE test_proc10(IN a int, OUT b int, IN c int DEFAULT 11)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RAISE NOTICE 'a: %, b: %, c: %', a, b, c;\n b := a - c;\nEND;\n$$",
209211
"plpgsql_call-25.sql": "DO $$\nDECLARE _a int; _b int; _c int;\nBEGIN\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(_a, _b, _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(_a, _b, c => _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(a => _a, b => _b, c => _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(_a, c => _c, b => _b);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(_a, _b);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(_a, b => _b);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\n\n _a := 10; _b := 30; _c := 7;\n CALL test_proc10(b => _b, a => _a);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
210-
"plpgsql_call-26.sql": "DO $$\nDECLARE _a int; _b int; _c int;\nBEGIN\n _a := 10; _b := 30; _c := 7;\n CALL test_proc11(_a, _b, _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
211-
"plpgsql_call-27.sql": "CREATE PROCEDURE test_proc12(a anyelement, OUT b anyelement, OUT c anyarray)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RAISE NOTICE 'a: %', a;\n b := a;\n c := array[a];\nEND;\n$$",
212-
"plpgsql_call-28.sql": "DO $$\nDECLARE _a int; _b int; _c int[];\nBEGIN\n _a := 10;\n CALL test_proc12(_a, _b, _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
213-
"plpgsql_call-29.sql": "DO $$\nDECLARE _a int; _b int; _c text[];\nBEGIN\n _a := 10;\n CALL test_proc12(_a, _b, _c); -- error\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
214-
"plpgsql_call-30.sql": "CREATE FUNCTION triggerfunc1() RETURNS trigger\nLANGUAGE plpgsql\nAS $$\nDECLARE\n z int := 0;\nBEGIN\n CALL test_proc6(2, NEW.a, NEW.a);\n RETURN NEW;\nEND;\n$$",
215-
"plpgsql_call-31.sql": "CREATE PROCEDURE p1(v_cnt int, v_Text inout text = NULL)\nAS $$\nBEGIN\n v_Text := 'v_cnt = ' || v_cnt;\nEND\n$$ LANGUAGE plpgsql",
216-
"plpgsql_call-32.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer := 42;\nBEGIN\n CALL p1(v_cnt := v_cnt); -- error, must supply something for v_Text\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
217-
"plpgsql_call-33.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer := 42;\nBEGIN\n CALL p1(v_cnt := v_cnt, v_Text := v_Text);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
218-
"plpgsql_call-34.sql": "DO $$\nDECLARE\n v_Text text;\nBEGIN\n CALL p1(10, v_Text := v_Text);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
219-
"plpgsql_call-35.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer;\nBEGIN\n CALL p1(v_Text := v_Text, v_cnt := v_cnt);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
220-
"plpgsql_call-36.sql": "CREATE PROCEDURE inner_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'inner_p(%)', f1;\nEND\n$$ LANGUAGE plpgsql",
221-
"plpgsql_call-37.sql": "CREATE PROCEDURE outer_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'outer_p(%)', f1;\n CALL inner_p(f(f1));\nEND\n$$ LANGUAGE plpgsql",
222-
"plpgsql_call-38.sql": "CREATE FUNCTION outer_f (f1 int) RETURNS void\nAS $$\nBEGIN\n RAISE NOTICE 'outer_f(%)', f1;\n CALL inner_p(f(f1));\nEND\n$$ LANGUAGE plpgsql",
223-
"plpgsql_call-39.sql": "CREATE FUNCTION f_get_x () RETURNS int\nAS $$\nDECLARE l_result int;\nBEGIN\n SELECT x INTO l_result FROM t_test;\n RETURN l_result;\nEND\n$$ LANGUAGE plpgsql STABLE",
224-
"plpgsql_call-40.sql": "CREATE PROCEDURE f_print_x (x int)\nAS $$\nBEGIN\n RAISE NOTICE 'f_print_x(%)', x;\nEND\n$$ LANGUAGE plpgsql",
225-
"plpgsql_call-41.sql": "DO $$\nBEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n ROLLBACK;\nEND\n$$",
226-
"plpgsql_call-42.sql": "DO $$\nBEGIN\n BEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n EXCEPTION WHEN division_by_zero THEN\n RAISE NOTICE '%', SQLERRM;\n END;\n ROLLBACK;\nEND\n$$",
227-
"plpgsql_call-43.sql": "DO $$\nBEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\nEND\n$$",
212+
"plpgsql_call-26.sql": "CREATE PROCEDURE test_proc11(a OUT int, VARIADIC b int[])\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RAISE NOTICE 'a: %, b: %', a, b;\n a := b[1] + b[2];\nEND;\n$$",
213+
"plpgsql_call-27.sql": "DO $$\nDECLARE _a int; _b int; _c int;\nBEGIN\n _a := 10; _b := 30; _c := 7;\n CALL test_proc11(_a, _b, _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
214+
"plpgsql_call-28.sql": "CREATE PROCEDURE test_proc12(a anyelement, OUT b anyelement, OUT c anyarray)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RAISE NOTICE 'a: %', a;\n b := a;\n c := array[a];\nEND;\n$$",
215+
"plpgsql_call-29.sql": "DO $$\nDECLARE _a int; _b int; _c int[];\nBEGIN\n _a := 10;\n CALL test_proc12(_a, _b, _c);\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
216+
"plpgsql_call-30.sql": "DO $$\nDECLARE _a int; _b int; _c text[];\nBEGIN\n _a := 10;\n CALL test_proc12(_a, _b, _c); -- error\n RAISE NOTICE '_a: %, _b: %, _c: %', _a, _b, _c;\nEND\n$$",
217+
"plpgsql_call-31.sql": "CREATE FUNCTION triggerfunc1() RETURNS trigger\nLANGUAGE plpgsql\nAS $$\nDECLARE\n z int := 0;\nBEGIN\n CALL test_proc6(2, NEW.a, NEW.a);\n RETURN NEW;\nEND;\n$$",
218+
"plpgsql_call-32.sql": "CREATE PROCEDURE p1(v_cnt int, v_Text inout text = NULL)\nAS $$\nBEGIN\n v_Text := 'v_cnt = ' || v_cnt;\nEND\n$$ LANGUAGE plpgsql",
219+
"plpgsql_call-33.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer := 42;\nBEGIN\n CALL p1(v_cnt := v_cnt); -- error, must supply something for v_Text\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
220+
"plpgsql_call-34.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer := 42;\nBEGIN\n CALL p1(v_cnt := v_cnt, v_Text := v_Text);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
221+
"plpgsql_call-35.sql": "DO $$\nDECLARE\n v_Text text;\nBEGIN\n CALL p1(10, v_Text := v_Text);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
222+
"plpgsql_call-36.sql": "DO $$\nDECLARE\n v_Text text;\n v_cnt integer;\nBEGIN\n CALL p1(v_Text := v_Text, v_cnt := v_cnt);\n RAISE NOTICE '%', v_Text;\nEND;\n$$",
223+
"plpgsql_call-37.sql": "CREATE PROCEDURE inner_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'inner_p(%)', f1;\nEND\n$$ LANGUAGE plpgsql",
224+
"plpgsql_call-38.sql": "CREATE PROCEDURE outer_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'outer_p(%)', f1;\n CALL inner_p(f(f1));\nEND\n$$ LANGUAGE plpgsql",
225+
"plpgsql_call-39.sql": "CREATE FUNCTION outer_f (f1 int) RETURNS void\nAS $$\nBEGIN\n RAISE NOTICE 'outer_f(%)', f1;\n CALL inner_p(f(f1));\nEND\n$$ LANGUAGE plpgsql",
226+
"plpgsql_call-40.sql": "CREATE FUNCTION f_get_x () RETURNS int\nAS $$\nDECLARE l_result int;\nBEGIN\n SELECT x INTO l_result FROM t_test;\n RETURN l_result;\nEND\n$$ LANGUAGE plpgsql STABLE",
227+
"plpgsql_call-41.sql": "CREATE PROCEDURE f_print_x (x int)\nAS $$\nBEGIN\n RAISE NOTICE 'f_print_x(%)', x;\nEND\n$$ LANGUAGE plpgsql",
228+
"plpgsql_call-42.sql": "DO $$\nBEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n ROLLBACK;\nEND\n$$",
229+
"plpgsql_call-43.sql": "DO $$\nBEGIN\n BEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n EXCEPTION WHEN division_by_zero THEN\n RAISE NOTICE '%', SQLERRM;\n END;\n ROLLBACK;\nEND\n$$",
230+
"plpgsql_call-44.sql": "DO $$\nBEGIN\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\n UPDATE t_test SET x = x + 1;\n RAISE NOTICE 'f_get_x(%)', f_get_x();\n CALL f_print_x(f_get_x());\nEND\n$$",
228231
"plpgsql_cache-1.sql": "create function c_sillyaddone(int) returns int language plpgsql as\n$$ declare r c_mutable; begin r.f1 := $1; return r.f1 + 1; end $$",
229232
"plpgsql_cache-2.sql": "create function show_result_type(text) returns text language plpgsql as\n$$\n declare\n r record;\n t text;\n begin\n execute $1 into r;\n select pg_typeof(r.a) into t;\n return format('type %s value %s', t, r.a::text);\n end;\n$$",
230233
"plpgsql_array-1.sql": "do $$ declare a int[];\nbegin a := array[1,2]; a[3] := 4; raise notice 'a = %', a; end$$",

__fixtures__/plpgsql/plpgsql_deparser_fixes.sql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,25 @@ BEGIN
815815
updated.updated_at := now();
816816
RETURN updated;
817817
END$$;
818+
819+
-- Test 66: assignment whose value is a call using named arguments
820+
CREATE FUNCTION test_assign_named_args() RETURNS text
821+
LANGUAGE plpgsql AS $$
822+
DECLARE
823+
v_body text;
824+
BEGIN
825+
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
826+
RETURN v_body;
827+
END$$;
828+
829+
-- Test 67: field and subscript assignment targets with named-argument values
830+
CREATE FUNCTION test_assign_targets() RETURNS void
831+
LANGUAGE plpgsql AS $$
832+
DECLARE
833+
a int[] := ARRAY[1, 2];
834+
r record;
835+
BEGIN
836+
SELECT 1 AS f INTO r;
837+
a[2] := coalesce(nullif(3, 0), 4);
838+
r.f := greatest(1, 2);
839+
END$$;

packages/plpgsql-deparser/__tests__/__snapshots__/deparser-fixes.test.ts.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ EXCEPTION
276276
END"
277277
`;
278278

279+
exports[`plpgsql-deparser bug fixes assignment targets vs named arguments should keep the target when the value uses named arguments 1`] = `
280+
"DECLARE
281+
v_body text;
282+
BEGIN
283+
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
284+
RETURN v_body;
285+
END"
286+
`;
287+
279288
exports[`plpgsql-deparser bug fixes bare RAISE re-throw should keep a bare RAISE bare (not RAISE EXCEPTION;) 1`] = `
280289
"BEGIN
281290
PERFORM 1;

packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { loadModule, parsePlPgSQLSync } from 'libpg-query';
22

3-
import { deparseSync, PLpgSQLParseResult } from '../src';
3+
import { deparseSync, ParseMode, PLpgSQLParseResult } from '../src';
44
import { PLpgSQLTestUtils } from '../test-utils';
55

66
describe('plpgsql-deparser bug fixes', () => {
@@ -1216,6 +1216,112 @@ END$$`;
12161216
});
12171217
});
12181218

1219+
describe('assignment targets vs named arguments', () => {
1220+
it('should keep the target when the value uses named arguments', async () => {
1221+
const sql = `CREATE FUNCTION test_assign_named_args() RETURNS text
1222+
LANGUAGE plpgsql AS $$
1223+
DECLARE
1224+
v_body text;
1225+
BEGIN
1226+
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
1227+
RETURN v_body;
1228+
END$$`;
1229+
1230+
await testUtils.expectAstMatch('assignment with named arguments', sql);
1231+
1232+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1233+
const deparsed = deparseSync(parsed);
1234+
expect(deparsed).toMatchSnapshot();
1235+
expect(deparsed).toContain(
1236+
`v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');`
1237+
);
1238+
});
1239+
1240+
it('should emit a self-contained assignment exactly once', async () => {
1241+
const sql = `CREATE FUNCTION test_assign_self_contained(n int) RETURNS int
1242+
LANGUAGE plpgsql AS $$
1243+
DECLARE
1244+
sum int := 0;
1245+
BEGIN
1246+
sum := sum + n;
1247+
RETURN sum;
1248+
END$$`;
1249+
1250+
await testUtils.expectAstMatch('self-contained assignment', sql);
1251+
1252+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1253+
const deparsed = deparseSync(parsed);
1254+
expect(deparsed).toContain('sum := sum + n;');
1255+
expect(deparsed).not.toContain('sum := sum := ');
1256+
});
1257+
1258+
it('should keep field and subscript targets', async () => {
1259+
const sql = `CREATE FUNCTION test_assign_targets() RETURNS void
1260+
LANGUAGE plpgsql AS $$
1261+
DECLARE
1262+
a int[] := ARRAY[1, 2];
1263+
r record;
1264+
BEGIN
1265+
SELECT 1 AS f INTO r;
1266+
a[2] := coalesce(nullif(3, 0), 4);
1267+
r.f := greatest(1, 2);
1268+
END$$`;
1269+
1270+
await testUtils.expectAstMatch('field and subscript assignment targets', sql);
1271+
1272+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1273+
const deparsed = deparseSync(parsed);
1274+
expect(deparsed).toContain('a[2] := coalesce(nullif(3, 0), 4);');
1275+
expect(deparsed).toContain('r.f := greatest(1, 2);');
1276+
});
1277+
1278+
const buildAssign = (query: string, parseMode?: number): PLpgSQLParseResult => ({
1279+
plpgsql_funcs: [
1280+
{
1281+
PLpgSQL_function: {
1282+
datums: [{ PLpgSQL_var: { refname: 'v_x' } }],
1283+
action: {
1284+
PLpgSQL_stmt_block: {
1285+
body: [
1286+
{
1287+
PLpgSQL_stmt_assign: {
1288+
varno: 0,
1289+
expr: { PLpgSQL_expr: { query, parseMode } },
1290+
},
1291+
},
1292+
],
1293+
},
1294+
},
1295+
},
1296+
},
1297+
],
1298+
});
1299+
1300+
it('should keep the target of a hand-built assignment whose value uses named arguments', () => {
1301+
const deparsed = deparseSync(
1302+
buildAssign(`ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f')`)
1303+
);
1304+
1305+
expect(deparsed).toContain(
1306+
`v_x := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');`
1307+
);
1308+
});
1309+
1310+
// A missing parseMode is treated as "not an assignment parse mode": the query
1311+
// text is the value only, and the target comes from varno.
1312+
it('should treat an absent parseMode as a value-only expression', () => {
1313+
expect(deparseSync(buildAssign('1 + 1'))).toContain('v_x := 1 + 1;');
1314+
expect(deparseSync(buildAssign('v_x := 1'))).toContain('v_x := v_x := 1;');
1315+
expect(deparseSync(buildAssign('v_x := 1', ParseMode.RAW_PARSE_PLPGSQL_ASSIGN1))).toContain('v_x := 1;');
1316+
});
1317+
1318+
it('should un-parenthesize a subscripted target carried in the query text', () => {
1319+
const deparsed = deparseSync(buildAssign('(v_x)[2] := 5', ParseMode.RAW_PARSE_PLPGSQL_ASSIGN3));
1320+
1321+
expect(deparsed).toContain('v_x[2] := 5;');
1322+
});
1323+
});
1324+
12191325
describe('uppercase %ROWTYPE/%TYPE references', () => {
12201326
it('should not quote uppercase %ROWTYPE and %TYPE references', async () => {
12211327
const sql = `CREATE FUNCTION test_rowtype_case() RETURNS void

packages/plpgsql-deparser/src/plpgsql-deparser.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import { QuoteUtils } from 'pgsql-deparser';
1515

16+
import { ParseMode } from './hydrate-types';
1617
import {
1718
DiagItemKind,
1819
ElogLevel,
@@ -59,6 +60,18 @@ import {
5960
RaiseOptionType,
6061
} from './types';
6162

63+
/**
64+
* PostgreSQL parses the right-hand side of an assignment in a dedicated raw
65+
* parse mode per target shape — RAW_PARSE_PLPGSQL_ASSIGN1 (3), ASSIGN2 (4) and
66+
* ASSIGN3 (5) for `var`, `var.field` and `var.field[...]` targets — and that
67+
* mode is preserved on `PLpgSQL_expr.parseMode`. In those modes the stored
68+
* query text is the entire assignment, target included (`cnt := cnt + 1`);
69+
* every lower mode (RAW_PARSE_DEFAULT, RAW_PARSE_TYPE_NAME,
70+
* RAW_PARSE_PLPGSQL_EXPR) is a plain expression/statement parse whose text is
71+
* only the value.
72+
*/
73+
const LOWEST_ASSIGN_PARSE_MODE = ParseMode.RAW_PARSE_PLPGSQL_ASSIGN1;
74+
6275
export interface PLpgSQLDeparserOptions {
6376
indent?: string;
6477
newline?: string;
@@ -1129,19 +1142,35 @@ export class PLpgSQLDeparser {
11291142
private deparseAssign(assign: PLpgSQL_stmt_assign, context: PLpgSQLDeparserContext): string {
11301143
const varName = this.getVarName(assign.varno, context);
11311144
const expr = assign.expr ? this.deparseExpr(assign.expr) : '';
1132-
1133-
// The expression already contains the assignment in the query
1134-
// e.g., "sum := sum + n"
1135-
if (expr.includes(':=')) {
1145+
const parseMode = assign.expr?.PLpgSQL_expr?.parseMode;
1146+
1147+
if (this.exprCarriesAssignmentTarget(parseMode)) {
11361148
// The SQL deparser parenthesizes subscripted targets like '(a)[2]',
11371149
// but the PL/pgSQL assignment grammar requires a bare identifier
11381150
// before subscripts/field selections.
11391151
return expr.replace(/^\((\w+(?:\.\w+)*)\)(?=\[|\.)/, '$1');
11401152
}
1141-
1153+
11421154
return `${varName} := ${expr}`;
11431155
}
11441156

1157+
/**
1158+
* Whether the stored query text of an assignment already spells out the
1159+
* assignment target, so that prefixing the target again would emit it twice.
1160+
*
1161+
* The parse mode answers this exactly. A substring test for ':=' cannot: ':='
1162+
* is also PostgreSQL's named-argument operator, so a value like `f(a := 1)`
1163+
* reads as self-contained and the target gets silently dropped.
1164+
*
1165+
* An absent parseMode — hand-built nodes, or output from a PostgreSQL older
1166+
* than the assignment parse modes — means the same as mode 0: the text is the
1167+
* value only and the target comes from `varno`. A hand-built node that does
1168+
* embed its target in the text must say so with `parseMode`.
1169+
*/
1170+
private exprCarriesAssignmentTarget(parseMode?: number): boolean {
1171+
return (parseMode ?? ParseMode.RAW_PARSE_DEFAULT) >= LOWEST_ASSIGN_PARSE_MODE;
1172+
}
1173+
11451174
/**
11461175
* Deparse an IF statement
11471176
*/

0 commit comments

Comments
 (0)