From 6416efa933c222fcb8f1999781eaf4dd667f8dc5 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 9 Jan 2026 01:21:32 +0000 Subject: [PATCH 1/2] test: add SELECT INTO statement parsing tests Add tests to verify that PL/pgSQL functions containing SELECT INTO statements are correctly parsed and hydrated. These tests serve as regression tests to ensure SELECT INTO parsing continues to work. Tests added: - should parse function with SELECT INTO statement - should parse function with SELECT INTO and schema-qualified table - should deparse function with SELECT INTO correctly - should parse function with DELETE statement (contrast test) --- .../__tests__/plpgsql-parser.test.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts index 1d738444..6776a60f 100644 --- a/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts +++ b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts @@ -209,4 +209,105 @@ describe('plpgsql-parser', () => { expect(result).not.toMatch(/RETURN\s+NULL\s*;/); }); }); + + describe('SELECT INTO statement parsing', () => { + // This test documents a bug in @libpg-query/parser where PL/pgSQL functions + // containing SELECT INTO statements fail to parse, causing the function + // to not be recognized as a PL/pgSQL function and preventing hydration. + // + // Bug: parsePlPgSQLSync throws "Unexpected non-whitespace character after JSON" + // when the function body contains SELECT INTO statements. + // + // This causes inconsistent behavior: + // - Functions with DELETE/INSERT/UPDATE: parse successfully, get hydrated + // - Functions with SELECT INTO: fail to parse, not recognized as PL/pgSQL + // + // Related issue: https://github.com/pganalyze/libpg_query/issues/XXX + + it('should parse function with SELECT INTO statement', () => { + const selectIntoSql = ` + CREATE FUNCTION get_data() + RETURNS void + LANGUAGE plpgsql AS $$ + DECLARE + v_result text; + BEGIN + SELECT * INTO v_result FROM some_table WHERE id = 1; + END; + $$; + `; + + const parsed = parse(selectIntoSql); + + // This currently fails because @libpg-query/parser cannot parse + // PL/pgSQL functions with SELECT INTO statements + expect(parsed.functions).toHaveLength(1); + expect(parsed.functions[0].kind).toBe('plpgsql-function'); + expect(parsed.functions[0].plpgsql.hydrated).toBeDefined(); + }); + + it('should parse function with SELECT INTO and schema-qualified table', () => { + const selectIntoSchemaSql = ` + CREATE FUNCTION "my_schema".get_data() + RETURNS void + LANGUAGE plpgsql AS $$ + DECLARE + v_result text; + BEGIN + SELECT * INTO v_result FROM "my_schema".some_table WHERE id = 1; + END; + $$; + `; + + const parsed = parse(selectIntoSchemaSql); + + // This currently fails because @libpg-query/parser cannot parse + // PL/pgSQL functions with SELECT INTO statements + expect(parsed.functions).toHaveLength(1); + expect(parsed.functions[0].kind).toBe('plpgsql-function'); + }); + + it('should deparse function with SELECT INTO correctly', () => { + const selectIntoSql = ` + CREATE FUNCTION get_data() + RETURNS void + LANGUAGE plpgsql AS $$ + DECLARE + v_result text; + BEGIN + SELECT * INTO v_result FROM "quoted_schema".some_table WHERE id = 1; + END; + $$; + `; + + const parsed = parse(selectIntoSql); + const result = deparseSync(parsed); + + // When this works, the deparsed SQL should have consistent quoting + // (either all quoted or all unquoted based on QuoteUtils rules) + expect(result).toContain('SELECT'); + expect(result).toContain('INTO'); + expect(result).toContain('v_result'); + }); + + // Contrast: DELETE statements parse correctly + it('should parse function with DELETE statement (works correctly)', () => { + const deleteSql = ` + CREATE FUNCTION delete_data() + RETURNS void + LANGUAGE plpgsql AS $$ + BEGIN + DELETE FROM some_table WHERE id = 1; + END; + $$; + `; + + const parsed = parse(deleteSql); + + // DELETE statements work correctly + expect(parsed.functions).toHaveLength(1); + expect(parsed.functions[0].kind).toBe('plpgsql-function'); + expect(parsed.functions[0].plpgsql.hydrated).toBeDefined(); + }); + }); }); From 02b901ad10933ad78535ae26d0c7d0c4f32b2ddd Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 9 Jan 2026 01:23:09 +0000 Subject: [PATCH 2/2] test: fix comments to reflect regression test purpose Update test comments to accurately describe that these are regression tests to ensure SELECT INTO parsing continues to work, rather than documenting a bug that no longer exists. --- .../__tests__/plpgsql-parser.test.ts | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts index 6776a60f..874032d0 100644 --- a/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts +++ b/packages/plpgsql-parser/__tests__/plpgsql-parser.test.ts @@ -211,18 +211,9 @@ describe('plpgsql-parser', () => { }); describe('SELECT INTO statement parsing', () => { - // This test documents a bug in @libpg-query/parser where PL/pgSQL functions - // containing SELECT INTO statements fail to parse, causing the function - // to not be recognized as a PL/pgSQL function and preventing hydration. - // - // Bug: parsePlPgSQLSync throws "Unexpected non-whitespace character after JSON" - // when the function body contains SELECT INTO statements. - // - // This causes inconsistent behavior: - // - Functions with DELETE/INSERT/UPDATE: parse successfully, get hydrated - // - Functions with SELECT INTO: fail to parse, not recognized as PL/pgSQL - // - // Related issue: https://github.com/pganalyze/libpg_query/issues/XXX + // Regression tests to ensure PL/pgSQL functions containing SELECT INTO + // statements are correctly parsed and hydrated. These tests verify that + // SELECT INTO works consistently with other DML statements like DELETE. it('should parse function with SELECT INTO statement', () => { const selectIntoSql = ` @@ -239,8 +230,6 @@ describe('plpgsql-parser', () => { const parsed = parse(selectIntoSql); - // This currently fails because @libpg-query/parser cannot parse - // PL/pgSQL functions with SELECT INTO statements expect(parsed.functions).toHaveLength(1); expect(parsed.functions[0].kind).toBe('plpgsql-function'); expect(parsed.functions[0].plpgsql.hydrated).toBeDefined(); @@ -261,8 +250,6 @@ describe('plpgsql-parser', () => { const parsed = parse(selectIntoSchemaSql); - // This currently fails because @libpg-query/parser cannot parse - // PL/pgSQL functions with SELECT INTO statements expect(parsed.functions).toHaveLength(1); expect(parsed.functions[0].kind).toBe('plpgsql-function'); }); @@ -283,15 +270,13 @@ describe('plpgsql-parser', () => { const parsed = parse(selectIntoSql); const result = deparseSync(parsed); - // When this works, the deparsed SQL should have consistent quoting - // (either all quoted or all unquoted based on QuoteUtils rules) + // Deparsed SQL should have consistent quoting based on QuoteUtils rules expect(result).toContain('SELECT'); expect(result).toContain('INTO'); expect(result).toContain('v_result'); }); - // Contrast: DELETE statements parse correctly - it('should parse function with DELETE statement (works correctly)', () => { + it('should parse function with DELETE statement', () => { const deleteSql = ` CREATE FUNCTION delete_data() RETURNS void @@ -304,7 +289,6 @@ describe('plpgsql-parser', () => { const parsed = parse(deleteSql); - // DELETE statements work correctly expect(parsed.functions).toHaveLength(1); expect(parsed.functions[0].kind).toBe('plpgsql-function'); expect(parsed.functions[0].plpgsql.hydrated).toBeDefined();