Fix PG 18 pg_query_parse_plpgsql() regressions + add regression samples#355
Open
pyramation wants to merge 1 commit into
Open
Fix PG 18 pg_query_parse_plpgsql() regressions + add regression samples#355pyramation wants to merge 1 commit into
pyramation wants to merge 1 commit into
Conversation
Adds test coverage (and the fixes needed for it) for the three pg_query_parse_plpgsql() regressions reported in pganalyze#337, so they are caught during future release updates: - Trigger functions emitted malformed JSON: the PLPGSQL_DTYPE_PROMISE datums created for TG_* variables were not handled in dump_function(), producing empty {} objects with mismatched braces. Serialize them via dump_var() (PROMISE datums are PLpgSQL_var structs). - Schema-qualified variable types outside pg_catalog/public returned the literal string "Not implemented (LookupExplicitNamespace ...)" instead of JSON. Fall back to the public namespace for unknown schemas (fixed in the mock so it survives extract_source). - RETURN <variable> dropped the return target: retvarno was commented out in dump_return()/dump_return_next(). Emit it when >= 0 (an expression return uses retvarno -1 and keeps its expr). Adds the three cases to test/plpgsql_samples.sql and regenerates the expected golden JSON.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thanks @lfittl ! Per your last comment, I work with my agent on bringing those tests cases into the repo... :)
Summary
This brings the three
pg_query_parse_plpgsql()PG 18 regressions from pganalyze/libpg_query#337 into libpg_query's own output-based test suite (test/plpgsql_samples.sql→ goldentest/plpgsql_samples.expected.json), plus the minimal fixes required for those samples to emit valid, meaningful JSON.This is a follow-up iteration on the earlier reference fix constructive-io/libpg_query#1 (
fix/plpgsql-json-serialization), which fixed the bugs but did not add regression coverage. In this comment on #337 Lukas asked:So this iteration does exactly that — the repros now live in the harness that the maintainer regenerates on every Postgres version bump, so a re-break shows up as a golden diff. Two things also changed vs. #1: the namespace fix is applied in the mock (
scripts/mocks/) so it survivesextract_source, andretvarnois emitted only for real datums instead of unconditionally.All three regressions were re-confirmed still-present on
18-latestbefore fixing (they were introduced by the PG 18 PL/pgSQL compilation rework —bc734d7"Rework how PL/pgSQL compilation is done to rely on original code more" andca4d024"Rework PL/pgSQL parsing to use proper type definitions", against Postgres 18.4).The three fixes
1. Trigger functions emitted malformed JSON — handle
PLPGSQL_DTYPE_PROMISEdatumsCause. PG 18 represents the implicit trigger variables (
tg_name,tg_when,tg_level,tg_op,tg_relid, …) as promise datums (PLPGSQL_DTYPE_PROMISE) instead of eagerly-populated vars.dump_function()'s datumswitchhad no case for that dtype, so each promise datum fell through todefault:, which emits nothing — but the surrounding loop still appended the object braces, producing{}with a mismatched trailing}for each one (…,{}},{}},{}},…). That corrupts the entiredatumsarray and makes the whole result failJSON.parse.Fix. Route promise datums to
dump_var():Why this is correct and safe. A promise datum is a
PLpgSQL_var— Postgres stores both in the same struct. From the struct's own doc comment (src/pl/plpgsql/src/plpgsql.h):So the cast
(PLpgSQL_var *) dis exactly what upstream itself does. See:struct PLpgSQL_var,PLpgSQL_datum_type,PLpgSQL_promise_type)plpgsql_compilein https://github.com/postgres/postgres/blob/REL_18_STABLE/src/pl/plpgsql/src/pl_comp.c (thetg_*vars are built, thenvar->dtype = PLPGSQL_DTYPE_PROMISE; var->promise = PLPGSQL_PROMISE_TG_*)plpgsql_fulfill_promise/ thePLPGSQL_DTYPE_PROMISEcases in https://github.com/postgres/postgres/blob/REL_18_STABLE/src/pl/plpgsql/src/pl_exec.cdump_var()only reads the leadingPLpgSQL_varfields (refname,lineno,datatype,default_val, cursor fields) — all populated for promise datums — so there is no possibility of reading uninitialized/mismatched memory. It cannot affect non-trigger functions (no promise datums), and it strictly adds previously-missing cases to aswitchwhose other arms are untouched.2. Schema-qualified types outside
pg_catalog/publicreturned an error string instead of JSONCause. The PG 18 rework resolves a schema-qualified variable type (e.g.
"my_schema".users) throughLookupExplicitNamespace(). libpg_query has no real catalog, so it stubs that function with a mock; the mockelog(ERROR, …)'d for any schema other thanpg_catalog/public, which aborts the whole parse and makespg_query_parse_plpgsql()return the literal stringNot implemented (LookupExplicitNamespace only supports pg_catalog and public)instead of JSON.Fix (
scripts/mocks/LookupExplicitNamespace.c, and the extractedsrc/postgres/src_backend_catalog_namespace.cto match):Why this is correct and safe. This does not invent a new code path — it funnels every non-catalog schema into the
publicpath that libpg_query already supports. The caller isLookupTypeNameExtended()(scripts/mocks/LookupTypeNameExtended.c), which then callsGetSysCacheOid2(TYPENAMENSP, …, namespaceId). TheGetSysCacheOidmock is already written to expect exactly this and to resolve unknown public-namespace types toRECORDOID(its own comment):So the change is behavior-preserving for the cases that already worked, and only converts former hard-errors into the pre-existing public/
RECORDOIDbehavior:pg_catalogPG_CATALOG_NAMESPACE→ builtin type oidpublicPG_PUBLIC_NAMESPACE→RECORDOIDfor unknown typesmy_schema, …)elog(ERROR), whole parse failsPG_PUBLIC_NAMESPACE→RECORDOID, same aspublicEmpirically verified —
public.usersand"my_schema".usersnow produce byte-identical AST (PLpgSQL_rec→RECORDOID), andpg_catalog.int4still resolves to the builtinint4:Because
elog(ERROR)produced no JSON at all, broadening it cannot regress any input that previously parsed successfully — the only inputs whose behavior changes are ones that used to fail outright. In a parse-only context there is no real namespace to be "wrong" about; the mock exists solely to let type resolution proceed far enough to build the AST. Relevant upstream reference for the real function's contract:LookupExplicitNamespace/LookupTypeNameExtendedin https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/catalog/namespace.c and https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/parser/parse_type.c.3.
RETURN <variable>dropped the return targetCause. For a bare variable/record return (
RETURN v,RETURN NEW), PG 18 records the target inPLpgSQL_stmt_return.retvarnorather than inexpr. Butretvarnowas commented out indump_return()/dump_return_next(), so the entire return payload disappeared from the JSON ({"PLpgSQL_stmt_return":{"lineno":4}}), making the source unreconstructable.Fix.
Why this is correct and safe.
retvarnois anintwhose sentinel-1means "this return usesexpr, not a datum" (seePLpgSQL_stmt_returnin https://github.com/postgres/postgres/blob/REL_18_STABLE/src/pl/plpgsql/src/plpgsql.h and how it is set inpl_gram.y/ consumed inexec_stmt_returnin https://github.com/postgres/postgres/blob/REL_18_STABLE/src/pl/plpgsql/src/pl_exec.c). The two representations are mutually exclusive: expression-returns keepexprand carryretvarno == -1; variable-returns carry a real datum index and noexpr. Guarding on>= 0therefore emitsretvarnoexactly for variable-returns (the ones that were losing data) and never adds a misleadingretvarno":-1to the many expression-returns. This is a deliberate refinement over PR #1, which uncommentedWRITE_INT_FIELD(retvarno,…)unconditionally and thus stamped-1onto every expression-return.Test changes
test/plpgsql_samples.sql(trigger, schema-qualified type,RETURN <var>), matching the repros in pg_query_parse_plpgsql() regressions in 18.0.0 #337.test/plpgsql_samples.expected.jsonvia the existingtest/parse_plpgsqlharness.retvarnoto a handful of pre-existing samples that doRETURN <var>/RETURN ref— expected and correct, not a behavior change beyond the fix itself.Note on the golden-file mechanism: a golden test only catches a regression if the committed baseline is correct — a snapshot of malformed JSON would simply lock the bug in. That's why the samples are paired with the fixes rather than added alone.
Verification
make test(valgrind disabled locally) — all suites pass, including theplpgsql_samplesdiff.JSON.parse-able JSON (was: throw / error-string / lossy before).pg_catalogbuiltins confirmed as shown above.