Skip to content

PG 18 issues — likely upstream issue #157

@pyramation

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions