Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions __fixtures__/plpgsql-generated/generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@
"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$$",
"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$$",
"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$$",
"plpgsql_deparser_fixes-47.sql": "-- Test 47: Bound cursor declared with SCROLL (option must stay on the declaration, not OPEN)\nCREATE FUNCTION test_scroll_cursor_decl() RETURNS int\nLANGUAGE plpgsql AS $$\nDECLARE\n c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;\n v int;\nBEGIN\n OPEN c;\n FETCH PRIOR FROM c INTO v;\n CLOSE c;\n RETURN v;\nEND$$",
"plpgsql_deparser_fixes-48.sql": "-- Test 48: Bound cursor declared with NO SCROLL\nCREATE FUNCTION test_no_scroll_cursor_decl() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n c NO SCROLL CURSOR FOR SELECT id FROM s.t;\n v int;\nBEGIN\n OPEN c;\n FETCH c INTO v;\n CLOSE c;\nEND$$",
"plpgsql_deparser_fixes-49.sql": "-- Test 49: Plain bound cursor (OPEN must not gain a SCROLL keyword)\nCREATE FUNCTION test_plain_cursor_open() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n c CURSOR FOR SELECT id FROM s.t;\n v int;\nBEGIN\n OPEN c;\n FETCH c INTO v;\n CLOSE c;\nEND$$",
"plpgsql_deparser_fixes-50.sql": "-- Test 50: MOVE with count/expression directions (counts must be preserved)\nCREATE FUNCTION test_move_directions() RETURNS void\nLANGUAGE plpgsql AS $$\nDECLARE\n c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;\nBEGIN\n OPEN c;\n MOVE FORWARD 3 FROM c;\n MOVE BACKWARD 2 FROM c;\n MOVE FORWARD ALL FROM c;\n MOVE BACKWARD ALL FROM c;\n MOVE LAST IN c;\n CLOSE c;\nEND$$",
"plpgsql_deparser_fixes-51.sql": "-- Test 51: Exception handler with SQLSTATE condition (must emit SQLSTATE 'xxxxx')\nCREATE FUNCTION test_sqlstate_condition() RETURNS int\nLANGUAGE plpgsql AS $$\nBEGIN\n RETURN 1;\nEXCEPTION\n WHEN unique_violation OR SQLSTATE '23503' THEN\n RETURN -1;\n WHEN SQLSTATE 'P0001' THEN\n RETURN -2;\nEND$$",
"plpgsql_deparser_fixes-52.sql": "-- Test 52: Bare RAISE re-throw inside an exception handler (must stay bare)\nCREATE FUNCTION test_bare_raise_rethrow() RETURNS void\nLANGUAGE plpgsql AS $$\nBEGIN\n PERFORM 1;\nEXCEPTION\n WHEN OTHERS THEN\n RAISE;\nEND$$",
"plpgsql_deparser_fixes-53.sql": "-- Test 53: Array element and slice assignment (target must not be parenthesized)\nCREATE FUNCTION test_array_element_assignment() RETURNS int[]\nLANGUAGE plpgsql AS $$\nDECLARE\n a int[] := ARRAY[1, 2, 3, 4, 5];\n m int[][] := ARRAY[ARRAY[1, 2], ARRAY[3, 4]];\nBEGIN\n a[2] := 20;\n a[2:3] := ARRAY[9, 9];\n m[1][2] := 42;\n RETURN a;\nEND$$",
"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$$",
"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$$",
"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$$",
Expand Down
87 changes: 87 additions & 0 deletions __fixtures__/plpgsql/plpgsql_deparser_fixes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -589,3 +589,90 @@ BEGIN
NEW.updated_at := now();
RETURN NEW;
END$$;

-- Test 47: Bound cursor declared with SCROLL (option must stay on the declaration, not OPEN)
CREATE FUNCTION test_scroll_cursor_decl() RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
v int;
BEGIN
OPEN c;
FETCH PRIOR FROM c INTO v;
CLOSE c;
RETURN v;
END$$;

-- Test 48: Bound cursor declared with NO SCROLL
CREATE FUNCTION test_no_scroll_cursor_decl() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
c NO SCROLL CURSOR FOR SELECT id FROM s.t;
v int;
BEGIN
OPEN c;
FETCH c INTO v;
CLOSE c;
END$$;

-- Test 49: Plain bound cursor (OPEN must not gain a SCROLL keyword)
CREATE FUNCTION test_plain_cursor_open() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
c CURSOR FOR SELECT id FROM s.t;
v int;
BEGIN
OPEN c;
FETCH c INTO v;
CLOSE c;
END$$;

-- Test 50: MOVE with count/expression directions (counts must be preserved)
CREATE FUNCTION test_move_directions() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
BEGIN
OPEN c;
MOVE FORWARD 3 FROM c;
MOVE BACKWARD 2 FROM c;
MOVE FORWARD ALL FROM c;
MOVE BACKWARD ALL FROM c;
MOVE LAST IN c;
CLOSE c;
END$$;

