Skip to content

Commit 792dcf3

Browse files
authored
Merge pull request #312 from constructive-io/fix/plpgsql-alias-declarations
fix(plpgsql-deparser): preserve ALIAS FOR declarations
2 parents d57254a + b6ffee2 commit 792dcf3

6 files changed

Lines changed: 170 additions & 8 deletions

File tree

‎__fixtures__/plpgsql-generated/generated.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@
160160
"plpgsql_deparser_fixes-60.sql": "CREATE FUNCTION test_explicit_nested_exception(p_id integer) RETURNS text\nLANGUAGE plpgsql AS $$\nDECLARE\n v_result text;\nBEGIN\n v_result := 'unknown';\n BEGIN\n SELECT status INTO v_result FROM items WHERE id = p_id;\n EXCEPTION\n WHEN no_data_found THEN\n v_result := 'not_found';\n END;\n RETURN v_result;\nEND$$",
161161
"plpgsql_deparser_fixes-61.sql": "CREATE FUNCTION test_out_param_bare_return(\n IN a text,\n IN b uuid,\n OUT result uuid\n) RETURNS uuid\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\nBEGIN\n v_id := b;\n SELECT v_id INTO result;\n RETURN;\nEND$$",
162162
"plpgsql_deparser_fixes-62.sql": "CREATE FUNCTION test_multi_out_bare_return(\n IN a integer,\n OUT x integer,\n OUT y text\n)\nLANGUAGE plpgsql AS $$\nBEGIN\n x := a;\n y := 'ok';\n RETURN;\nEND$$",
163+
"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$$",
164+
"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$$",
165+
"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$$",
163166
"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$$",
164167
"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$$",
165168
"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$$",

