Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to plx are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/), and plx uses the extension
version in `plx.control` (currently `1.0`).

## [Unreleased]

### Security

- A local variable's type is spliced into the plpgsql `DECLARE` section, and
plpgsql reads a declaration up to its `;`. A type annotation could therefore
carry an initializer or more statements. For example, `x #:: int := f()` or
`let x /*:: int DEFAULT f() */;` ran `f()` on entry, while the source showed
only a type, and in plxjs only a comment. Every type that reaches `DECLARE`
(annotations, plxts types, plxgo and plxtsql declarations, the plxcobol `TYPE`
clause) must now be a type name accepted by the SQL type-name grammar, or
`name%TYPE` / `name%ROWTYPE`. Anything else is rejected at `CREATE FUNCTION`.

### Fixed

- In plxjs and plxphp, a block annotation placed before an initializer
(`let n /*:: bigint */ = 3;`) silently dropped the initializer, so `n` was
NULL. That form is now rejected with a hint to write
`let n = 3 /*:: bigint */;`.

## [2.0.1] - 2026-08-25

Packaging only. **The extension is unchanged at 2.0.0**, so there is no
Expand Down
3 changes: 2 additions & 1 deletion doc/plxcobol.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ WORKING-STORAGE SECTION.
```

For types a PICTURE cannot express (`%TYPE`, `%ROWTYPE`, `RECORD`, `refcursor`),
use a `TYPE` clause, whose text is emitted verbatim as the plpgsql type:
use a `TYPE` clause, whose text becomes the plpgsql type (it must be a type name
and nothing else):

```sql
01 WS-EMP TYPE emp%ROWTYPE.
Expand Down
7 changes: 5 additions & 2 deletions doc/plxjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ expressions are passed through to plpgsql and SQL unchanged.
Declare locals with `let`, `const`, or `var`; the keyword is removed in the
generated plpgsql. plx infers the type from the first value when it is an
integer, numeric, text, or boolean literal. Otherwise annotate the variable with
a `/*:: type */` comment before the `;`.
a `/*:: type */` comment before the `;`. An annotation before an initializer
(`let n /*:: bigint */ = 3;`) is rejected; write `let n = 3 /*:: bigint */;`.

```sql
let n = 42; // inferred integer
Expand All @@ -41,7 +42,9 @@ let amount /*:: numeric */;
amount = principal * rate;
```

The annotation text is emitted verbatim, so `%TYPE` and `%ROWTYPE` work:
The annotation becomes the plpgsql type. It must be a type name and nothing
else (plx checks this at `CREATE FUNCTION` time), and `%TYPE` and `%ROWTYPE`
work:

```sql
let e /*:: emp%ROWTYPE */;
Expand Down
7 changes: 5 additions & 2 deletions doc/plxphp.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ through to plpgsql and SQL unchanged.

Assignment creates a local. plx infers the type from the first value when it is
an integer, numeric, text, or boolean literal. Otherwise annotate the variable
with a `/*:: type */` comment placed before the `;`.
with a `/*:: type */` comment placed before the `;`. An annotation before an initializer
(`$n /*:: bigint */ = 3;`) is rejected; write `$n = 3 /*:: bigint */;`.

```sql
$n = 42; // inferred integer
Expand All @@ -40,7 +41,9 @@ $amount /*:: numeric */;
$amount = $principal * $rate;
```

The annotation text is emitted verbatim, so `%TYPE` and `%ROWTYPE` work:
The annotation becomes the plpgsql type. It must be a type name and nothing
else (plx checks this at `CREATE FUNCTION` time), and `%TYPE` and `%ROWTYPE`
work:

```sql
$e /*:: emp%ROWTYPE */;
Expand Down
4 changes: 3 additions & 1 deletion doc/plxpython3.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ amount #:: numeric
amount = principal * rate
```

The annotation text is emitted verbatim, so `%TYPE` and `%ROWTYPE` work:
The annotation becomes the plpgsql type. It must be a type name and nothing
else (plx checks this at `CREATE FUNCTION` time), and `%TYPE` and `%ROWTYPE`
work:

```sql
e #:: emp%ROWTYPE
Expand Down
5 changes: 3 additions & 2 deletions doc/plxruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ amount = principal * rate
A first assignment of a constant literal at the top level is folded into the
declaration, so `n = 42` becomes `DECLARE n integer := 42;`.

The annotation text is emitted verbatim as the plpgsql type, so `%TYPE` and
`%ROWTYPE` work:
The annotation becomes the plpgsql type. It must be a type name and nothing
else (plx checks this at `CREATE FUNCTION` time), and `%TYPE` and `%ROWTYPE`
work:

```sql
e #:: emp%ROWTYPE
Expand Down
145 changes: 145 additions & 0 deletions src/plx_transpile.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"

#include "plx.h"
Expand Down Expand Up @@ -2257,6 +2258,11 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel)

if (plx_is_param(cx, t0->s, t0->len))
plx_err(cx, t0->line, "cannot annotate parameter \"%.*s\"", t0->len, t0->s);
/* a block annotation before "= 3" used to drop the "= 3" silently */
if (a + 2 < b)
plx_err(cx, t0->line,
"a type annotation must come after the value: write \"%s%.*s = value /*:: type */\"",
cx->surf->var_sigil == '$' ? "$" : "", t0->len, t0->s);
l = plx_local_find(cx, t0->s, t0->len);
if (!l)
l = plx_local_add(cx, t0->s, t0->len);
Expand Down Expand Up @@ -2613,7 +2619,137 @@ b64_encode_body(const char *body)
return dst;
}

/*
* Local-variable types are checked before they are spliced into DECLARE.
*
* plpgsql reads a declaration's type up to the next ':=', DEFAULT, NOT NULL,
* COLLATE or ';'. A type taken verbatim from an annotation could therefore
* carry an initializer: "x #:: int := f()", or "int DEFAULT f()" in a JS
* block-comment annotation, would run f() on entry while the source shows
* only a type (and in JS, only a comment). So a type must be exactly a type name, as
* plpgsql's own parse_datatype accepts it, or name%TYPE / name%ROWTYPE.
*/
static const char *
tt_ws(const char *p)
{
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
return p;
}

/* one plain or double-quoted identifier; NULL when there is none */
static const char *
tt_ident(const char *p)
{
if (*p == '"')
{
for (p++; *p; p++)
if (*p == '"')
{
if (p[1] != '"')
return p + 1;
p++;
}
return NULL;
}
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_' ||
IS_HIGHBIT_SET(*p)))
return NULL;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_' || *p == '$' || IS_HIGHBIT_SET(*p))
p++;
return p;
}

/* [schema.]table[.column]%TYPE or %ROWTYPE, optionally followed by [] */
static bool
tt_is_pct_type(const char *p)
{
p = tt_ws(p);
for (;;)
{
if (!(p = tt_ident(p)))
return false;
p = tt_ws(p);
if (*p != '.')
break;
p = tt_ws(p + 1);
}
if (*p != '%')
return false;
p = tt_ws(p + 1);
if (pg_strncasecmp(p, "rowtype", 7) == 0)
p += 7;
else if (pg_strncasecmp(p, "type", 4) == 0)
p += 4;
else
return false;
p = tt_ws(p);
while (*p == '[')
{
p = tt_ws(p + 1);
while (*p >= '0' && *p <= '9')
p++;
p = tt_ws(p);
if (*p != ']')
return false;
p = tt_ws(p + 1);
}
return *p == '\0';
}

/* the SQL type-name grammar, exactly as plpgsql's parse_datatype reads it */
static bool
tt_is_sql_type(const char *typ)
{
MemoryContext oldcxt = CurrentMemoryContext;
volatile bool ok = true;
const char *c;

/* a comment would hide the declaration's ';' */
for (c = typ; *c; c++)
if ((c[0] == '-' && c[1] == '-') || (c[0] == '/' && c[1] == '*') ||
((unsigned char) *c < 0x20 && *c != ' ' && *c != '\t' &&
*c != '\n' && *c != '\r'))
return false;

PG_TRY();
{
#if PG_VERSION_NUM >= 160000
(void) typeStringToTypeName(typ, NULL);
#else
(void) typeStringToTypeName(typ);
#endif
}
PG_CATCH();
{
ErrorData *ed;

MemoryContextSwitchTo(oldcxt);
ed = CopyErrorData();
if (ed->sqlerrcode != ERRCODE_SYNTAX_ERROR)
PG_RE_THROW();
FlushErrorState();
FreeErrorData(ed);
ok = false;
}
PG_END_TRY();
return ok;
}

static void
plx_check_local_type(const PlxSurface *surf, const PlxLocal2 *l)
{
if (tt_is_pct_type(l->typ) || tt_is_sql_type(l->typ))
return;
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("%s: invalid type \"%s\" for local variable \"%s\"",
surf->lanname, l->typ, l->name),
errhint("A type annotation must be a type name only, such as integer, "
"numeric(10,2), text[] or tbl%%ROWTYPE. Assign the initial value "
"in the body.")));
}

bool
plx_has_sentinel(const char *prosrc)
Expand Down Expand Up @@ -2667,9 +2803,18 @@ plx_transpile(const char *body, const PlxFuncMeta *meta, const PlxSurface *surf,
{
if (cx.locals)
{
const char *checked = NULL; /* last type that passed; runs repeat */

appendStringInfoString(&asm_, "DECLARE\n");
for (l = cx.locals; l; l = l->next)
{
CHECK_FOR_INTERRUPTS();
if (l->typ && !l->is_record &&
(checked == NULL || strcmp(checked, l->typ) != 0))
{
plx_check_local_type(surf, l);
checked = l->typ;
}
if (l->is_record)
appendStringInfo(&asm_, " %s RECORD;\n", l->name);
else if (!l->typ)
Expand Down
74 changes: 74 additions & 0 deletions test/expected/plx_errors.out
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,77 @@ return 0xFFFFFFFFFFFFFFFFFF;
$$;
ERROR: plxjs: integer literal out of range: 0xFFFFFFFFFFFFFFFFFF
DETAIL: at plxjs source line 0
-- a type annotation is spliced into DECLARE, so it must be a type name only;
-- anything after the type would become an initializer or a new statement
CREATE FUNCTION e_rb_ann_init() RETURNS int LANGUAGE plxruby AS $$
x #:: int := (SELECT 42)
return x
$$;
ERROR: plxruby: invalid type "int := (SELECT 42)" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
CREATE FUNCTION e_js_ann_default() RETURNS int LANGUAGE plxjs AS $$
let x /*:: int DEFAULT 42 */;
return x;
$$;
ERROR: plxjs: invalid type "int DEFAULT 42" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
CREATE FUNCTION e_py_ann_stmt() RETURNS int LANGUAGE plxpython3 AS $$
x #:: int; BEGIN RAISE NOTICE 'pwn'; END; DECLARE y int
return 1
$$;
ERROR: plxpython3: invalid type "int; BEGIN RAISE NOTICE 'pwn'; END; DECLARE y int" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
CREATE FUNCTION e_rb_ann_comment() RETURNS text LANGUAGE plxruby AS $$
x #:: text --
return x
$$;
ERROR: plxruby: invalid type "text --" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
CREATE FUNCTION e_rb_ann_notnull() RETURNS int LANGUAGE plxruby AS $$
x #:: int NOT NULL
return 1
$$;
ERROR: plxruby: invalid type "int NOT NULL" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
CREATE FUNCTION e_rb_ann_rowtype_init() RETURNS int LANGUAGE plxruby AS $$
x #:: pg_class%ROWTYPE := NULL
return 1
$$;
ERROR: plxruby: invalid type "pg_class%ROWTYPE := NULL" for local variable "x"
HINT: A type annotation must be a type name only, such as integer, numeric(10,2), text[] or tbl%ROWTYPE. Assign the initial value in the body.
-- a block annotation before the initializer used to drop the initializer
CREATE FUNCTION e_js_ann_before_init() RETURNS int LANGUAGE plxjs AS $$
let n /*:: bigint */ = 3;
return n;
$$;
ERROR: plxjs: a type annotation must come after the value: write "n = value /*:: type */"
DETAIL: at plxjs source line 2
CREATE FUNCTION e_php_ann_before_init() RETURNS int LANGUAGE plxphp AS $$
$n /*:: bigint */ = 3;
return $n;
$$;
ERROR: plxphp: a type annotation must come after the value: write "$n = value /*:: type */"
DETAIL: at plxphp source line 2
-- every type-name form plpgsql accepts still works
CREATE FUNCTION ok_rb_ann_types() RETURNS text LANGUAGE plxruby AS $$
a #:: double precision
b #:: timestamp(3) with time zone
c #:: numeric(10, 2)
d #:: character varying(20)[]
e #:: pg_class%ROWTYPE
f #:: pg_catalog.pg_class.relname%TYPE
g #:: "pg_catalog"."int8"
h #:: int[][]
i #:: interval day to second
c = 1.005
f = "rel"
g = 7
return "#{c} #{f} #{g}"
$$;
SELECT ok_rb_ann_types();
ok_rb_ann_types
-----------------
1.01 rel 7
(1 row)

DROP FUNCTION ok_rb_ann_types();
Loading