Skip to content
Closed
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
3 changes: 3 additions & 0 deletions __fixtures__/plpgsql-generated/generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@
"plpgsql_deparser_fixes-58.sql": "-- Test 58: RETURN NEXT with a variable (retvarno must be emitted as the variable name)\nCREATE FUNCTION test_return_next_var() RETURNS SETOF integer\nLANGUAGE plpgsql AS $$\nDECLARE\n r integer;\nBEGIN\n FOR r IN SELECT g FROM generate_series(1, 3) g LOOP\n RETURN NEXT r;\n END LOOP;\nEND$$",
"plpgsql_deparser_fixes-59.sql": "-- Test 59: Top-level block with EXCEPTION clause (compiler wraps it in a synthetic outer block; must not deparse a nested BEGIN)\nCREATE FUNCTION test_toplevel_exception(a numeric, b numeric) RETURNS numeric\nLANGUAGE plpgsql AS $$\nDECLARE\n v_result numeric;\nBEGIN\n v_result := a / b;\n RETURN v_result;\nEXCEPTION\n WHEN division_by_zero THEN\n RETURN NULL;\nEND$$",
"plpgsql_deparser_fixes-60.sql": "-- Test 60: Explicit nested block with EXCEPTION inside a top-level block (nesting must be preserved)\nCREATE 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$$",
"plpgsql_deparser_fixes-61.sql": "-- Test 61: ALIAS FOR a positional parameter (alias declaration must be preserved)\nCREATE 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-62.sql": "-- Test 62: ALIAS FOR a named parameter and a local variable\nCREATE 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-63.sql": "-- Test 63: ALIAS FOR OLD/NEW in a trigger function\nCREATE 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_control-1.sql": "--\n-- Tests for PL/pgSQL control structures\n--\n\n-- integer FOR loop\n\ndo $$\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": "-- BY can't be zero or negative\ndo $$\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
32 changes: 32 additions & 0 deletions __fixtures__/plpgsql/plpgsql_deparser_fixes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,35 @@ BEGIN
END;
RETURN v_result;
END$$;

-- Test 61: ALIAS FOR a positional parameter (alias declaration must be preserved)
CREATE FUNCTION test_alias_positional_param(integer) RETURNS integer
LANGUAGE plpgsql AS $$
DECLARE
arg ALIAS FOR $1;
BEGIN
RETURN arg + 1;
END$$;

-- Test 62: ALIAS FOR a named parameter and a local variable
CREATE FUNCTION test_alias_named(input_value text) RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
val ALIAS FOR input_value;
buffer text := 'x';
buf ALIAS FOR buffer;
BEGIN
buf := buf || val;
RETURN buf;
END$$;

-- Test 63: ALIAS FOR OLD/NEW in a trigger function
CREATE FUNCTION test_alias_trigger() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
prior ALIAS FOR old;
updated ALIAS FOR new;
BEGIN
updated.updated_at := now();
RETURN updated;
END$$;
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`plpgsql-deparser bug fixes ALIAS declarations should interleave aliases with variable declarations by source order 1`] = `
"DECLARE
val ALIAS FOR input_value;
buffer text := 'x';
buf ALIAS FOR buffer;
BEGIN
buf := buf || val;
RETURN buf;
END"
`;

exports[`plpgsql-deparser bug fixes ALIAS declarations should preserve ALIAS FOR OLD/NEW in trigger functions 1`] = `
"DECLARE
prior ALIAS FOR old;
updated ALIAS FOR new;
BEGIN
updated.updated_at := now();
RETURN updated;
END"
`;

exports[`plpgsql-deparser bug fixes ALIAS declarations should preserve ALIAS FOR a positional parameter 1`] = `
"DECLARE
arg ALIAS FOR $1;
BEGIN
RETURN arg + 1;
END"
`;

exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should handle multi-column RETURNING ... INTO 1`] = `
"DECLARE
v_id uuid;
Expand Down
61 changes: 61 additions & 0 deletions packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1345,4 +1345,65 @@ END$$`;
expect(beginCount).toBe(2);
});
});

describe('ALIAS declarations', () => {
it('should preserve ALIAS FOR a positional parameter', async () => {
const sql = `CREATE FUNCTION test_alias_positional_param(integer) RETURNS integer
LANGUAGE plpgsql AS $$
DECLARE
arg ALIAS FOR $1;
BEGIN
RETURN arg + 1;
END$$`;

await testUtils.expectAstMatch('alias for positional parameter', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('arg ALIAS FOR $1;');
});

it('should interleave aliases with variable declarations by source order', async () => {
const sql = `CREATE FUNCTION test_alias_named(input_value text) RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
val ALIAS FOR input_value;
buffer text := 'x';
buf ALIAS FOR buffer;
BEGIN
buf := buf || val;
RETURN buf;
END$$`;

await testUtils.expectAstMatch('aliases interleaved with variables', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
// the alias of a local variable must come after that variable's declaration
expect(deparsed.indexOf('buf ALIAS FOR buffer;')).toBeGreaterThan(deparsed.indexOf('buffer text'));
expect(deparsed).toContain('val ALIAS FOR input_value;');
});

it('should preserve ALIAS FOR OLD/NEW in trigger functions', async () => {
const sql = `CREATE FUNCTION test_alias_trigger() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
prior ALIAS FOR old;
updated ALIAS FOR new;
BEGIN
updated.updated_at := now();
RETURN updated;
END$$`;

await testUtils.expectAstMatch('alias for OLD/NEW in trigger', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('prior ALIAS FOR old;');
expect(deparsed).toContain('updated ALIAS FOR new;');
});
});
});
41 changes: 33 additions & 8 deletions packages/plpgsql-deparser/src/plpgsql-deparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PLpgSQLFunctionNode,
PLpgSQL_function,
PLpgSQLDatum,
PLpgSQLAliasNode,
PLpgSQL_var,
PLpgSQL_rec,
PLpgSQL_row,
Expand Down Expand Up @@ -213,7 +214,8 @@ export class PLpgSQLDeparser {
context,
loopVarLinenos,
undefined, // includedIndices - not used for top-level
nestedDatumIndices // excludedIndices - exclude datums that belong to nested blocks
nestedDatumIndices, // excludedIndices - exclude datums that belong to nested blocks
func.aliases
);
if (declareSection) {
parts.push(declareSection);
Expand Down Expand Up @@ -631,7 +633,8 @@ export class PLpgSQLDeparser {
context: PLpgSQLDeparserContext,
loopVarLinenos: Set<number> = new Set(),
includedIndices?: Set<number>,
excludedIndices?: Set<number>
excludedIndices?: Set<number>,
aliases?: PLpgSQLAliasNode[]
): string {
if (!datums || datums.length === 0) {
return '';
Expand Down Expand Up @@ -698,20 +701,42 @@ export class PLpgSQLDeparser {
return false;
});

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

const kw = this.keyword;
const parts: string[] = [kw('DECLARE')];

for (const datum of localVars) {
const varDecl = this.deparseDatum(datum, context);
if (varDecl) {
parts.push(this.indent(varDecl + ';', context.indentLevel + 1));
const lineno =
('PLpgSQL_var' in datum ? datum.PLpgSQL_var.lineno : undefined) ??
('PLpgSQL_rec' in datum ? datum.PLpgSQL_rec.lineno : undefined) ??
0;
decls.push({ lineno, order: 0, text: varDecl });
}
}

if (decls.length === 0) {
return '';
}

decls.sort((a, b) => a.lineno - b.lineno || a.order - b.order);

const kw = this.keyword;
const parts: string[] = [kw('DECLARE')];

for (const decl of decls) {
parts.push(this.indent(decl.text + ';', context.indentLevel + 1));
}

return parts.join(this.options.newline);
}

Expand Down
12 changes: 12 additions & 0 deletions packages/plpgsql-deparser/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export interface PLpgSQL_function {
datums?: PLpgSQLDatum[];
action?: PLpgSQLStmtNode;
out_param_varno?: number;
aliases?: PLpgSQLAliasNode[];
}

export type PLpgSQLAliasNode = {
PLpgSQL_alias: PLpgSQL_alias;
};

/** An ALIAS declaration (e.g. `arg ALIAS FOR $1`): a name bound to an existing datum */
export interface PLpgSQL_alias {
name: string;
varno: number;
lineno?: number;
}

export type PLpgSQLDatum =
Expand Down
Loading