Skip to content

Commit 141de8d

Browse files
authored
Merge pull request #269 from constructive-io/devin/1767720648-fix-failing-fixtures
fix(plpgsql-deparser): fix failing fixtures - EXCEPTION blocks, schema qualification, implicit variables
2 parents 19068b8 + 64c900c commit 141de8d

5 files changed

Lines changed: 54 additions & 26 deletions

File tree

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ exports[`hydrate demonstration with big-function.sql should parse, hydrate, modi
4949
v_sql text;
5050
v_rowcount int := 0;
5151
v_lock_key bigint := CAST(CAST('x' || substr(md5(p_org_id::text), 1, 16) AS pg_catalog.bit(64)) AS bigint);
52-
sqlstate CONSTANT text;
53-
sqlerrm CONSTANT text;
5452
BEGIN
5553
BEGIN
5654
IF p_org_id IS NULL
@@ -192,6 +190,15 @@ BEGIN
192190
message := format('rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate);
193191
RETURN NEXT;
194192
RETURN;
193+
EXCEPTION
194+
WHEN unique_violation THEN
195+
RAISE NOTICE 'unique_violation: %', sqlerrm;
196+
RAISE EXCEPTION;
197+
WHEN others THEN
198+
IF p_debug THEN
199+
RAISE NOTICE 'error: % (%:%)', sqlerrm, sqlstate, sqlerrm;
200+
END IF;
201+
RAISE EXCEPTION;
195202
END;
196203
RETURN;
197204
END$$"

packages/plpgsql-deparser/__tests__/plpgsql-deparser.test.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,14 @@ describe('PLpgSQLDeparser', () => {
3737
// - Tagged dollar quote reconstruction ($tag$...$tag$ not supported)
3838
// - Exception block handling issues
3939
// TODO: Fix these underlying issues and remove from allowlist
40+
// Remaining known failing fixtures:
41+
// - plpgsql_varprops-13.sql: nested DECLARE inside FOR loop (loop variable scope issue)
42+
// - plpgsql_transaction-17.sql: CURSOR FOR loop with EXCEPTION block
43+
// - plpgsql_control-15.sql: labeled block with EXIT statement
4044
const KNOWN_FAILING_FIXTURES = new Set([
4145
'plpgsql_varprops-13.sql',
42-
'plpgsql_trap-1.sql',
43-
'plpgsql_trap-2.sql',
44-
'plpgsql_trap-3.sql',
45-
'plpgsql_trap-4.sql',
46-
'plpgsql_trap-5.sql',
47-
'plpgsql_trap-6.sql',
48-
'plpgsql_trap-7.sql',
4946
'plpgsql_transaction-17.sql',
50-
'plpgsql_transaction-19.sql',
51-
'plpgsql_transaction-20.sql',
52-
'plpgsql_transaction-21.sql',
5347
'plpgsql_control-15.sql',
54-
'plpgsql_control-17.sql',
55-
'plpgsql_call-44.sql',
56-
'plpgsql_array-20.sql',
5748
]);
5849

5950
it('should round-trip ALL generated fixtures (excluding known failures)', async () => {

packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ exports[`lowercase: big-function.sql 1`] = `
1919
v_sql text;
2020
v_rowcount int := 0;
2121
v_lock_key bigint := ('x' || substr(md5(p_org_id::text), 1, 16))::bit(64)::bigint;
22-
sqlstate constant text;
23-
sqlerrm constant text;
2422
begin
2523
begin
2624
if p_org_id IS NULL OR p_user_id IS NULL then
@@ -165,6 +163,15 @@ begin
165163
);
166164
return next;
167165
return;
166+
exception
167+
when unique_violation then
168+
raise notice 'unique_violation: %', SQLERRM;
169+
raise exception;
170+
when others then
171+
if p_debug then
172+
raise notice 'error: % (%:%)', SQLERRM, SQLSTATE, SQLERRM;
173+
end if;
174+
raise exception;
168175
end;
169176
return;
170177
end"
@@ -220,8 +227,6 @@ exports[`uppercase: big-function.sql 1`] = `
220227
v_sql text;
221228
v_rowcount int := 0;
222229
v_lock_key bigint := ('x' || substr(md5(p_org_id::text), 1, 16))::bit(64)::bigint;
223-
sqlstate CONSTANT text;
224-
sqlerrm CONSTANT text;
225230
BEGIN
226231
BEGIN
227232
IF p_org_id IS NULL OR p_user_id IS NULL THEN
@@ -366,6 +371,15 @@ BEGIN
366371
);
367372
RETURN NEXT;
368373
RETURN;
374+
EXCEPTION
375+
WHEN unique_violation THEN
376+
RAISE NOTICE 'unique_violation: %', SQLERRM;
377+
RAISE EXCEPTION;
378+
WHEN others THEN
379+
IF p_debug THEN
380+
RAISE NOTICE 'error: % (%:%)', SQLERRM, SQLSTATE, SQLERRM;
381+
END IF;
382+
RAISE EXCEPTION;
369383
END;
370384
RETURN;
371385
END"

packages/plpgsql-deparser/src/plpgsql-deparser.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,11 @@ export class PLpgSQLDeparser {
342342
const localVars = datums.filter(datum => {
343343
if ('PLpgSQL_var' in datum) {
344344
const v = datum.PLpgSQL_var;
345-
// Skip internal variables
346-
if (v.refname === 'found' || v.refname.startsWith('__')) {
345+
// Skip internal variables:
346+
// - 'found' is the implicit FOUND variable
347+
// - 'sqlstate' and 'sqlerrm' are implicit exception handling variables
348+
// - variables starting with '__' are internal
349+
if (v.refname === 'found' || v.refname === 'sqlstate' || v.refname === 'sqlerrm' || v.refname.startsWith('__')) {
347350
return false;
348351
}
349352
// Skip variables without lineno (usually parameters or internal)
@@ -457,8 +460,13 @@ export class PLpgSQLDeparser {
457460
private deparseType(typeNode: PLpgSQLTypeNode): string {
458461
if ('PLpgSQL_type' in typeNode) {
459462
let typname = typeNode.PLpgSQL_type.typname;
460-
// Clean up type names (remove pg_catalog prefix and quotes)
461-
typname = typname.replace(/^pg_catalog\./, '').replace(/"/g, '');
463+
// Remove quotes
464+
typname = typname.replace(/"/g, '');
465+
// Strip pg_catalog. prefix for built-in types, but preserve schema qualification
466+
// for %rowtype and %type references where the schema is part of the table/variable reference
467+
if (!typname.includes('%rowtype') && !typname.includes('%type')) {
468+
typname = typname.replace(/^pg_catalog\./, '');
469+
}
462470
return typname.trim();
463471
}
464472
return '';
@@ -567,12 +575,17 @@ export class PLpgSQLDeparser {
567575
}
568576

569577
// Exception handlers
570-
if (block.exceptions?.exc_list) {
578+
// The exceptions property can be either:
579+
// - { exc_list: [...] } (direct)
580+
// - { PLpgSQL_exception_block: { exc_list: [...] } } (wrapped)
581+
const excList = block.exceptions?.exc_list ||
582+
(block.exceptions as any)?.PLpgSQL_exception_block?.exc_list;
583+
if (excList) {
571584
parts.push(kw('EXCEPTION'));
572-
for (const exc of block.exceptions.exc_list) {
585+
for (const exc of excList) {
573586
if ('PLpgSQL_exception' in exc) {
574587
const excData = exc.PLpgSQL_exception;
575-
const conditions = excData.conditions?.map(c => {
588+
const conditions = excData.conditions?.map((c: any) => {
576589
if ('PLpgSQL_condition' in c) {
577590
return c.PLpgSQL_condition.condname || c.PLpgSQL_condition.sqlerrstate || 'OTHERS';
578591
}

packages/plpgsql-deparser/test-utils/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ export const cleanPlpgsqlTree = (tree: any) => {
213213
location: noop,
214214
stmt_len: noop,
215215
stmt_location: noop,
216+
// varno values are assigned based on position in datums array and can change
217+
// when implicit variables (like sqlstate/sqlerrm) are filtered out during deparse
218+
varno: noop,
216219
query: normalizeQueryWhitespace,
217220
});
218221
};

0 commit comments

Comments
 (0)