Skip to content

Commit af1a5b4

Browse files
committed
feat(plpgsql-parser): automatically compute return info for correct RETURN handling
The deparser now automatically extracts return type information from the CreateFunctionStmt and passes it to the PL/pgSQL deparser. This ensures correct RETURN statement handling without users needing to manually provide return context: - SETOF functions: bare RETURN stays as RETURN - Scalar functions: empty RETURN becomes RETURN NULL - Void functions: bare RETURN stays as RETURN - Trigger functions: handled correctly - OUT parameter functions: bare RETURN stays as RETURN Added 5 tests to verify automatic return info handling works correctly.
1 parent a5944ab commit af1a5b4

File tree

2 files changed

+113
-5
lines changed

2 files changed

+113
-5
lines changed

packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,102 @@ describe('plpgsql-parser', () => {
111111
expect(result).toContain('\n');
112112
});
113113
});
114+
115+
describe('automatic return info handling', () => {
116+
it('should preserve bare RETURN for SETOF functions', () => {
117+
const setofSql = `
118+
CREATE FUNCTION get_users()
119+
RETURNS SETOF users
120+
LANGUAGE plpgsql AS $$
121+
BEGIN
122+
RETURN QUERY SELECT * FROM users;
123+
RETURN;
124+
END;
125+
$$;
126+
`;
127+
128+
const parsed = parse(setofSql);
129+
const result = deparseSync(parsed);
130+
131+
// SETOF functions should keep bare RETURN (not RETURN NULL)
132+
expect(result).toMatch(/RETURN\s*;/);
133+
expect(result).not.toMatch(/RETURN\s+NULL\s*;/);
134+
});
135+
136+
it('should emit RETURN NULL for scalar functions with empty return', () => {
137+
const scalarSql = `
138+
CREATE FUNCTION get_value()
139+
RETURNS int
140+
LANGUAGE plpgsql AS $$
141+
BEGIN
142+
RETURN;
143+
END;
144+
$$;
145+
`;
146+
147+
const parsed = parse(scalarSql);
148+
const result = deparseSync(parsed);
149+
150+
// Scalar functions with empty RETURN should become RETURN NULL
151+
expect(result).toMatch(/RETURN\s+NULL\s*;/);
152+
});
153+
154+
it('should preserve bare RETURN for void functions', () => {
155+
const voidSql = `
156+
CREATE FUNCTION do_something()
157+
RETURNS void
158+
LANGUAGE plpgsql AS $$
159+
BEGIN
160+
RAISE NOTICE 'done';
161+
RETURN;
162+
END;
163+
$$;
164+
`;
165+
166+
const parsed = parse(voidSql);
167+
const result = deparseSync(parsed);
168+
169+
// Void functions should keep bare RETURN
170+
expect(result).toMatch(/RETURN\s*;/);
171+
expect(result).not.toMatch(/RETURN\s+NULL\s*;/);
172+
});
173+
174+
it('should preserve bare RETURN for trigger functions', () => {
175+
const triggerSql = `
176+
CREATE FUNCTION my_trigger()
177+
RETURNS trigger
178+
LANGUAGE plpgsql AS $$
179+
BEGIN
180+
RETURN NEW;
181+
END;
182+
$$;
183+
`;
184+
185+
const parsed = parse(triggerSql);
186+
const result = deparseSync(parsed);
187+
188+
// Trigger functions should work correctly (case-insensitive check)
189+
expect(result.toLowerCase()).toContain('return new');
190+
});
191+
192+
it('should preserve bare RETURN for OUT parameter functions', () => {
193+
const outParamSql = `
194+
CREATE FUNCTION get_info(OUT result text)
195+
RETURNS text
196+
LANGUAGE plpgsql AS $$
197+
BEGIN
198+
result := 'hello';
199+
RETURN;
200+
END;
201+
$$;
202+
`;
203+
204+
const parsed = parse(outParamSql);
205+
const result = deparseSync(parsed);
206+
207+
// OUT parameter functions should keep bare RETURN
208+
expect(result).toMatch(/RETURN\s*;/);
209+
expect(result).not.toMatch(/RETURN\s+NULL\s*;/);
210+
});
211+
});
114212
});

packages/plpgsql-parser/src/deparse.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { deparse as deparseSql } from 'pgsql-deparser';
22
import {
33
dehydratePlpgsqlAst,
4-
deparseSync as deparsePlpgsql
4+
deparseSync as deparsePlpgsql,
5+
deparseFunctionSync as deparsePlpgsqlFunction
56
} from 'plpgsql-deparser';
67
import type {
78
ParsedScript,
89
TransformContext,
910
DeparseOptions,
1011
ParsedFunction
1112
} from './types';
13+
import { getReturnInfoFromParsedFunction } from './return-info';
1214

1315
function stitchBodyIntoSqlAst(
1416
sqlAst: any,
@@ -48,8 +50,12 @@ export async function deparse(
4850

4951
for (const fn of functions) {
5052
const dehydrated = dehydratePlpgsqlAst(fn.plpgsql.hydrated);
51-
const newBody = deparsePlpgsql(dehydrated);
52-
stitchBodyIntoSqlAst(sqlAst, fn, newBody);
53+
const returnInfo = getReturnInfoFromParsedFunction(fn);
54+
const plpgsqlFunc = dehydrated.plpgsql_funcs?.[0]?.PLpgSQL_function;
55+
if (plpgsqlFunc) {
56+
const newBody = deparsePlpgsqlFunction(plpgsqlFunc, undefined, returnInfo);
57+
stitchBodyIntoSqlAst(sqlAst, fn, newBody);
58+
}
5359
}
5460

5561
if (sqlAst.stmts && sqlAst.stmts.length > 0) {
@@ -77,8 +83,12 @@ export function deparseSync(
7783

7884
for (const fn of functions) {
7985
const dehydrated = dehydratePlpgsqlAst(fn.plpgsql.hydrated);
80-
const newBody = deparsePlpgsql(dehydrated);
81-
stitchBodyIntoSqlAst(sqlAst, fn, newBody);
86+
const returnInfo = getReturnInfoFromParsedFunction(fn);
87+
const plpgsqlFunc = dehydrated.plpgsql_funcs?.[0]?.PLpgSQL_function;
88+
if (plpgsqlFunc) {
89+
const newBody = deparsePlpgsqlFunction(plpgsqlFunc, undefined, returnInfo);
90+
stitchBodyIntoSqlAst(sqlAst, fn, newBody);
91+
}
8292
}
8393

8494
if (sqlAst.stmts && sqlAst.stmts.length > 0) {

0 commit comments

Comments
 (0)