Skip to content

Commit a1f8a49

Browse files
authored
Merge pull request #273 from constructive-io/devin/1767756786-auto-return-info
feat(plpgsql-parser): automatically compute return info for correct RETURN handling
2 parents a5944ab + 70e018a commit a1f8a49

5 files changed

Lines changed: 440 additions & 14 deletions

File tree

packages/plpgsql-deparser/README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ PL/pgSQL AST Deparser - Converts PL/pgSQL function ASTs back to SQL strings.
1616

1717
> **⚠️ Experimental:** This package is currently experimental. If you're looking for SQL deparsing (not PL/pgSQL), see [`pgsql-deparser`](https://www.npmjs.com/package/pgsql-deparser).
1818
19+
> **For full SQL + PL/pgSQL deparsing:** If you need to deparse complete `CREATE FUNCTION` statements (not just function bodies), use [`plpgsql-parser`](https://www.npmjs.com/package/plpgsql-parser) instead. It handles the full heterogeneous parsing/deparsing pipeline automatically.
20+
1921
## Overview
2022

21-
This package provides a deparser for PL/pgSQL (PostgreSQL's procedural language) AST structures. It works with the AST output from `parsePlPgSQL` function in `@libpg-query/parser`.
23+
This package provides a **body-only** deparser for PL/pgSQL (PostgreSQL's procedural language) AST structures. It converts PL/pgSQL function bodies (the `BEGIN...END` part) back to strings. It works with the AST output from `parsePlPgSQL` function in `@libpg-query/parser`.
2224

2325
The PL/pgSQL AST is different from the regular SQL AST - it represents the internal structure of PL/pgSQL function bodies, including:
2426

@@ -177,13 +179,32 @@ interface PLpgSQLDeparserOptions {
177179

178180
## Note on AST Structure
179181

180-
The PL/pgSQL AST returned by `parsePlPgSQL` represents the internal structure of function bodies, not the `CREATE FUNCTION` statement itself. To get a complete function definition, you would need to:
182+
This package deparses **only the function body** (the `BEGIN...END` part), not the full `CREATE FUNCTION` statement.
183+
184+
For full SQL + PL/pgSQL deparsing, use [`plpgsql-parser`](https://www.npmjs.com/package/plpgsql-parser):
185+
186+
```typescript
187+
import { parse, deparseSync, loadModule } from 'plpgsql-parser';
188+
189+
await loadModule();
190+
191+
const parsed = parse(`
192+
CREATE FUNCTION my_func() RETURNS void LANGUAGE plpgsql AS $$
193+
BEGIN
194+
RAISE NOTICE 'Hello';
195+
END;
196+
$$;
197+
`);
198+
199+
// Full round-trip: parses SQL + PL/pgSQL, deparses back to complete SQL
200+
const sql = deparseSync(parsed);
201+
```
181202

182-
1. Parse the `CREATE FUNCTION` statement with the regular `parse()` function
183-
2. Extract the function body
184-
3. Parse the body with `parsePlPgSQL()`
185-
4. Deparse the body with this package
186-
5. Combine with the outer `CREATE FUNCTION` statement
203+
The `plpgsql-parser` package handles:
204+
- Parsing the outer `CREATE FUNCTION` statement
205+
- Hydrating embedded SQL expressions in the PL/pgSQL body
206+
- Correct `RETURN` statement handling based on function return type
207+
- Stitching the deparsed body back into the full SQL
187208

188209
## License
189210

packages/plpgsql-parser/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@
1414

1515
Combined SQL + PL/pgSQL parser with hydrated ASTs and transform API.
1616

17-
> **⚠️ Experimental:** This package is currently experimental. If you're looking for just SQL parsing, see [`pgsql-parser`](https://www.npmjs.com/package/pgsql-parser). For just PL/pgSQL deparsing, see [`plpgsql-deparser`](https://www.npmjs.com/package/plpgsql-deparser).
17+
> **⚠️ Experimental:** This package is currently experimental. If you're looking for just SQL parsing, see [`pgsql-parser`](https://www.npmjs.com/package/pgsql-parser). For body-only PL/pgSQL deparsing, see [`plpgsql-deparser`](https://www.npmjs.com/package/plpgsql-deparser).
1818
1919
## Overview
2020

21-
This package provides a unified API for parsing SQL scripts containing PL/pgSQL functions. It combines the SQL parser and PL/pgSQL parser, automatically detecting and hydrating PL/pgSQL function bodies.
21+
This package provides a unified API for **heterogeneous parsing and deparsing** of SQL scripts containing PL/pgSQL functions. It handles the full pipeline: parsing SQL + PL/pgSQL together, transforming ASTs, and deparsing back to complete SQL.
22+
23+
**Use this package when you need to:**
24+
- Parse and deparse complete `CREATE FUNCTION` statements with PL/pgSQL bodies
25+
- Transform both SQL and embedded PL/pgSQL expressions (e.g., rename schemas)
26+
- Round-trip SQL through parse → modify → deparse
2227

2328
Key features:
2429

2530
- Auto-detects `CREATE FUNCTION` statements with `LANGUAGE plpgsql`
2631
- Hydrates PL/pgSQL function bodies into structured ASTs
32+
- Automatic `RETURN` statement handling based on function return type
2733
- Transform API for parse → modify → deparse workflows
2834
- Re-exports underlying primitives for power users
2935

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
});

0 commit comments

Comments
 (0)