Skip to content

fix(plpgsql-deparser): keep multi-line literals, "char" and empty CASE ELSE intact - #365

Open
pyramation wants to merge 1 commit into
mainfrom
devin/1792899353-plpgsql-literal-fidelity
Open

pyramation wants to merge 1 commit into
mainfrom
devin/1792899353-plpgsql-literal-fidelity

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

Summary

Three PL/pgSQL deparser bugs that changed what a function does after a parse → deparse round trip:

Input Deparsed before Effect
RAISE NOTICE 'a
b'; inside an IF
'a
b'
indent() added indentation after every newline, including the ones inside string literals and dollar quotes. Message text and EXECUTE format(...) SQL changed.
DECLARE c "char"; c char; Simple type names had their quotes stripped. "char" (1-byte internal type) is not char (bpchar(1)).
CASE x WHEN 1 THEN ... ELSE END CASE; ELSE dropped The ELSE was emitted only when else_stmts was non-empty. Without it, a non-matching value raises CASE_NOT_FOUND.

Fixes:

  • indent() now scans the text and indents only newlines outside '...' (with E'...' backslash escapes), "...", and $tag$...$tag$. Newlines in comments are still indented. skipQuoted() is the new helper.
  • deparseType() keeps the quotes on "char" (and "char"[]). All other simple types behave as before.
  • CASE emits ELSE whenever have_else is set.

Tests

Added fixtures 68–70 to __fixtures__/plpgsql/plpgsql_deparser_fixes.sql and regenerated plpgsql-generated/generated.json with npm run fixtures.

  • Before the fix: AST_MISMATCH: plpgsql_deparser_fixes-68.sql / -69.sql / -70.sql
  • After the fix: plpgsql-deparser has 115/115 tests passing (255/255 fixtures round-trip). plpgsql-parser (345), @pgsql/transform (250) and @pgsql/traverse (36) also pass.

All three bugs also exist in plpgsql-deparser 18.3.0, so they are not regressions in 18.3.1.

Link to Devin session: https://app.devin.ai/sessions/f8a1d342fa3b4580a90de9031ed4bd86
Open in Devin Desktop: https://app.devin.ai/desktop/session/f8a1d342fa3b4580a90de9031ed4bd86?variant=devin
Requested by: @pyramation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Nested block comments close at first */ (plpgsql-deparser.ts:2443) — skipQuoted handles /* by jumping to the first */ (plpgsql-deparser.ts line 2444), but PostgreSQL allows nested block comments (/* /* */ */).

This PR fixes three PL/pgSQL deparser round-trip bugs, each backed by a new fixture in plpgsql_deparser_fixes.sql. The indent helper is rewritten as a scanner that skips string literals, quoted identifiers, dollar-quoted strings, and comments before re-indenting newlines, so multi-line literal text survives unchanged. The simple-type branch now keeps quotes for "char" (which is a distinct type from char), and the CASE deparser emits an ELSE keyword even when the else branch has no statements, using ?? [] to guard a missing list.

Files Change
packages/plpgsql-deparser/src/plpgsql-deparser.ts Literal-aware indent plus new skipQuoted scanner; "char" type preserved; CASE ... ELSE emitted on have_else alone
__fixtures__/plpgsql/plpgsql_deparser_fixes.sql, __fixtures__/plpgsql-generated/generated.json Tests 68-70 covering multi-line literals in nested blocks, quoted "char" type, and empty ELSE

Reviewed commit: ffd037e

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds three PL/pgSQL fixtures (multi-line literals, quoted "char", empty ELSE) and teaches the deparser to preserve literal newlines when indenting, keep "char" quoted, and deparse an empty ELSE branch.

Key findings

Comment on lines +886 to 889
// Simple type - strip quotes unless they change the meaning ("char" is not char)
const trimmed = typname.trim();
return /^"char"(\[\d*\])*$/.test(trimmed) ? trimmed : trimmed.replace(/"/g, '');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Quote-stripping still mangles other quoted type names

The new guard protects only the exact name "char" (plus array suffixes); every other quoted simple type is still blindly unquoted at packages/plpgsql-deparser/src/plpgsql-deparser.ts:888. A quoted mixed-case or otherwise unquotable type name such as "CHAR" or "my type" is emitted as CHAR / my type, silently re-resolving to the built-in char or producing invalid SQL. This is the same meaning-changing-unquoting defect the change fixes for "char", left unhandled for all other quoted names.

📋 Prompt for AI Agents

In packages/plpgsql-deparser/src/plpgsql-deparser.ts around lines 886-889, replace the blanket trimmed.replace(/"/g, '') with a conditional: keep the quotes unless the inner content is already a valid lowercase unquoted identifier (/^[a-z_][a-z0-9_$]*$/), with the documented exception that "char" is always preserved as-is per the existing regex. This prevents quoted names like "CHAR" or "my type" from being silently unquoted into a different or invalid type.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Folded into #371

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant