Skip to content

Commit 55fe8b7

Browse files
authored
Merge pull request #291 from constructive-io/devin/1775519727-plpgsql-deparser-edge-cases
fix: add 21 plpgsql deparser edge case fixtures and fix 7 schema_rename failures
2 parents 0266000 + aaa25c2 commit 55fe8b7

9 files changed

Lines changed: 1109 additions & 20 deletions

File tree

__fixtures__/plpgsql-generated/generated.json

Lines changed: 38 additions & 5 deletions
Large diffs are not rendered by default.

__fixtures__/plpgsql/plpgsql_deparser_fixes.sql

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,302 @@ BEGIN
206206
FROM users u
207207
WHERE u.id = p_id;
208208
END$$;
209+
210+
-- =============================================================================
211+
-- Edge Case Tests: Nested Block Compositions (END; bug class)
212+
-- These test the exact bug class where END; of a nested block could be
213+
-- confused with statement keywords that follow it.
214+
-- =============================================================================
215+
216+
-- Test 17: Nested block followed by RETURN (the original END; bug pattern)
217+
CREATE FUNCTION test_nested_block_return() RETURNS integer
218+
LANGUAGE plpgsql AS $$
219+
DECLARE
220+
v_result integer;
221+
BEGIN
222+
BEGIN
223+
v_result := 1;
224+
END;
225+
RETURN v_result;
226+
END$$;
227+
228+
-- Test 18: Nested block followed by IF
229+
CREATE FUNCTION test_nested_block_if() RETURNS boolean
230+
LANGUAGE plpgsql AS $$
231+
BEGIN
232+
BEGIN
233+
PERFORM setup_something();
234+
END;
235+
IF FOUND THEN
236+
RETURN TRUE;
237+
END IF;
238+
RETURN FALSE;
239+
END$$;
240+
241+
-- Test 19: Nested block followed by RAISE
242+
CREATE FUNCTION test_nested_block_raise() RETURNS void
243+
LANGUAGE plpgsql AS $$
244+
BEGIN
245+
BEGIN
246+
PERFORM risky_operation();
247+
END;
248+
RAISE NOTICE 'Operation completed';
249+
RETURN;
250+
END$$;
251+
252+
-- Test 20: Nested block followed by PERFORM
253+
CREATE FUNCTION test_nested_block_perform() RETURNS integer
254+
LANGUAGE plpgsql AS $$
255+
DECLARE
256+
v_count integer;
257+
BEGIN
258+
BEGIN
259+
v_count := 42;
260+
END;
261+
PERFORM log_result(v_count);
262+
RETURN v_count;
263+
END$$;
264+
265+
-- Test 21: Nested block followed by assignment
266+
CREATE FUNCTION test_nested_block_assign() RETURNS text
267+
LANGUAGE plpgsql AS $$
268+
DECLARE
269+
v_status text;
270+
BEGIN
271+
BEGIN
272+
PERFORM init();
273+
END;
274+
v_status := 'complete';
275+
RETURN v_status;
276+
END$$;
277+
278+
-- Test 22: Labeled nested block
279+
CREATE FUNCTION test_labeled_nested_block() RETURNS boolean
280+
LANGUAGE plpgsql AS $$
281+
BEGIN
282+
<<inner>>
283+
BEGIN
284+
PERFORM do_work();
285+
END inner;
286+
RETURN TRUE;
287+
END$$;
288+
289+
-- =============================================================================
290+
-- Edge Case Tests: Blocks Inside Control Structures
291+
-- =============================================================================
292+
293+
-- Test 23: Block inside IF THEN branch with exception handler
294+
CREATE FUNCTION test_block_in_if() RETURNS integer
295+
LANGUAGE plpgsql AS $$
296+
BEGIN
297+
IF 1 > 0 THEN
298+
BEGIN
299+
PERFORM positive_handler();
300+
EXCEPTION
301+
WHEN others THEN
302+
RAISE NOTICE 'error in positive handler';
303+
END;
304+
ELSE
305+
RETURN 0;
306+
END IF;
307+
RETURN 1;
308+
END$$;
309+
310+
-- Test 24: Block inside LOOP body with exception handler
311+
CREATE FUNCTION test_block_in_loop() RETURNS void
312+
LANGUAGE plpgsql AS $$
313+
BEGIN
314+
LOOP
315+
BEGIN
316+
PERFORM process_next();
317+
EXCEPTION
318+
WHEN others THEN
319+
RAISE NOTICE 'skipping bad record';
320+
END;
321+
EXIT WHEN NOT FOUND;
322+
END LOOP;
323+
RETURN;
324+
END$$;
325+
326+
-- Test 25: Block inside CASE WHEN
327+
CREATE FUNCTION test_block_in_case(p_status text) RETURNS void
328+
LANGUAGE plpgsql AS $$
329+
BEGIN
330+
CASE p_status
331+
WHEN 'retry' THEN
332+
BEGIN
333+
PERFORM retry_operation();
334+
EXCEPTION
335+
WHEN others THEN
336+
RAISE EXCEPTION 'retry failed';
337+
END;
338+
WHEN 'skip' THEN
339+
RAISE NOTICE 'skipping';
340+
END CASE;
341+
RETURN;
342+
END$$;
343+
344+
-- =============================================================================
345+
-- Edge Case Tests: Deep Nesting & Sequential Blocks
346+
-- =============================================================================
347+
348+
-- Test 26: Two sequential nested blocks
349+
CREATE FUNCTION test_sequential_blocks() RETURNS void
350+
LANGUAGE plpgsql AS $$
351+
BEGIN
352+
BEGIN
353+
PERFORM step_one();
354+
END;
355+
BEGIN
356+
PERFORM step_two();
357+
END;
358+
RETURN;
359+
END$$;
360+
361+
-- Test 27: Triple-nested blocks
362+
CREATE FUNCTION test_triple_nested() RETURNS void
363+
LANGUAGE plpgsql AS $$
364+
BEGIN
365+
BEGIN
366+
BEGIN
367+
PERFORM deep_call();
368+
END;
369+
RAISE NOTICE 'middle';
370+
END;
371+
RETURN;
372+
END$$;
373+
374+
-- Test 28: Block inside exception handler action
375+
CREATE FUNCTION test_block_in_exception() RETURNS void
376+
LANGUAGE plpgsql AS $$
377+
BEGIN
378+
PERFORM risky();
379+
EXCEPTION
380+
WHEN others THEN
381+
BEGIN
382+
PERFORM log_error();
383+
EXCEPTION
384+
WHEN others THEN
385+
RAISE NOTICE 'even logging failed';
386+
END;
387+
END$$;
388+
389+
-- =============================================================================
390+
-- Edge Case Tests: Untested Statement Types
391+
-- =============================================================================
392+
393+
-- Test 29: FOR integer loop
394+
CREATE FUNCTION test_for_integer_loop() RETURNS void
395+
LANGUAGE plpgsql AS $$
396+
BEGIN
397+
FOR i IN 1..10 LOOP
398+
PERFORM process(i);
399+
END LOOP;
400+
RETURN;
401+
END$$;
402+
403+
-- Test 30: FOR query loop
404+
CREATE FUNCTION test_for_query_loop() RETURNS void
405+
LANGUAGE plpgsql AS $$
406+
DECLARE
407+
rec record;
408+
BEGIN
409+
FOR rec IN SELECT id, name FROM my_table LOOP
410+
PERFORM handle(rec);
411+
END LOOP;
412+
RETURN;
413+
END$$;
414+
415+
-- Test 31: Labeled FOR loop with EXIT
416+
CREATE FUNCTION test_labeled_for_loop() RETURNS void
417+
LANGUAGE plpgsql AS $$
418+
DECLARE
419+
rec record;
420+
BEGIN
421+
<<row_loop>>
422+
FOR rec IN SELECT id, name FROM items LOOP
423+
EXIT row_loop WHEN rec.id IS NULL;
424+
PERFORM process(rec);
425+
END LOOP row_loop;
426+
RETURN;
427+
END$$;
428+
429+
-- Test 32: RETURN NEXT in set-returning function with OUT parameters
430+
CREATE FUNCTION test_return_next_out(OUT x integer, OUT y text) RETURNS SETOF record
431+
LANGUAGE plpgsql AS $$
432+
BEGIN
433+
FOR i IN 1..5 LOOP
434+
x := i;
435+
y := 'item_' || i::text;
436+
RETURN NEXT;
437+
END LOOP;
438+
RETURN;
439+
END$$;
440+
441+
-- Test 33: RETURN QUERY
442+
CREATE FUNCTION test_return_query_simple() RETURNS SETOF record
443+
LANGUAGE plpgsql AS $$
444+
BEGIN
445+
RETURN QUERY SELECT id, name FROM my_table WHERE active = TRUE;
446+
END$$;
447+
448+
-- Test 34: ASSERT statement
449+
CREATE FUNCTION test_assert(p_x integer) RETURNS integer
450+
LANGUAGE plpgsql AS $$
451+
BEGIN
452+
ASSERT p_x > 0, 'x must be positive';
453+
RETURN p_x;
454+
END$$;
455+
456+
-- Test 35: CALL statement
457+
CREATE FUNCTION test_call_statement() RETURNS void
458+
LANGUAGE plpgsql AS $$
459+
BEGIN
460+
CALL my_procedure(1, 'hello');
461+
RETURN;
462+
END$$;
463+
464+
-- =============================================================================
465+
-- Edge Case Tests: Real-World Patterns
466+
-- =============================================================================
467+
468+
-- Test 36: Permission bitnum trigger pattern (the function that exposed the END; bug)
469+
CREATE FUNCTION test_permission_bitnum_trigger() RETURNS trigger
470+
LANGUAGE plpgsql AS $$
471+
DECLARE
472+
bitlen int;
473+
v_len int;
474+
BEGIN
475+
v_len := 32;
476+
BEGIN
477+
bitlen := bit_length(NEW.bitstr);
478+
EXCEPTION
479+
WHEN others THEN
480+
bitlen := 0;
481+
END;
482+
IF bitlen = 0 THEN
483+
NEW.bitstr := lpad('', v_len, '0');
484+
END IF;
485+
RETURN NEW;
486+
END$$;
487+
488+
-- Test 37: Multi-step sign-in pattern (deeply nested IF chains)
489+
CREATE FUNCTION test_signin_pattern(v_email text) RETURNS record
490+
LANGUAGE plpgsql AS $$
491+
DECLARE
492+
v_user record;
493+
v_secret record;
494+
BEGIN
495+
SELECT * INTO v_user FROM users WHERE email = v_email;
496+
IF NOT FOUND THEN
497+
RAISE EXCEPTION 'USER_NOT_FOUND';
498+
END IF;
499+
SELECT * INTO v_secret FROM secrets WHERE user_id = v_user.id;
500+
IF NOT FOUND THEN
501+
RAISE EXCEPTION 'NO_CREDENTIALS';
502+
END IF;
503+
IF v_secret.locked_at IS NOT NULL THEN
504+
RAISE EXCEPTION 'ACCOUNT_LOCKED';
505+
END IF;
506+
RETURN v_user;
507+
END$$;

0 commit comments

Comments
 (0)