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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ 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]

### Fixed

- **An interpolated string as a local's first value read stale values.** The
first top-level assignment of a string literal is folded into the `DECLARE`
section as the variable's initializer, which runs when the function starts.
An interpolating string (Ruby `"#{v}"`, PHP `"$v"`, JS `` `${v}` ``, Python
`f"{v}"`) was folded too, so it read its variables before the statements
above it had run: `v = "v0"; v = "v2"; s = "got #{v}"` returned `got v0`. Such
an assignment now runs in place. A plain literal is still folded.
- **A constant computed from a local variable read the variable's initial
value.** A constant becomes a plpgsql `CONSTANT`, computed when the function
starts, so `v = 1; v = 2; c = v + 1 #:: int const` gave `c = 2`. A constant
may now use only literals, parameters and earlier constants; reading another
local is rejected at `CREATE FUNCTION`.

## [2.0.1] - 2026-08-25

Packaging only. **The extension is unchanged at 2.0.0**, so there is no
Expand Down
5 changes: 5 additions & 0 deletions doc/plxjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ A constant uses the `const` suffix in the annotation:
let pi = 3.14159 /*:: numeric const */;
```

A constant becomes a plpgsql `CONSTANT`, whose value is computed when the
function starts. So it can use literals, parameters and earlier constants, but
not another local variable: that would read the variable's initial value, not
the one assigned above it, and is rejected.

## Control flow

### if / else if / else
Expand Down
5 changes: 5 additions & 0 deletions doc/plxphp.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ A constant uses the `const` suffix in the annotation:
$pi = 3.14159 /*:: numeric const */;
```

A constant becomes a plpgsql `CONSTANT`, whose value is computed when the
function starts. So it can use literals, parameters and earlier constants, but
not another local variable: that would read the variable's initial value, not
the one assigned above it, and is rejected.

## Control flow

### if / elseif / else
Expand Down
5 changes: 5 additions & 0 deletions doc/plxpython3.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ A constant uses the `const` suffix on the annotation:
pi = 3.14159 #:: numeric const
```

A constant becomes a plpgsql `CONSTANT`, whose value is computed when the
function starts. So it can use literals, parameters and earlier constants, but
not another local variable: that would read the variable's initial value, not
the one assigned above it, and is rejected.

## Control flow

### if / elif / else
Expand Down
5 changes: 5 additions & 0 deletions doc/plxruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ A constant uses the `const` suffix on the annotation:
pi = 3.14159 #:: numeric const
```

A constant becomes a plpgsql `CONSTANT`, whose value is computed when the
function starts. So it can use literals, parameters and earlier constants, but
not another local variable: that would read the variable's initial value, not
the one assigned above it, and is rejected.

## Control flow

### if / elsif / else, unless
Expand Down
2 changes: 2 additions & 0 deletions src/plx_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ typedef struct PlxCtx
int exc_varlen;
int diag_mask; /* stacked-diagnostics fields used in this handler */
bool diag_msg; /* rewriting a RAISE message: interpolate NULL as '' */
int ninterp; /* string interpolations rendered so far; an
* interpolating initializer is not folded */
bool retset;
MemoryContext mcx; /* caller-supplied scratch context (reserved) */
} Ctx;
Expand Down
123 changes: 121 additions & 2 deletions src/plx_transpile.c
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@ emit_string_value(Ctx *cx, Tok *tk, StringInfo out)
need_e = false;
}
rw = plx_rewrite_expr(cx, exs, exl, false);
cx->ninterp++;
s = nx;
if (nparts++)
appendStringInfoString(out, " || ");
Expand Down Expand Up @@ -1501,6 +1502,102 @@ const_literal_type(Ctx *cx, int a, int b)
}
}

