Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions __fixtures__/plpgsql-generated/generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
Expand Down Expand Up @@ -207,24 +209,25 @@
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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",
"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$$",
"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$$",
"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$$",
"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$$",
"plpgsql_call-36.sql": "CREATE PROCEDURE inner_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'inner_p(%)', f1;\nEND\n$$ LANGUAGE plpgsql",
"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",
"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",
"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",
"plpgsql_call-40.sql": "CREATE PROCEDURE f_print_x (x int)\nAS $$\nBEGIN\n RAISE NOTICE 'f_print_x(%)', x;\nEND\n$$ LANGUAGE plpgsql",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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$$",
"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",
"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$$",
"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$$",
"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$$",
"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$$",
"plpgsql_call-37.sql": "CREATE PROCEDURE inner_p (f1 int)\nAS $$\nBEGIN\n RAISE NOTICE 'inner_p(%)', f1;\nEND\n$$ LANGUAGE plpgsql",
"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",
"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",
"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",
"plpgsql_call-41.sql": "CREATE PROCEDURE f_print_x (x int)\nAS $$\nBEGIN\n RAISE NOTICE 'f_print_x(%)', x;\nEND\n$$ LANGUAGE plpgsql",
"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$$",
"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$$",
"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$$",
"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 $$",
"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$$",
"plpgsql_array-1.sql": "do $$ declare a int[];\nbegin a := array[1,2]; a[3] := 4; raise notice 'a = %', a; end$$",
Expand Down
22 changes: 22 additions & 0 deletions __fixtures__/plpgsql/plpgsql_deparser_fixes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,25 @@ BEGIN
updated.updated_at := now();
RETURN updated;
END$$;

-- Test 66: assignment whose value is a call using named arguments
CREATE FUNCTION test_assign_named_args() RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_body text;
BEGIN
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
RETURN v_body;
END$$;

-- Test 67: field and subscript assignment targets with named-argument values
CREATE FUNCTION test_assign_targets() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
a int[] := ARRAY[1, 2];
r record;
BEGIN
SELECT 1 AS f INTO r;
a[2] := coalesce(nullif(3, 0), 4);
r.f := greatest(1, 2);
END$$;
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ EXCEPTION
END"
`;

exports[`plpgsql-deparser bug fixes assignment targets vs named arguments should keep the target when the value uses named arguments 1`] = `
"DECLARE
v_body text;
BEGIN
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
RETURN v_body;
END"
`;

exports[`plpgsql-deparser bug fixes bare RAISE re-throw should keep a bare RAISE bare (not RAISE EXCEPTION;) 1`] = `
"BEGIN
PERFORM 1;
Expand Down
108 changes: 107 additions & 1 deletion packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { loadModule, parsePlPgSQLSync } from 'libpg-query';

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

describe('plpgsql-deparser bug fixes', () => {
Expand Down Expand Up @@ -1216,6 +1216,112 @@ END$$`;
});
});

describe('assignment targets vs named arguments', () => {
it('should keep the target when the value uses named arguments', async () => {
const sql = `CREATE FUNCTION test_assign_named_args() RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_body text;
BEGIN
v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');
RETURN v_body;
END$$`;

await testUtils.expectAstMatch('assignment with named arguments', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain(
`v_body := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');`
);
});