‎__fixtures__/plpgsql/plpgsql_deparser_fixes.sql‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,3 +783,35 @@ BEGIN
783783
y := 'ok';
784784
RETURN;
785785
END$$;
786+
787+
-- Test 63: ALIAS FOR a positional parameter (alias declaration must be preserved)
788+
CREATE FUNCTION test_alias_positional_param(integer) RETURNS integer
789+
LANGUAGE plpgsql AS $$
790+
DECLARE
791+
arg ALIAS FOR $1;
792+
BEGIN
793+
RETURN arg + 1;
794+
END$$;
795+
796+
-- Test 64: ALIAS FOR a named parameter and a local variable
797+
CREATE FUNCTION test_alias_named(input_value text) RETURNS text
798+
LANGUAGE plpgsql AS $$
799+
DECLARE
800+
val ALIAS FOR input_value;
801+
buffer text := 'x';
802+
buf ALIAS FOR buffer;
803+
BEGIN
804+
buf := buf || val;
805+
RETURN buf;
806+
END$$;
807+
808+
-- Test 65: ALIAS FOR OLD/NEW in a trigger function
809+
CREATE FUNCTION test_alias_trigger() RETURNS trigger
810+
LANGUAGE plpgsql AS $$
811+
DECLARE
812+
prior ALIAS FOR old;
813+
updated ALIAS FOR new;
814+
BEGIN
815+
updated.updated_at := now();
816+
RETURN updated;
817+
END$$;

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3+
exports[`plpgsql-deparser bug fixes ALIAS declarations should interleave aliases with variable declarations by source order 1`] = `
4+
"DECLARE
5+
val ALIAS FOR input_value;
6+
buffer text := 'x';
7+
buf ALIAS FOR buffer;
8+
BEGIN
9+
buf := buf || val;
10+
RETURN buffer;
11+
END"
12+
`;
13+
14+
exports[`plpgsql-deparser bug fixes ALIAS declarations should preserve ALIAS FOR OLD/NEW in trigger functions 1`] = `
15+
"DECLARE
16+
prior ALIAS FOR old;
17+
updated ALIAS FOR new;
18+
BEGIN
19+
updated.updated_at := now();
20+
RETURN NEW;
21+
END"
22+
`;
23+
24+
exports[`plpgsql-deparser bug fixes ALIAS declarations should preserve ALIAS FOR a positional parameter 1`] = `
25+
"DECLARE
26+
arg ALIAS FOR $1;
27+
BEGIN
28+
RETURN arg + 1;
29+
END"
30+
`;
31+
332
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should handle multi-column RETURNING ... INTO 1`] = `
433
"DECLARE
534
v_id uuid;

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,4 +1345,65 @@ END$$`;
13451345
expect(beginCount).toBe(2);
13461346
});
13471347
});
1348+
1349+
describe('ALIAS declarations', () => {
1350+
it('should preserve ALIAS FOR a positional parameter', async () => {
1351+
const sql = `CREATE FUNCTION test_alias_positional_param(integer) RETURNS integer
1352+
LANGUAGE plpgsql AS $$
1353+
DECLARE
1354+
arg ALIAS FOR $1;
1355+
BEGIN
1356+
RETURN arg + 1;
1357+
END$$`;
1358+
1359+
await testUtils.expectAstMatch('alias for positional parameter', sql);
1360+
1361+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1362+
const deparsed = deparseSync(parsed);
1363+
expect(deparsed).toMatchSnapshot();
1364+
expect(deparsed).toContain('arg ALIAS FOR $1;');
1365+
});
1366+
1367+
it('should interleave aliases with variable declarations by source order', async () => {
1368+
const sql = `CREATE FUNCTION test_alias_named(input_value text) RETURNS text
1369+
LANGUAGE plpgsql AS $$
1370+
DECLARE
1371+
val ALIAS FOR input_value;
1372+
buffer text := 'x';
1373+
buf ALIAS FOR buffer;
1374+
BEGIN
1375+
buf := buf || val;
1376+
RETURN buf;
1377+
END$$`;
1378+
1379+
await testUtils.expectAstMatch('aliases interleaved with variables', sql);
1380+
1381+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1382+
const deparsed = deparseSync(parsed);
1383+
expect(deparsed).toMatchSnapshot();
1384+
// the alias of a local variable must come after that variable's declaration
1385+
expect(deparsed.indexOf('buf ALIAS FOR buffer;')).toBeGreaterThan(deparsed.indexOf('buffer text'));
1386+
expect(deparsed).toContain('val ALIAS FOR input_value;');
1387+
});
1388+
1389+
it('should preserve ALIAS FOR OLD/NEW in trigger functions', async () => {
1390+
const sql = `CREATE FUNCTION test_alias_trigger() RETURNS trigger
1391+
LANGUAGE plpgsql AS $$
1392+
DECLARE
1393+
prior ALIAS FOR old;
1394+
updated ALIAS FOR new;
1395+
BEGIN
1396+
updated.updated_at := now();
1397+
RETURN updated;
1398+
END$$`;
1399+
1400+
await testUtils.expectAstMatch('alias for OLD/NEW in trigger', sql);
1401+
1402+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
1403+
const deparsed = deparseSync(parsed);
1404+
expect(deparsed).toMatchSnapshot();
1405+
expect(deparsed).toContain('prior ALIAS FOR old;');
1406+
expect(deparsed).toContain('updated ALIAS FOR new;');
1407+
});
1408+
});
13481409
});

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
PLpgSQLFunctionNode,
1818
PLpgSQL_function,
1919
PLpgSQLDatum,
20+
PLpgSQLAliasNode,
2021
PLpgSQL_var,
2122
PLpgSQL_rec,
2223
PLpgSQL_row,
@@ -215,7 +216,8 @@ export class PLpgSQLDeparser {
215216
context,
216217
loopVarLinenos,
217218
undefined, // includedIndices - not used for top-level
218-
nestedDatumIndices // excludedIndices - exclude datums that belong to nested blocks
219+
nestedDatumIndices, // excludedIndices - exclude datums that belong to nested blocks
220+
func.aliases
219221
);
220222
if (declareSection) {
221223
parts.push(declareSection);
@@ -633,7 +635,8 @@ export class PLpgSQLDeparser {
633635
context: PLpgSQLDeparserContext,
634636
loopVarLinenos: Set<number> = new Set(),
635637
includedIndices?: Set<number>,
636-
excludedIndices?: Set<number>
638+
excludedIndices?: Set<number>,
639+
aliases?: PLpgSQLAliasNode[]
637640
): string {
638641
if (!datums || datums.length === 0) {
639642
return '';
@@ -700,20 +703,42 @@ export class PLpgSQLDeparser {
700703
return false;
701704
});
702705

703-
if (localVars.length === 0) {
704-
return '';
706+
// ALIAS declarations bind a new name to an existing datum (e.g. `arg ALIAS FOR $1`).
707+
// They are interleaved with variable declarations by source lineno so an alias
708+
// targeting a local variable is emitted after that variable's declaration.
709+
// On equal linenos, variables sort before aliases so a same-line alias
710+
// targeting a variable still follows its declaration.
711+
const decls: { lineno: number; order: number; text: string }[] = [];
712+
for (const alias of aliases ?? []) {
713+
const { name, varno, lineno } = alias.PLpgSQL_alias;
714+
const targetName = this.getVarName(varno, context);
715+
decls.push({ lineno: lineno ?? 0, order: 1, text: `${name} ${this.keyword('ALIAS FOR')} ${targetName}` });
705716
}
706717

707-
const kw = this.keyword;
708-
const parts: string[] = [kw('DECLARE')];
709-
710718
for (const datum of localVars) {
711719
const varDecl = this.deparseDatum(datum, context);
712720
if (varDecl) {
713-
parts.push(this.indent(varDecl + ';', context.indentLevel + 1));
721+
const lineno =
722+
('PLpgSQL_var' in datum ? datum.PLpgSQL_var.lineno : undefined) ??
723+
('PLpgSQL_rec' in datum ? datum.PLpgSQL_rec.lineno : undefined) ??
724+
0;
725+
decls.push({ lineno, order: 0, text: varDecl });
714726
}
715727
}
716728

729+
if (decls.length === 0) {
730+
return '';
731+
}
732+
733+
decls.sort((a, b) => a.lineno - b.lineno || a.order - b.order);
734+
735+
const kw = this.keyword;
736+
const parts: string[] = [kw('DECLARE')];
737+
738+
for (const decl of decls) {
739+
parts.push(this.indent(decl.text + ';', context.indentLevel + 1));
740+
}
741+
717742
return parts.join(this.options.newline);
718743
}
719744

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export interface PLpgSQL_function {
1818
datums?: PLpgSQLDatum[];
1919
action?: PLpgSQLStmtNode;
2020
out_param_varno?: number;
21+
aliases?: PLpgSQLAliasNode[];
22+
}
23+
24+
export type PLpgSQLAliasNode = {
25+
PLpgSQL_alias: PLpgSQL_alias;
26+
};
27+
28+
/** An ALIAS declaration (e.g. `arg ALIAS FOR $1`): a name bound to an existing datum */
29+
export interface PLpgSQL_alias {
30+
name: string;
31+
varno: number;
32+
lineno?: number;
2133
}
2234

2335
export type PLpgSQLDatum =

0 commit comments

Comments
 (0)