Skip to content

PG 18 issues — likely upstream issue #157

Description

@pyramation

pg_query_parse_plpgsql() regressions in 18.0.0

Three regressions in pg_query_parse_plpgsql() that all work correctly in 17-6.1.0.

Tested via the WASM build (@libpg-query/parser) but the bugs are in the C library's JSON serialization — the WASM wrapper just passes through the raw result.plpgsql_funcs string.


Bug 1: Trigger functions produce malformed JSON

Any function returning trigger emits invalid JSON. The PLpgSQL_rec datums for NEW/OLD records have trailing empty {} objects with mismatched braces.

SQL:

CREATE FUNCTION audit_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END;
$$

PG 17 (valid JSON):

{"plpgsql_funcs":[
{"PLpgSQL_function":{"new_varno":1,"old_varno":2,"datums":[
  {"PLpgSQL_var":{"refname":"found","datatype":{"PLpgSQL_type":{"typname":"pg_catalog.\"boolean\""}}}},
  {"PLpgSQL_rec":{"refname":"new","dno":1}},
  {"PLpgSQL_rec":{"refname":"old","dno":2}},
  {"PLpgSQL_recfield":{"fieldname":"updated_at","recparentno":1}}
],"action":{...}}}
]}

PG 18 (MALFORMED — JSON.parse fails):

{"plpgsql_funcs":[
{"PLpgSQL_function":{"old_varno":1,"datums":[
  {"PLpgSQL_rec":{"refname":"new"}},
  {"PLpgSQL_rec":{"refname":"old","dno":1}},
  {}},{}},{}},{}},{}},{}},{}},{}},{}},{}},
  {"PLpgSQL_var":{"refname":"found","datatype":{"PLpgSQL_type":{"typname":"bool"}}}},
  {"PLpgSQL_recfield":{"fieldname":"updated_at"}}
],"action":{...}}}
]}

Note the 10 extra {} with mismatched closing braces after each PLpgSQL_rec. This causes JSON.parse to throw: "Expected ',' or ']' after array element in JSON".


Bug 2: Qualified type names with non-public schemas return "Not implemented"

Any DECLARE variable using a schema-qualified type outside pg_catalog/public fails entirely — the function returns a plain error string instead of JSON.

SQL:

CREATE FUNCTION test_func() RETURNS void LANGUAGE plpgsql AS $$
DECLARE
    v_user "my_schema".users;
BEGIN
    RETURN;
END;
$$

PG 17: Parses successfully → {"typname":"\"my_schema\".users"}

PG 18: Returns the literal string (not JSON):

Not implemented (LookupExplicitNamespace only supports pg_catalog and public)

Bug 3: RETURN <variable> drops the expression from JSON output

When a PL/pgSQL function uses RETURN <variable>, PG 18 drops both the expr and retvarno fields from PLpgSQL_stmt_return. Literal and complex expression returns are fine.

SQL:

CREATE FUNCTION t() RETURNS int LANGUAGE plpgsql AS $$
DECLARE v int := 1;
BEGIN
    RETURN v;
END;
$$

PG 17:

{"PLpgSQL_stmt_return":{"lineno":1,"expr":{"PLpgSQL_expr":{"query":"v","parseMode":2}}}}

PG 18:

{"PLpgSQL_stmt_return":{"lineno":1}}

The expr is completely missing. This also affects RETURN NEW in trigger functions.

Return expression PG 17 PG 18
RETURN 42 ✅ expr preserved ✅ expr preserved
RETURN x + 1 ✅ expr preserved ✅ expr preserved
RETURN v (declared var) ✅ expr preserved expr dropped
RETURN NEW (trigger) ✅ expr preserved expr dropped

This makes it impossible to reconstruct the original PL/pgSQL source from the JSON AST.


Additional structural changes (may be intentional)

PG 18 also changed several fields in the PL/pgSQL JSON output. These may be intentional PG 18 changes rather than bugs, but they break backward compatibility:

Field PG 17 PG 18
new_varno on trigger functions Present (e.g. 1) Missing
PLpgSQL_recfield.recparentno Present (links field → parent record) Missing for NEW fields, present for OLD
PLpgSQL_type.typname Schema-qualified: pg_catalog.int4, pg_catalog."boolean" Simplified: int4, bool
User-declared types Original name: int Canonical name: int4

Environment

  • libpg_query tag: 18.0.0 (compared against 17-6.1.0)
  • Reproduced via WASM build (@libpg-query/parser@18.0.0 vs @libpg-query/parser@17.6.3)
  • The WASM wrapper (wasm_parse_plpgsql) just wraps result.plpgsql_funcs in {"plpgsql_funcs":%s} — the malformed JSON comes from the C library itself

Reproduction

mkdir plpgsql-repro && cd plpgsql-repro
cat > package.json << 'EOF'
{
  "dependencies": {
    "@libpg-query/parser-v17": "npm:@libpg-query/parser@17.6.3",
    "@libpg-query/parser-v18": "npm:@libpg-query/parser@18.0.0"
  }
}
EOF
npm install
const v17 = require('@libpg-query/parser-v17');
const v18 = require('@libpg-query/parser-v18');

async function main() {
  await v17.loadModule();
  await v18.loadModule();

  const sql = `CREATE FUNCTION t() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN NEW.updated_at := now(); RETURN NEW; END; $$`;

  console.log('PG 17:', JSON.stringify(v17.parsePlPgSQLSync(sql), null, 2));
  console.log('PG 18:', v18.parsePlPgSQLSync(sql)); // throws
}
main();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions