Skip to content

[2.1.0] Accept only a type name in a type annotation; reject an annotation before an initializer - #20

Open
OffgridwithJD wants to merge 1 commit into
commandprompt:masterfrom
OffgridwithJD:fix/validate-type-annotations
Open

OffgridwithJD wants to merge 1 commit into
commandprompt:masterfrom
OffgridwithJD:fix/validate-type-annotations

Conversation

@OffgridwithJD

@OffgridwithJD OffgridwithJD commented Nov 9, 2026 •

Copy link
Copy Markdown

Summary

Found while reviewing the new plxlua front end. The problem is in the shared code, so it affects every dialect that hoists DECLAREs.

Type annotations could carry code

A local's type is spliced into the plpgsql DECLARE section, and plpgsql reads a declaration up to its ;. So anything after the type in an annotation became part of the declaration:

Source Generated Effect
x #:: int := (SELECT ...) x int := (SELECT ...); runs a query on entry
let x /*:: int DEFAULT 42 */; x int DEFAULT 42; plxjs source that reads as let x; returns 42
x #:: int; BEGIN ... END; DECLARE y int spliced as statements

This is the same class as the string-literal breakout in #12: the source shows only a type (in plxjs, only a comment), but other code runs. I confirmed that set_config('search_path', ...) in an annotation initializer runs and changes the caller's setting.

Fix. Before a type reaches DECLARE it must be one of these, with no comment in it:

  • name%TYPE or name%ROWTYPE, optionally qualified, optionally followed by []
  • a type name the SQL type-name grammar accepts. The check uses typeStringToTypeName, the same parser plpgsql's own parse_datatype uses.

The check runs where DECLARE is assembled, so it covers:

  • annotations in every dialect
  • plxts types
  • plxgo and plxtsql declarations
  • the plxcobol TYPE clause
  • the new dialects (plxlua, plxcsharp), with no work on their side

Why this is sound. plpgsql's read_datatype stops at :=, =, DEFAULT, NOT and COLLATE, and at , or ) outside parentheses. None of these can appear at the top level of a SQL type name. When one appears inside a type modifier, plpgsql hands an unbalanced prefix to parse_datatype, which fails. So any text that passes the check is either consumed whole as the type or rejected by plpgsql. Comments (--, /*) are rejected up front, because the SQL parser skips them but a -- would hide the declaration's ;.

Only syntax errors from the parser are turned into the plx error. Any other error is re-thrown unchanged.

Annotation before an initializer dropped the initializer

In plxjs and plxphp, let n /*:: bigint */ = 3; was treated as a bare annotation, and the = 3 was silently dropped, leaving n NULL. It is now rejected with a hint to write let n = 3 /*:: bigint */;, the documented form.

Compatibility

Code-only, so no ALTER EXTENSION is needed. A function whose annotation held anything but a type is now rejected at CREATE FUNCTION. Every documented form still works, and a new test covers them:

  • double precision
  • timestamp(3) with time zone
  • numeric(10, 2)
  • character varying(20)[]
  • %ROWTYPE and qualified %TYPE
  • quoted qualified names
  • int[][]
  • interval day to second

Cost

One raw parse per distinct type, about 2 µs each. Consecutive locals with the same type are checked once. With #12's locals hash, a body of 40,000 locals that each have a different type transpiles in 0.12 s.

Testing

  • make installcheck: all 13 suites pass on PG 18.4. CI covers 13 to 18. The version split (typeStringToTypeName gained an escontext argument in 16) is handled with #if.
  • make differentialcheck: 0 unexplained.
  • New cases in plx_errors: six rejected annotations across plxruby, plxjs and plxpython3; the two misplaced-annotation cases; and the accepted forms above.

🤖 Generated with Claude Code

…e an initializer

A local's type is spliced into DECLARE, where plpgsql reads up to the ';'.
An annotation such as "int := f()" or "int DEFAULT f()" therefore became an
initializer that ran on entry while the source showed only a type (in plxjs,
only a comment), and "int; BEGIN ... END; DECLARE y int" became statements.

Before a type reaches DECLARE it must now be name%TYPE / name%ROWTYPE or a
type name the SQL type-name grammar accepts (typeStringToTypeName, which is
what plpgsql's own parse_datatype uses), with no comment in it. The check is
at the one place DECLARE is assembled, so it covers annotations, plxts types,
plxgo and plxtsql declarations and the plxcobol TYPE clause.

Also: in plxjs and plxphp, "let n /*:: bigint */ = 3;" was treated as a bare
annotation and the "= 3" was dropped, leaving n NULL. It is now rejected with
a hint to put the annotation after the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OffgridwithJD pushed a commit to OffgridwithJD/plx that referenced this pull request Nov 9, 2026
"let x /*:: int */ = 0;" drops the "= 0" (commandprompt#20 makes that form an error).
The test's result does not depend on it, but it should use the documented
form so it keeps passing once commandprompt#20 is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants