Skip to content

Commit 2170367

Browse files
committed
fix: resolve OUT-param varno by signature names for bare RETURN
1 parent 13f064c commit 2170367

6 files changed

Lines changed: 106 additions & 9 deletions

File tree

‎__fixtures__/plpgsql-generated/generated.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@
158158
"plpgsql_deparser_fixes-58.sql": "CREATE 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$$",
159159
"plpgsql_deparser_fixes-59.sql": "CREATE 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$$",
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$$",
161+
"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$$",
162+
"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$$",
161163
"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$$",
162164
"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$$",
163165
"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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,3 +754,32 @@ BEGIN
754754
END;
755755
RETURN v_result;
756756
END$$;
757+
758+
-- Test 61: Bare RETURN in function whose single OUT param is not the first datum
759+
-- (libpg-query 18 omits out_param_varno, so the OUT datum must be resolved by name)
760+
CREATE FUNCTION test_out_param_bare_return(
761+
IN a text,
762+
IN b uuid,
763+
OUT result uuid
764+
) RETURNS uuid
765+
LANGUAGE plpgsql AS $$
766+
DECLARE
767+
v_id uuid;
768+
BEGIN
769+
v_id := b;
770+
SELECT v_id INTO result;
771+
RETURN;
772+
END$$;
773+
774+
-- Test 62: Bare RETURN with multiple OUT params (unnamed-row OUT datum)
775+
CREATE FUNCTION test_multi_out_bare_return(
776+
IN a integer,
777+
OUT x integer,
778+
OUT y text
779+
)
780+
LANGUAGE plpgsql AS $$
781+
BEGIN
782+
x := a;
783+
y := 'ok';
784+
RETURN;
785+
END$$;

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export type ReturnInfoKind = 'void' | 'setof' | 'trigger' | 'scalar' | 'out_para
7575

7676
export interface ReturnInfo {
7777
kind: ReturnInfoKind;
78+
/** Names of OUT/INOUT/TABLE parameters, used to locate the OUT-param datum */
79+
outParamNames?: string[];
7880
}
7981

