Skip to content

Commit 741352a

Browse files
authored
Merge pull request #301 from constructive-io/feat/plpgsql-deparser-into-return-fixes
fix(plpgsql-deparser): re-insert INTO after DML RETURNING + skip implicit final RETURN
2 parents 987c0c6 + 8b162ff commit 741352a

8 files changed

Lines changed: 401 additions & 15 deletions

File tree

‎__fixtures__/plpgsql-generated/generated.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@
136136
"plpgsql_deparser_fixes-35.sql": "-- Test 35: CALL statement\nCREATE FUNCTION test_call_statement() RETURNS void\nLANGUAGE plpgsql AS $$\nBEGIN\n CALL my_procedure(1, 'hello');\n RETURN;\nEND$$",
137137
"plpgsql_deparser_fixes-36.sql": "-- =============================================================================\n-- Edge Case Tests: Real-World Patterns\n-- =============================================================================\n\n-- Test 36: Permission bitnum trigger pattern (the function that exposed the END; bug)\nCREATE FUNCTION test_permission_bitnum_trigger() RETURNS trigger\nLANGUAGE plpgsql AS $$\nDECLARE\n bitlen int;\n v_len int;\nBEGIN\n v_len := 32;\n BEGIN\n bitlen := bit_length(NEW.bitstr);\n EXCEPTION\n WHEN others THEN\n bitlen := 0;\n END;\n IF bitlen = 0 THEN\n NEW.bitstr := lpad('', v_len, '0');\n END IF;\n RETURN NEW;\nEND$$",
138138
"plpgsql_deparser_fixes-37.sql": "-- Test 37: Multi-step sign-in pattern (deeply nested IF chains)\nCREATE FUNCTION test_signin_pattern(v_email text) RETURNS record\nLANGUAGE plpgsql AS $$\nDECLARE\n v_user record;\n v_secret record;\nBEGIN\n SELECT * INTO v_user FROM users WHERE email = v_email;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'USER_NOT_FOUND';\n END IF;\n SELECT * INTO v_secret FROM secrets WHERE user_id = v_user.id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'NO_CREDENTIALS';\n END IF;\n IF v_secret.locked_at IS NOT NULL THEN\n RAISE EXCEPTION 'ACCOUNT_LOCKED';\n END IF;\n RETURN v_user;\nEND$$",
139+
"plpgsql_deparser_fixes-38.sql": "-- Test 38: INSERT ... RETURNING ... INTO (INTO must be re-inserted after RETURNING)\nCREATE FUNCTION test_insert_returning_into() RETURNS uuid\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\nBEGIN\n INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO v_id;\n RETURN v_id;\nEND$$",
140+
"plpgsql_deparser_fixes-39.sql": "-- Test 39: UPDATE ... RETURNING ... INTO\nCREATE FUNCTION test_update_returning_into() RETURNS uuid\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\nBEGIN\n UPDATE s.t SET name = 'y' WHERE name = 'x' RETURNING id INTO v_id;\n RETURN v_id;\nEND$$",
141+
"plpgsql_deparser_fixes-40.sql": "-- Test 40: DELETE ... RETURNING ... INTO\nCREATE FUNCTION test_delete_returning_into() RETURNS uuid\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\nBEGIN\n DELETE FROM s.t WHERE name = 'x' RETURNING id INTO v_id;\n RETURN v_id;\nEND$$",
142+
"plpgsql_deparser_fixes-41.sql": "-- Test 41: INSERT ... RETURNING ... INTO STRICT\nCREATE FUNCTION test_insert_returning_into_strict() RETURNS uuid\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\nBEGIN\n INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO STRICT v_id;\n RETURN v_id;\nEND$$",
143+
"plpgsql_deparser_fixes-42.sql": "-- Test 42: INSERT ... RETURNING multiple columns INTO\nCREATE FUNCTION test_insert_returning_multi_into() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n v_id uuid;\n v_name text;\nBEGIN\n INSERT INTO s.t (name) VALUES ('x') RETURNING id, name INTO v_id, v_name;\nEND$$",
144+
"plpgsql_deparser_fixes-43.sql": "-- Test 43: INSERT ... RETURNING expression with subquery INTO (INTO must not land inside the subquery)\nCREATE FUNCTION test_insert_returning_subquery_into() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n v_total bigint;\nBEGIN\n INSERT INTO s.t (name) VALUES ('x') RETURNING (SELECT count(*) FROM s.t WHERE name = 'x') INTO v_total;\nEND$$",
145+
"plpgsql_deparser_fixes-44.sql": "-- Test 44: Trigger function with no final return (implicit compiler RETURN must not be emitted)\nCREATE FUNCTION test_trigger_no_final_return() RETURNS trigger\nLANGUAGE plpgsql AS $$\nBEGIN\n IF TG_OP = 'INSERT' THEN\n RETURN NEW;\n END IF;\nEND$$",
146+
"plpgsql_deparser_fixes-45.sql": "-- Test 45: Void function with explicit trailing RETURN (must be preserved)\nCREATE FUNCTION test_void_explicit_return() RETURNS void\nLANGUAGE plpgsql AS $$\nBEGIN\n RAISE NOTICE 'hi';\n RETURN;\nEND$$",
147+
"plpgsql_deparser_fixes-46.sql": "-- Test 46: Trigger function ending in RETURN NEW (unchanged)\nCREATE FUNCTION test_trigger_return_new() RETURNS trigger\nLANGUAGE plpgsql AS $$\nBEGIN\n NEW.updated_at := now();\n RETURN NEW;\nEND$$",
139148
"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$$",
140149
"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$$",
141150
"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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,87 @@ BEGIN
505505
END IF;
506506
RETURN v_user;
507507
END$$;
508+
509+
-- Test 38: INSERT ... RETURNING ... INTO (INTO must be re-inserted after RETURNING)
510+
CREATE FUNCTION test_insert_returning_into() RETURNS uuid
511+
LANGUAGE plpgsql AS $$
512+
DECLARE
513+
v_id uuid;
514+
BEGIN
515+
INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO v_id;
516+
RETURN v_id;
517+
END$$;
518+
519+
-- Test 39: UPDATE ... RETURNING ... INTO
520+
CREATE FUNCTION test_update_returning_into() RETURNS uuid
521+
LANGUAGE plpgsql AS $$
522+
DECLARE
523+
v_id uuid;
524+
BEGIN
525+
UPDATE s.t SET name = 'y' WHERE name = 'x' RETURNING id INTO v_id;
526+
RETURN v_id;
527+
END$$;
528+
529+
-- Test 40: DELETE ... RETURNING ... INTO
530+
CREATE FUNCTION test_delete_returning_into() RETURNS uuid
531+
LANGUAGE plpgsql AS $$
532+
DECLARE
533+
v_id uuid;
534+
BEGIN
535+
DELETE FROM s.t WHERE name = 'x' RETURNING id INTO v_id;
536+
RETURN v_id;
537+
END$$;
538+
539+
-- Test 41: INSERT ... RETURNING ... INTO STRICT
540+
CREATE FUNCTION test_insert_returning_into_strict() RETURNS uuid
541+
LANGUAGE plpgsql AS $$
542+
DECLARE
543+
v_id uuid;
544+
BEGIN
545+
INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO STRICT v_id;
546+
RETURN v_id;
547+
END$$;
548+
549+
-- Test 42: INSERT ... RETURNING multiple columns INTO
550+
CREATE FUNCTION test_insert_returning_multi_into() RETURNS void
551+
LANGUAGE plpgsql AS $$
552+
DECLARE
553+
v_id uuid;
554+
v_name text;
555+
BEGIN
556+
INSERT INTO s.t (name) VALUES ('x') RETURNING id, name INTO v_id, v_name;
557+
END$$;
558+
559+
-- Test 43: INSERT ... RETURNING expression with subquery INTO (INTO must not land inside the subquery)
560+
CREATE FUNCTION test_insert_returning_subquery_into() RETURNS void
561+
LANGUAGE plpgsql AS $$
562+
DECLARE
563+
v_total bigint;
564+
BEGIN
565+
INSERT INTO s.t (name) VALUES ('x') RETURNING (SELECT count(*) FROM s.t WHERE name = 'x') INTO v_total;
566+
END$$;
567+
568+
-- Test 44: Trigger function with no final return (implicit compiler RETURN must not be emitted)
569+
CREATE FUNCTION test_trigger_no_final_return() RETURNS trigger
570+
LANGUAGE plpgsql AS $$
571+
BEGIN
572+
IF TG_OP = 'INSERT' THEN
573+
RETURN NEW;
574+
END IF;
575+
END$$;
576+
577+
-- Test 45: Void function with explicit trailing RETURN (must be preserved)
578+
CREATE FUNCTION test_void_explicit_return() RETURNS void
579+
LANGUAGE plpgsql AS $$
580+
BEGIN
581+
RAISE NOTICE 'hi';
582+
RETURN;
583+
END$$;
584+
585+
-- Test 46: Trigger function ending in RETURN NEW (unchanged)
586+
CREATE FUNCTION test_trigger_return_new() RETURNS trigger
587+
LANGUAGE plpgsql AS $$
588+
BEGIN
589+
NEW.updated_at := now();
590+
RETURN NEW;
591+
END$$;

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

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

3+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should handle multi-column RETURNING ... INTO 1`] = `
4+
"DECLARE
5+
v_id uuid;
6+
v_name text;
7+
BEGIN
8+
INSERT INTO s.t (name) VALUES ('x') RETURNING id, name INTO v_id, v_name;
9+
END"
10+
`;
11+
12+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should not insert INTO inside a RETURNING subquery 1`] = `
13+
"DECLARE
14+
v_total bigint;
15+
BEGIN
16+
INSERT INTO s.t (name) VALUES ('x') RETURNING (SELECT count(*) FROM s.t WHERE name = 'x') INTO v_total;
17+
END"
18+
`;
19+
20+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should preserve STRICT in RETURNING ... INTO STRICT 1`] = `
21+
"DECLARE
22+
v_id uuid;
23+
BEGIN
24+
INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO STRICT v_id;
25+
RETURN v_id;
26+
END"
27+
`;
28+
29+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should re-insert INTO after RETURNING for DELETE 1`] = `
30+
"DECLARE
31+
v_id uuid;
32+
BEGIN
33+
DELETE FROM s.t WHERE name = 'x' RETURNING id INTO v_id;
34+
RETURN v_id;
35+
END"
36+
`;
37+
38+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should re-insert INTO after RETURNING for INSERT 1`] = `
39+
"DECLARE
40+
v_id uuid;
41+
BEGIN
42+
INSERT INTO s.t (name) VALUES ('x') RETURNING id INTO v_id;
43+
RETURN v_id;
44+
END"
45+
`;
46+
47+
exports[`plpgsql-deparser bug fixes DML RETURNING ... INTO re-insertion should re-insert INTO after RETURNING for UPDATE 1`] = `
48+
"DECLARE
49+
v_id uuid;
50+
BEGIN
51+
UPDATE s.t SET name = 'y' WHERE name = 'x' RETURNING id INTO v_id;
52+
RETURN v_id;
53+
END"
54+
`;
55+
356
exports[`plpgsql-deparser bug fixes INTO clause depth-aware scanner should handle INTO STRICT 1`] = `
457
"DECLARE
558
v_id integer;
@@ -74,7 +127,6 @@ exports[`plpgsql-deparser bug fixes OUT parameters with SELECT INTO multiple var
74127
"BEGIN
75128
SELECT u.name, u.email INTO STRICT name, email FROM users u
76129
WHERE u.id = p_id;
77-
RETURN;
78130
END"
79131
`;
80132

@@ -98,21 +150,18 @@ exports[`plpgsql-deparser bug fixes PERFORM SELECT fix should handle PERFORM wit
98150
"BEGIN
99151
PERFORM set_config('search_path', 'public', true);
100152
PERFORM nextval('my_sequence');
101-
RETURN;
102153
END"
103154
`;
104155

105156
exports[`plpgsql-deparser bug fixes PERFORM SELECT fix should handle PERFORM with subquery 1`] = `
106157
"BEGIN
107158
PERFORM 1 FROM users WHERE id = 1;
108-
RETURN;
109159
END"
110160
`;
111161

112162
exports[`plpgsql-deparser bug fixes PERFORM SELECT fix should strip SELECT keyword from PERFORM statements 1`] = `
113163
"BEGIN
114164
PERFORM pg_sleep(1);
115-
RETURN;
116165
END"
117166
`;
118167

@@ -139,7 +188,6 @@ BEGIN
139188
FOR r IN SELECT id, name FROM users LOOP
140189
RAISE NOTICE 'User: % - %', r.id, r.name;
141190
END LOOP;
142-
RETURN;
143191
END"
144192
`;
145193

@@ -241,7 +289,6 @@ exports[`plpgsql-deparser bug fixes deep nesting and sequential blocks should ha
241289
RAISE NOTICE 'even logging failed';
242290
END;
243291
END;
244-
RETURN;
245292
END"
246293
`;
247294

@@ -269,6 +316,28 @@ exports[`plpgsql-deparser bug fixes deep nesting and sequential blocks should ha
269316
END"
270317
`;
271318

319+
exports[`plpgsql-deparser bug fixes implicit trailing RETURN suppression should leave trigger function ending in RETURN NEW unchanged 1`] = `
320+
"BEGIN
321+
NEW.updated_at := now();
322+
RETURN NEW;
323+
END"
324+
`;
325+
326+
exports[`plpgsql-deparser bug fixes implicit trailing RETURN suppression should not emit implicit compiler-generated RETURN in trigger function 1`] = `
327+
"BEGIN
328+
IF TG_OP = 'INSERT' THEN
329+
RETURN NEW;
330+
END IF;
331+
END"
332+
`;
333+
334+
exports[`plpgsql-deparser bug fixes implicit trailing RETURN suppression should preserve explicit trailing RETURN in void function 1`] = `
335+
"BEGIN
336+
RAISE NOTICE 'hi';
337+
RETURN;
338+
END"
339+
`;
340+
272341
exports[`plpgsql-deparser bug fixes nested block compositions (END; bug class) should handle labeled nested block 1`] = `
273342
"BEGIN
274343
<<inner>>
@@ -423,7 +492,6 @@ END"
423492
exports[`plpgsql-deparser bug fixes untested statement types should handle RETURN QUERY 1`] = `
424493
"BEGIN
425494
RETURN QUERY SELECT id, name FROM my_table WHERE active = TRUE;
426-
RETURN;
427495
END"
428496
`;
429497

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,5 @@ BEGIN
184184
END IF;
185185
RAISE EXCEPTION;
186186
END;
187-
RETURN;
188187
END$$"
189188
`;

‎packages/plpgsql-deparser/__tests__/__snapshots__/schema-rename-mapped.test.ts.snap‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ CREATE FUNCTION myapp_v2.update_user_status(
227227
changed_at
228228
) VALUES
229229
(p_user_id, p_status, now());
230-
RETURN;
231230
END$$;
232231
233232
CREATE FUNCTION myapp_v2.cleanup_old_sessions(
@@ -403,6 +402,5 @@ BEGIN
403402
UPDATE myapp_v2.batch_items SET status = 'processed' WHERE id = item.id;
404403
END LOOP;
405404
UPDATE myapp_v2.batches SET status = 'completed',completed_at = now() WHERE id = p_batch_id;
406-
RETURN;
407405
END$$;"
408406
`;

0 commit comments

Comments
 (0)