it('should emit a self-contained assignment exactly once', async () => {
const sql = `CREATE FUNCTION test_assign_self_contained(n int) RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
sum int := 0;
BEGIN
sum := sum + n;
RETURN sum;
END$$`;

await testUtils.expectAstMatch('self-contained assignment', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toContain('sum := sum + n;');
expect(deparsed).not.toContain('sum := sum := ');
});

it('should keep field and subscript targets', async () => {
const sql = `CREATE FUNCTION test_assign_targets() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
a int[] := ARRAY[1, 2];
r record;
BEGIN
SELECT 1 AS f INTO r;
a[2] := coalesce(nullif(3, 0), 4);
r.f := greatest(1, 2);
END$$`;

await testUtils.expectAstMatch('field and subscript assignment targets', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toContain('a[2] := coalesce(nullif(3, 0), 4);');
expect(deparsed).toContain('r.f := greatest(1, 2);');
});

const buildAssign = (query: string, parseMode?: number): PLpgSQLParseResult => ({
plpgsql_funcs: [
{
PLpgSQL_function: {
datums: [{ PLpgSQL_var: { refname: 'v_x' } }],
action: {
PLpgSQL_stmt_block: {
body: [
{
PLpgSQL_stmt_assign: {
varno: 0,
expr: { PLpgSQL_expr: { query, parseMode } },
},
},
],
},
},
},
},
],
});

it('should keep the target of a hand-built assignment whose value uses named arguments', () => {
const deparsed = deparseSync(
buildAssign(`ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f')`)
);

expect(deparsed).toContain(
`v_x := ast_helpers.create_function(v_schema_name := 'app_public', v_function_name := 'f');`
);
});

// A missing parseMode is treated as "not an assignment parse mode": the query
// text is the value only, and the target comes from varno.
it('should treat an absent parseMode as a value-only expression', () => {
expect(deparseSync(buildAssign('1 + 1'))).toContain('v_x := 1 + 1;');
expect(deparseSync(buildAssign('v_x := 1'))).toContain('v_x := v_x := 1;');
expect(deparseSync(buildAssign('v_x := 1', ParseMode.RAW_PARSE_PLPGSQL_ASSIGN1))).toContain('v_x := 1;');
});

it('should un-parenthesize a subscripted target carried in the query text', () => {
const deparsed = deparseSync(buildAssign('(v_x)[2] := 5', ParseMode.RAW_PARSE_PLPGSQL_ASSIGN3));

expect(deparsed).toContain('v_x[2] := 5;');
});
});

describe('uppercase %ROWTYPE/%TYPE references', () => {
it('should not quote uppercase %ROWTYPE and %TYPE references', async () => {
const sql = `CREATE FUNCTION test_rowtype_case() RETURNS void
Expand Down
39 changes: 34 additions & 5 deletions packages/plpgsql-deparser/src/plpgsql-deparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import { QuoteUtils } from 'pgsql-deparser';

import { ParseMode } from './hydrate-types';
import {
DiagItemKind,
ElogLevel,
Expand Down Expand Up @@ -59,6 +60,18 @@ import {
RaiseOptionType,
} from './types';

/**
* PostgreSQL parses the right-hand side of an assignment in a dedicated raw
* parse mode per target shape — RAW_PARSE_PLPGSQL_ASSIGN1 (3), ASSIGN2 (4) and
* ASSIGN3 (5) for `var`, `var.field` and `var.field[...]` targets — and that
* mode is preserved on `PLpgSQL_expr.parseMode`. In those modes the stored
* query text is the entire assignment, target included (`cnt := cnt + 1`);
* every lower mode (RAW_PARSE_DEFAULT, RAW_PARSE_TYPE_NAME,
* RAW_PARSE_PLPGSQL_EXPR) is a plain expression/statement parse whose text is
* only the value.
*/
const LOWEST_ASSIGN_PARSE_MODE = ParseMode.RAW_PARSE_PLPGSQL_ASSIGN1;

export interface PLpgSQLDeparserOptions {
indent?: string;
newline?: string;
Expand Down Expand Up @@ -1129,19 +1142,35 @@ export class PLpgSQLDeparser {
private deparseAssign(assign: PLpgSQL_stmt_assign, context: PLpgSQLDeparserContext): string {
const varName = this.getVarName(assign.varno, context);
const expr = assign.expr ? this.deparseExpr(assign.expr) : '';

// The expression already contains the assignment in the query
// e.g., "sum := sum + n"
if (expr.includes(':=')) {
const parseMode = assign.expr?.PLpgSQL_expr?.parseMode;

if (this.exprCarriesAssignmentTarget(parseMode)) {
// The SQL deparser parenthesizes subscripted targets like '(a)[2]',
// but the PL/pgSQL assignment grammar requires a bare identifier
// before subscripts/field selections.
return expr.replace(/^\((\w+(?:\.\w+)*)\)(?=\[|\.)/, '$1');
}

return `${varName} := ${expr}`;
}

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

/**
* Deparse an IF statement
*/
Expand Down
Loading