/*
* The first local variable, other than a constant declared before self, that
* the generated SQL expression text reads; NULL when there is none. String
* literals, quoted identifiers, field names (after '.') and type names (after
* '::') are skipped. Used to reject a constant whose initializer reads a
* variable: the initializer runs when the function starts, so it would see
* the variable's initial value, not the one the source shows.
*/
static const char *
first_local_ref(Ctx *cx, const char *s, PlxLocal2 *self)
{
char prev = 0; /* last significant character before a word */
bool after_cast = false;

while (*s)
{
CHECK_FOR_INTERRUPTS();
if (*s == '\'' || ((*s == 'E' || *s == 'e') && s[1] == '\''))
{
bool esc = (*s != '\'');

s += esc ? 2 : 1;
while (*s)
{
if (esc && *s == '\\' && s[1])
s += 2;
else if (*s == '\'' && s[1] == '\'')
s += 2;
else if (*s == '\'')
{
s++;
break;
}
else
s++;
}
prev = '\'';
after_cast = false;
continue;
}
if (*s == '"')
{
for (s++; *s; s++)
if (*s == '"')
{
if (s[1] != '"')
{
s++;
break;
}
s++;
}
prev = '"';
continue;
}
if ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || *s == '_' ||
IS_HIGHBIT_SET(*s))
{
const char *w = s;
PlxLocal2 *l;

while ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') ||
(*s >= '0' && *s <= '9') || *s == '_' || *s == '$' ||
IS_HIGHBIT_SET(*s))
s++;
if (prev != '.' && !after_cast &&
(l = plx_local_find(cx, w, (int) (s - w))) != NULL &&
l != self && !l->is_const)
return l->name;
prev = 'a';
after_cast = false;
continue;
}
if (*s == ':' && s[1] == ':')
{
after_cast = true;
s += 2;
continue;
}
if (*s >= '0' && *s <= '9')
{
while ((*s >= '0' && *s <= '9') || *s == '.' || *s == 'e' || *s == 'E')
s++;
prev = '0';
continue;
}
if (*s != ' ' && *s != '\t' && *s != '\n' && *s != '\r')
{
prev = *s;
after_cast = false;
}
s++;
}
return NULL;
}

/* ---------------------------------------------------------------- indent */

void
Expand Down Expand Up @@ -2441,18 +2538,40 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel)
plx_err(cx, t0->line, "constant \"%.*s\" requires a type annotation", t0->len, t0->s);
l->is_const = true;
l->init = plx_rewrite_expr(cx, rhs, (int) strlen(rhs), false);
{
const char *v = first_local_ref(cx, l->init, l);

if (v)
plx_err(cx, t0->line,
"constant \"%.*s\" cannot use local variable \"%s\": a constant's value is computed when the function starts",
t0->len, t0->s, v);
}
return;
}

/* fold: top-level, first assignment, constant literal RHS */
if (toplevel && firstseen && !compound && littyp && !l->is_record)
{
char *rhs = plx_span_text(cx, rhs_a, rhs_b);
int ninterp = cx->ninterp;
char *r = plx_rewrite_expr(cx, rhs, (int) strlen(rhs), false);

if (!l->typ)
l->typ = pstrdup(littyp);
l->init = plx_rewrite_expr(cx, rhs, (int) strlen(rhs), false);
return; /* statement folded into DECLARE; emit nothing */
/*
* A DECLARE initializer runs when the function starts, before
* the statements above this one. An interpolated string would
* read its values (or call its functions) then, so it is
* assigned here instead.
*/
if (cx->ninterp == ninterp)
{
l->init = r;
return; /* statement folded into DECLARE; emit nothing */
}
plx_indent(o, ind);
appendStringInfo(o, "%.*s := %s;\n", t0->len, t0->s, r);
return;
}
/* otherwise infer type if still unknown */
if (!l->typ && littyp)
Expand Down
18 changes: 18 additions & 0 deletions test/expected/plx_errors.out
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,21 @@ return 0xFFFFFFFFFFFFFFFFFF;
$$;
ERROR: plxjs: integer literal out of range: 0xFFFFFFFFFFFFFFFFFF
DETAIL: at plxjs source line 0
-- a constant's initializer runs when the function starts, so it cannot read a
-- local variable (it would see the initial value, not the one assigned above)
CREATE FUNCTION e_rb_const_local() RETURNS int LANGUAGE plxruby AS $$
v = 1
v = 2
c = v + 1 #:: int const
return c
$$;
ERROR: plxruby: constant "c" cannot use local variable "v": a constant's value is computed when the function starts
DETAIL: at plxruby source line 4
CREATE FUNCTION e_js_const_interp() RETURNS text LANGUAGE plxjs AS $$
let v = "a";
v = "b";
let c = `got ${v}` /*:: text const */;
return c;
$$;
ERROR: plxjs: constant "c" cannot use local variable "v": a constant's value is computed when the function starts
DETAIL: at plxjs source line 4
61 changes: 61 additions & 0 deletions test/expected/plx_features.out
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,64 @@ SELECT f_strict() AS strict_no_row;
-1
(1 row)

