fix(plpgsql-deparser): decide assignment targets by parse mode, not a ':=' substring test - #345
Merged
Merged
Conversation
… ':=' substring test
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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.
Summary
PlpgsqlDeparser.deparseAssigndecided whether the stored query text already contained theassignment target with
expr.includes(':=').:=is also PostgreSQL's named-argumentoperator, so any assignment whose value is a call with named arguments looked self-contained
and its target was silently dropped:
The AST already answers the question, so no string inspection is needed. PostgreSQL parses an
assignment's right-hand side in a dedicated raw parse mode per target shape and libpg-query
preserves it on
PLpgSQL_expr.parseMode; in those modes the query text is the wholeassignment (
cnt := cnt + 1), and in every lower mode it is only the value.This mirrors the same fix already proven in
constructive-db's SQL deparser(
deparser_plpgsql.stmt_assign,parse_mode >= plpgsql_assign_parse_mode()).The existing un-parenthesizing of subscripted/field targets carried in the text
(
(a)[2] := 5→a[2] := 5) is unchanged, now gated on the parse mode instead.Confirmed parse-mode values
RawParseModevalues were confirmed empirically against thelibpg-query18.1.4 this packagebuilds on (and match
src/include/nodes/parsenodes.h):RAW_PARSE_DEFAULT0,RAW_PARSE_TYPE_NAME1,RAW_PARSE_PLPGSQL_EXPR2,RAW_PARSE_PLPGSQL_ASSIGN13,ASSIGN24,ASSIGN35. Parsingcnt := cnt + 1/a[2] := 5yieldsparseMode: 3,r.f := 7yields4, andPERFORM 1yields0. The constant reuses the existingParseModeenum inhydrate-types.tsrather than a bare literal.Back-compat decision: a missing
parseModemeans "not an assignment mode"Chosen deliberately (option (a) of the two considered):
parseMode, so parser-produced ASTs are correct in everycase, with the mode — not a string scan — deciding.
constructive-db'sast-plpgsql) put the value inexprand the target invarno, which is the naturalshape given the node's fields. Falling back to the substring test when the mode is absent
would leave exactly the reported bug unfixed for them, which is the whole point of this PR.
0 — i.e. treats absence as a non-assignment mode.
parseMode: 3(pinned by a test); previously it relied on the text containing:=. That isthe accepted cost — such a node was already ambiguous and, with named arguments in the
value, already wrong.
Tests
New cases in
packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts(two also added asround-trip fixtures in
__fixtures__/plpgsql/plpgsql_deparser_fixes.sql). The two hand-builtcases fail before the fix and pass after; the parser round-trip cases guard the behaviour:
parse → deparse → reparse, AST compared) andhand-built without
parseMode— target survives;sum := sum + nstill deparses once, not twice;a[2] := .../r.f := ...targets still emitted correctly;parseModepinned as value-only, andparseMode: 3pinned as whole-assignment;(v_x)[2] := 5withASSIGN3still un-parenthesized.Findings, deliberately left alone
__fixtures__/plpgsql-generated/generated.jsonre-numbers theplpgsql_call-*entries becausetest_proc11(a OUT int, VARIADIC b int[]) now parses andis no longer skipped, shifting later keys by one. This drift is pre-existing: running
pnpm fixtureson unmodifiedmainproduces the same 19 changed lines, so the committedfile was stale relative to the current
libpg-query. No existing snapshot changed; the onlysnapshot added is for the new named-argument test. All 252 fixtures round-trip.
packages/plpgsql-parse/src/deparse.tsderives an assignment's leading keyword withquery.trim().split(/[\s:=]+/)[0], which is wrong for value-only assignment text (it takesthe first token of the value rather than the target). It only affects statement-to-line
mapping in the pretty printer, so it is out of scope here.
Verification
pnpm build,pnpm lint(no new warnings), and the full suite pass: 3170 tests across everypackage that has tests.
pgsql-typesand@pgsql/clifailpnpm testonmainwith "Notests found" — unrelated to this change and unchanged by it.
Link to Devin session: https://app.devin.ai/sessions/20e40cc5892e4971bdd50c5f8ab84d24
Requested by: @pyramation