-- Test 51: Exception handler with SQLSTATE condition (must emit SQLSTATE 'xxxxx')
CREATE FUNCTION test_sqlstate_condition() RETURNS int
LANGUAGE plpgsql AS $$
BEGIN
RETURN 1;
EXCEPTION
WHEN unique_violation OR SQLSTATE '23503' THEN
RETURN -1;
WHEN SQLSTATE 'P0001' THEN
RETURN -2;
END$$;

-- Test 52: Bare RAISE re-throw inside an exception handler (must stay bare)
CREATE FUNCTION test_bare_raise_rethrow() RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
PERFORM 1;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END$$;

-- Test 53: Array element and slice assignment (target must not be parenthesized)
CREATE FUNCTION test_array_element_assignment() RETURNS int[]
LANGUAGE plpgsql AS $$
DECLARE
a int[] := ARRAY[1, 2, 3, 4, 5];
m int[][] := ARRAY[ARRAY[1, 2], ARRAY[3, 4]];
BEGIN
a[2] := 20;
a[2:3] := ARRAY[9, 9];
m[1][2] := 42;
RETURN a;
END$$;
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ BEGIN
END"
`;

exports[`plpgsql-deparser bug fixes FETCH/MOVE directions should preserve counts and FIRST/LAST directions 1`] = `
"DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
BEGIN
OPEN c;
MOVE FORWARD 3 FROM c;
MOVE BACKWARD 2 FROM c;
MOVE ALL FROM c;
MOVE BACKWARD ALL FROM c;
MOVE LAST FROM c;
MOVE FIRST FROM c;
CLOSE c;
END"
`;

exports[`plpgsql-deparser bug fixes INTO clause depth-aware scanner should handle INTO STRICT 1`] = `
"DECLARE
v_id integer;
Expand Down Expand Up @@ -209,6 +224,37 @@ exports[`plpgsql-deparser bug fixes Record field qualification (recfield) should
END"
`;

exports[`plpgsql-deparser bug fixes SQLSTATE exception conditions should emit SQLSTATE codes with the SQLSTATE keyword 1`] = `
"BEGIN
BEGIN
RETURN 1;
EXCEPTION
WHEN unique_violation OR SQLSTATE '23503' THEN
RETURN -1;
WHEN SQLSTATE 'P0001' THEN
RETURN -2;
END;
END"
`;

exports[`plpgsql-deparser bug fixes bare RAISE re-throw should keep a bare RAISE bare (not RAISE EXCEPTION;) 1`] = `
"BEGIN
BEGIN
PERFORM 1;
EXCEPTION
WHEN others THEN
RAISE;
END;
END"
`;

exports[`plpgsql-deparser bug fixes bare RAISE re-throw should still emit level and message for normal RAISE 1`] = `
"BEGIN
RAISE EXCEPTION 'boom %', 42 USING HINT = 'h';
RAISE NOTICE 'hi';
END"
`;

exports[`plpgsql-deparser bug fixes blocks inside control structures should handle block inside CASE WHEN 1`] = `
"BEGIN
CASE p_status
Expand Down Expand Up @@ -276,6 +322,29 @@ BEGIN
END"
`;

exports[`plpgsql-deparser bug fixes cursor SCROLL options should keep SCROLL on the cursor declaration, not OPEN 1`] = `
"DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
v int;
BEGIN
OPEN c;
FETCH PRIOR FROM c INTO v;
CLOSE c;
RETURN v;
END"
`;

exports[`plpgsql-deparser bug fixes cursor SCROLL options should not add SCROLL to OPEN of a plain bound cursor 1`] = `
"DECLARE
c CURSOR FOR SELECT id FROM s.t;
v int;
BEGIN
OPEN c;
FETCH FROM c INTO v;
CLOSE c;
END"
`;

exports[`plpgsql-deparser bug fixes deep nesting and sequential blocks should handle block inside exception handler 1`] = `
"BEGIN
BEGIN
Expand Down Expand Up @@ -444,6 +513,18 @@ BEGIN
END"
`;

exports[`plpgsql-deparser bug fixes subscripted assignment targets should not parenthesize array element assignment targets 1`] = `
"DECLARE
a int[] := ARRAY[1, 2, 3, 4, 5];
m int[][] := ARRAY[ARRAY[1, 2], ARRAY[3, 4]];
BEGIN
a[2] := 20;
a[2:3] := ARRAY[9, 9];
m[1][2] := 42;
RETURN a;
END"
`;

exports[`plpgsql-deparser bug fixes untested statement types should handle ASSERT statement 1`] = `
"BEGIN
ASSERT p_x > 0, 'x must be positive';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@ BEGIN
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'unique_violation: %', sqlerrm;
RAISE EXCEPTION;
RAISE;
WHEN others THEN
IF p_debug THEN
RAISE NOTICE 'error: % (%:%)', sqlerrm, sqlstate, sqlerrm;
END IF;
RAISE EXCEPTION;
RAISE;
END;
END$$"
`;
164 changes: 164 additions & 0 deletions packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,4 +1050,168 @@ END$$`;
expect(deparsed).not.toMatch(/RETURN;/);
});
});

describe('cursor SCROLL options', () => {
it('should keep SCROLL on the cursor declaration, not OPEN', async () => {
const sql = `CREATE FUNCTION test_scroll_cursor() RETURNS integer
LANGUAGE plpgsql AS $$
DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
v int;
BEGIN
OPEN c;
FETCH PRIOR FROM c INTO v;
CLOSE c;
RETURN v;
END$$`;

await testUtils.expectAstMatch('scroll cursor declaration', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('SCROLL CURSOR FOR');
expect(deparsed).not.toMatch(/OPEN\s+(NO\s+)?SCROLL/i);
expect(deparsed).not.toMatch(/OPEN\s+c\s+(NO\s+)?SCROLL/i);
});

it('should not add SCROLL to OPEN of a plain bound cursor', async () => {
const sql = `CREATE FUNCTION test_plain_cursor() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
c CURSOR FOR SELECT id FROM s.t;
v int;
BEGIN
OPEN c;
FETCH c INTO v;
CLOSE c;
END$$`;

await testUtils.expectAstMatch('plain cursor open', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('OPEN c');
expect(deparsed).not.toMatch(/SCROLL/i);
});
});

describe('FETCH/MOVE directions', () => {
it('should preserve counts and FIRST/LAST directions', async () => {
const sql = `CREATE FUNCTION test_move_directions() RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
c SCROLL CURSOR FOR SELECT id FROM s.t ORDER BY id;
BEGIN
OPEN c;
MOVE FORWARD 3 FROM c;
MOVE BACKWARD 2 FROM c;
MOVE FORWARD ALL FROM c;
MOVE BACKWARD ALL FROM c;
MOVE LAST IN c;
MOVE FIRST IN c;
CLOSE c;
END$$`;

await testUtils.expectAstMatch('move directions', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('FORWARD 3');
expect(deparsed).toContain('BACKWARD 2');
expect(deparsed).toContain('BACKWARD ALL');
expect(deparsed).toContain('LAST');
expect(deparsed).toContain('FIRST');
});
});

describe('SQLSTATE exception conditions', () => {
it('should emit SQLSTATE codes with the SQLSTATE keyword', async () => {
const sql = `CREATE FUNCTION test_sqlstate_cond() RETURNS integer
LANGUAGE plpgsql AS $$
BEGIN
RETURN 1;
EXCEPTION
WHEN unique_violation OR SQLSTATE '23503' THEN
RETURN -1;
WHEN SQLSTATE 'P0001' THEN
RETURN -2;
END$$`;

await testUtils.expectAstMatch('sqlstate condition', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain(`SQLSTATE '23503'`);
expect(deparsed).toContain(`SQLSTATE 'P0001'`);
expect(deparsed).not.toMatch(/WHEN\s+23503/);
});
});

describe('bare RAISE re-throw', () => {
it('should keep a bare RAISE bare (not RAISE EXCEPTION;)', async () => {
const sql = `CREATE FUNCTION test_bare_raise() RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
PERFORM 1;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END$$`;

await testUtils.expectAstMatch('bare raise', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toMatch(/RAISE;/);
expect(deparsed).not.toMatch(/RAISE EXCEPTION;/);
});

it('should still emit level and message for normal RAISE', async () => {
const sql = `CREATE FUNCTION test_raise_msg() RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'boom %', 42 USING HINT = 'h';
RAISE NOTICE 'hi';
END$$`;

await testUtils.expectAstMatch('raise with message', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain(`RAISE EXCEPTION 'boom %', 42`);
expect(deparsed).toContain(`RAISE NOTICE 'hi'`);
});
});

describe('subscripted assignment targets', () => {
it('should not parenthesize array element assignment targets', async () => {
const sql = `CREATE FUNCTION test_array_assign() RETURNS int[]
LANGUAGE plpgsql AS $$
DECLARE
a int[] := ARRAY[1, 2, 3, 4, 5];
m int[][] := ARRAY[ARRAY[1, 2], ARRAY[3, 4]];
BEGIN
a[2] := 20;
a[2:3] := ARRAY[9, 9];
m[1][2] := 42;
RETURN a;
END$$`;

await testUtils.expectAstMatch('array element assignment', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toContain('a[2] := 20');
expect(deparsed).toContain('m[1][2] := 42');
expect(deparsed).not.toMatch(/\(a\)\[/);
expect(deparsed).not.toMatch(/\(m\)\[/);
});
});
});
Loading
Loading