-- a first assignment from an interpolated string is not folded into DECLARE:
-- the initializer would run when the function starts and read the old value
CREATE FUNCTION f_fold_js() RETURNS text LANGUAGE plxjs AS $$
let v = "v0";
v = "v2";
let s = `got ${v}`;
return s;
$$;
CREATE FUNCTION f_fold_py() RETURNS text LANGUAGE plxpython3 AS $$
v = "v0"
v = "v2"
s = f"got {v}"
return s
$$;
CREATE FUNCTION f_fold_rb() RETURNS text LANGUAGE plxruby AS $$
v = "v0"
v = "v2"
s = "got #{v}"
return s
$$;
CREATE FUNCTION f_fold_php() RETURNS text LANGUAGE plxphp AS $$
$v = "v0";
$v = "v2";
$s = "got $v";
return $s;
$$;
SELECT f_fold_js(), f_fold_py(), f_fold_rb(), f_fold_php();
f_fold_js | f_fold_py | f_fold_rb | f_fold_php
-----------+-----------+-----------+------------
got v2 | got v2 | got v2 | got v2
(1 row)

-- a plain literal is still folded; a constant may use parameters and earlier
-- constants
CREATE FUNCTION f_fold_const(n int) RETURNS text LANGUAGE plxruby AS $$
k = 10 #:: int const
c = "n=#{n} k=#{k}" #:: text const
s = "plain"
return "#{c} #{s}"
$$;
SELECT f_fold_const(5);
f_fold_const
----------------
n=5 k=10 plain
(1 row)

SELECT regexp_replace(prosrc, '/\*plx-orig.*', '') FROM pg_proc WHERE proname = 'f_fold_const';
regexp_replace
---------------------------------------------------------------
/*plx:v1:plxruby:784cd38a*/ +
DECLARE +
k CONSTANT int := 10; +
c CONSTANT text := 'n=' || (n)::text || ' k=' || (k)::text;+
s text := 'plain'; +
BEGIN +
RETURN (c)::text || ' ' || (s)::text; +
END; +

(1 row)

DROP FUNCTION f_fold_js(), f_fold_py(), f_fold_rb(), f_fold_php(), f_fold_const(int);
15 changes: 15 additions & 0 deletions test/sql/plx_errors.sql
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,18 @@ END $outer$;
CREATE FUNCTION e_js_hex_overflow() RETURNS numeric LANGUAGE plxjs AS $$
return 0xFFFFFFFFFFFFFFFFFF;
$$;

-- a constant's initializer runs when the function starts, so it cannot read a
-- local variable (it would see the initial value, not the one assigned above)
CREATE FUNCTION e_rb_const_local() RETURNS int LANGUAGE plxruby AS $$
v = 1
v = 2
c = v + 1 #:: int const
return c
$$;
CREATE FUNCTION e_js_const_interp() RETURNS text LANGUAGE plxjs AS $$
let v = "a";
v = "b";
let c = `got ${v}` /*:: text const */;
return c;
$$;
39 changes: 39 additions & 0 deletions test/sql/plx_features.sql
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,42 @@ rescue => e
end
$$;
SELECT f_strict() AS strict_no_row;

-- a first assignment from an interpolated string is not folded into DECLARE:
-- the initializer would run when the function starts and read the old value
CREATE FUNCTION f_fold_js() RETURNS text LANGUAGE plxjs AS $$
let v = "v0";
v = "v2";
let s = `got ${v}`;
return s;
$$;
CREATE FUNCTION f_fold_py() RETURNS text LANGUAGE plxpython3 AS $$
v = "v0"
v = "v2"
s = f"got {v}"
return s
$$;
CREATE FUNCTION f_fold_rb() RETURNS text LANGUAGE plxruby AS $$
v = "v0"
v = "v2"
s = "got #{v}"
return s
$$;
CREATE FUNCTION f_fold_php() RETURNS text LANGUAGE plxphp AS $$
$v = "v0";
$v = "v2";
$s = "got $v";
return $s;
$$;
SELECT f_fold_js(), f_fold_py(), f_fold_rb(), f_fold_php();
-- a plain literal is still folded; a constant may use parameters and earlier
-- constants
CREATE FUNCTION f_fold_const(n int) RETURNS text LANGUAGE plxruby AS $$
k = 10 #:: int const
c = "n=#{n} k=#{k}" #:: text const
s = "plain"
return "#{c} #{s}"
$$;
SELECT f_fold_const(5);
SELECT regexp_replace(prosrc, '/\*plx-orig.*', '') FROM pg_proc WHERE proname = 'f_fold_const';
DROP FUNCTION f_fold_js(), f_fold_py(), f_fold_rb(), f_fold_php(), f_fold_const(int);