8082
export interface PLpgSQLDeparserContext {
@@ -2077,6 +2079,26 @@ export class PLpgSQLDeparser {
20772079
return func.out_param_varno;
20782080
}
20792081
if (func.datums) {
2082+
const outNames = returnInfo?.outParamNames;
2083+
if (returnInfo?.kind === 'out_params' && outNames && outNames.length === 1) {
2084+
const varIdx = func.datums.findIndex(
2085+
d => 'PLpgSQL_var' in d && d.PLpgSQL_var.refname === outNames[0]
2086+
);
2087+
if (varIdx >= 0) {
2088+
return varIdx;
2089+
}
2090+
}
2091+
if (returnInfo?.kind === 'out_params' && outNames && outNames.length > 1) {
2092+
const rowIdx = func.datums.findIndex(d => {
2093+
if (!('PLpgSQL_row' in d) || d.PLpgSQL_row.refname !== '(unnamed row)') return false;
2094+
const fields = d.PLpgSQL_row.fields || [];
2095+
return fields.length === outNames.length &&
2096+
fields.every((f, i) => f.name === outNames[i]);
2097+
});
2098+
if (rowIdx >= 0) {
2099+
return rowIdx;
2100+
}
2101+
}
20802102
const rowIdx = func.datums.findIndex(
20812103
d => 'PLpgSQL_row' in d && d.PLpgSQL_row.refname === '(unnamed row)' && d.PLpgSQL_row.lineno === -1
20822104
);

‎packages/plpgsql-deparser/test-utils/index.ts‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { parsePlPgSQL, parsePlPgSQLSync } from 'libpg-query';
2-
import { deparseSync, PLpgSQLParseResult } from '../src';
1+
import { parsePlPgSQL, parsePlPgSQLSync, parseSync } from 'libpg-query';
2+
import { deparseSync, PLpgSQLParseResult, ReturnInfo } from '../src';
33
import { readFileSync, readdirSync, existsSync } from 'fs';
44
import * as path from 'path';
55
import { diff } from 'jest-diff';
@@ -13,6 +13,39 @@ export interface PLpgSQLTestCase {
1313
functionBody: string;
1414
}
1515

16+
/**
17+
* Derive ReturnInfo from a CREATE FUNCTION/PROCEDURE statement's signature
18+
* so the deparser can distinguish bare RETURN (OUT params) from RETURN <expr>.
19+
*/
20+
export function deriveReturnInfo(sql: string): ReturnInfo | undefined {
21+
let stmts: any[];
22+
try {
23+
stmts = (parseSync(sql) as any).stmts || [];
24+
} catch {
25+
return undefined;
26+
}
27+
for (const s of stmts) {
28+
const fn = s?.stmt?.CreateFunctionStmt;
29+
if (!fn) continue;
30+
if (fn.is_procedure) return { kind: 'void' };
31+
const outParamNames: string[] = (fn.parameters || [])
32+
.map((p: any) => p?.FunctionParameter)
33+
.filter((fp: any) => fp && (fp.mode === 'FUNC_PARAM_OUT' || fp.mode === 'FUNC_PARAM_INOUT' || fp.mode === 'FUNC_PARAM_TABLE'))
34+
.map((fp: any) => fp.name)
35+
.filter((n: any): n is string => typeof n === 'string');
36+
if (outParamNames.length > 0) return { kind: 'out_params', outParamNames };
37+
const names = (fn.returnType?.names || [])
38+
.map((n: any) => n?.String?.sval)
39+
.filter((v: any): v is string => typeof v === 'string');
40+
const typeName = (names[names.length - 1] || '').toLowerCase();
41+
if (fn.returnType?.setof) return { kind: 'setof' };
42+
if (typeName === 'void' || !fn.returnType) return { kind: 'void' };
43+
if (typeName === 'trigger') return { kind: 'trigger' };
44+
return { kind: 'scalar' };
45+
}
46+
return undefined;
47+
}
48+
1649
export function extractFunctionBodies(sql: string): string[] {
1750
const bodies: string[] = [];
1851
const dollarQuoteRegex = /\$\$([^]*?)\$\$/g;
@@ -368,7 +401,7 @@ export class PLpgSQLTestUtils {
368401
throw createParseError('PARSE_FAILED', testName, sql);
369402
}
370403

371-
const deparsedBody = deparseSync(originalAst);
404+
const deparsedBody = deparseSync(originalAst, undefined, deriveReturnInfo(sql));
372405

373406
if (!deparsedBody || deparsedBody.trim().length === 0) {
374407
throw createParseError('DEPARSE_FAILED', testName, sql, deparsedBody);

‎packages/plpgsql-parser/__tests__/return-info.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,23 @@ describe('getReturnInfo', () => {
6464
CREATE FUNCTION test_out(IN x integer, OUT result integer)
6565
LANGUAGE plpgsql AS $$ BEGIN result := x * 2; END; $$
6666
`);
67-
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params' });
67+
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params', outParamNames: ['result'] });
6868
});
6969

7070
it('should return out_params for INOUT parameters', () => {
7171
const stmt = parseCreateFunction(`
7272
CREATE FUNCTION test_inout(INOUT x integer)
7373
LANGUAGE plpgsql AS $$ BEGIN x := x * 2; END; $$
7474
`);
75-
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params' });
75+
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params', outParamNames: ['x'] });
7676
});
7777

7878
it('should return out_params for RETURNS TABLE', () => {
7979
const stmt = parseCreateFunction(`
8080
CREATE FUNCTION test_table() RETURNS TABLE (id integer, name text)
8181
LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 1, 'test'; END; $$
8282
`);
83-
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params' });
83+
expect(getReturnInfo(stmt)).toEqual({ kind: 'out_params', outParamNames: ['id', 'name'] });
8484
});
8585
});
8686

‎packages/plpgsql-parser/src/return-info.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,27 @@ export function getReturnInfo(createFunctionStmt: any): ReturnInfo {
2121

2222
// Check for OUT/INOUT/TABLE parameters - these indicate out_params return type
2323
if (createFunctionStmt.parameters && Array.isArray(createFunctionStmt.parameters)) {
24+
const outParamNames = createFunctionStmt.parameters
25+
.map((param: any) => param?.FunctionParameter)
26+
.filter((fp: any) => {
27+
if (!fp) return false;
28+
const mode = fp.mode;
29+
return mode === 'FUNC_PARAM_OUT' ||
30+
mode === 'FUNC_PARAM_INOUT' ||
31+
mode === 'FUNC_PARAM_TABLE';
32+
})
33+
.map((fp: any) => fp.name)
34+
.filter((name: string | undefined): name is string => typeof name === 'string');
2435
const hasOutParams = createFunctionStmt.parameters.some((param: any) => {
2536
const fp = param?.FunctionParameter;
2637
if (!fp) return false;
2738
const mode = fp.mode;
28-
return mode === 'FUNC_PARAM_OUT' ||
29-
mode === 'FUNC_PARAM_INOUT' ||
39+
return mode === 'FUNC_PARAM_OUT' ||
40+
mode === 'FUNC_PARAM_INOUT' ||
3041
mode === 'FUNC_PARAM_TABLE';
3142
});
3243
if (hasOutParams) {
33-
return { kind: 'out_params' };
44+
return { kind: 'out_params', outParamNames };
3445
}
3546
}
3647

0 commit comments

Comments
 (0)