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

Extension version 2.1.0. Install the new build, then run
`ALTER EXTENSION plx UPDATE` in each database. The update changes no SQL
objects; it records the extension as non-relocatable (below). Run it straight
after installing the build: until it runs, the new build already writes plx's
schema into functions, but `ALTER EXTENSION plx SET SCHEMA` is still allowed and
would break them.

### Security

- **Generated code names plx's own objects with plx's schema.** A string
append (`s << x`, `$s .= x`, `s += x`, `STRING-APPEND`) declared the
variable as `plx_strbuild` and called `plx_sb_append(...)` unqualified, so
they were resolved through `search_path` every time the function ran. That
broke the standard hardening for a `SECURITY DEFINER` function
(`SET search_path = pg_catalog`: `type "plx_strbuild" does not exist`), broke
functions when plx was installed in a schema not on the path, and let a
same-named function planted earlier on the path run in their place, with the
definer's privileges for a `SECURITY DEFINER` function. Generated code now
writes `<plx schema>.plx_strbuild` and `<plx schema>.plx_sb_append`.

### Changed

- **The extension is no longer relocatable** (`relocatable = false`).
Functions now store plx's schema name, so `ALTER EXTENSION plx SET SCHEMA`
would leave them pointing at the old schema; it now fails with
`extension "plx" does not support SET SCHEMA`. Choose the schema at
`CREATE EXTENSION plx SCHEMA ...` time. Existing functions keep the
unqualified names they were generated with until their DDL is run again.
For the same reason, a function that uses a string append and is restored
into a database where plx is in a different schema (a `pg_dump -n` of one
schema, say) fails when it runs, with `schema "..." does not exist`: install
plx in the same schema in the target, or run the function's DDL again after
restoring. Functions without a string append are not affected.

## [2.0.1] - 2026-08-25

Packaging only. **The extension is unchanged at 2.0.0**, so there is no
Expand Down
4 changes: 2 additions & 2 deletions META.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"provides": {
"plx": {
"abstract": "Dialect-pluggable procedural language over plpgsql (Ruby, PHP, JavaScript, TypeScript, Python, Go, COBOL, Oracle PL/SQL, Transact-SQL)",
"file": "plx--2.0.0.sql",
"file": "plx--2.1.0.sql",
"docfile": "README.md",
"version": "2.0.0"
"version": "2.1.0"
}
},
"prereqs": {
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MODULE_big = plx
OBJS = src/plx_core.o src/plx_transpile.o src/plx_strbuild.o src/plx_dialect_ruby.o src/plx_dialect_php.o src/plx_dialect_js.o src/plx_dialect_python.o src/plx_dialect_cobol.o src/plx_dialect_plsql.o src/plx_dialect_ts.o src/plx_dialect_tsql.o src/plx_dialect_go.o src/plx_parse_brace.o

EXTENSION = plx
DATA = plx--1.0.sql plx--1.1.sql plx--1.1.1.sql plx--1.2.sql plx--1.2.1.sql plx--1.2.2.sql plx--1.3.0.sql plx--1.3.1.sql plx--1.3.2.sql plx--2.0.0.sql plx--1.0--1.1.sql plx--1.1--1.1.1.sql plx--1.1.1--1.2.sql plx--1.2--1.2.1.sql plx--1.2.1--1.2.2.sql plx--1.2.2--1.3.0.sql plx--1.3.0--1.3.1.sql plx--1.3.1--1.3.2.sql plx--1.3.2--2.0.0.sql
DATA = plx--1.0.sql plx--1.1.sql plx--1.1.1.sql plx--1.2.sql plx--1.2.1.sql plx--1.2.2.sql plx--1.3.0.sql plx--1.3.1.sql plx--1.3.2.sql plx--2.0.0.sql plx--2.1.0.sql plx--1.0--1.1.sql plx--1.1--1.1.1.sql plx--1.1.1--1.2.sql plx--1.2--1.2.1.sql plx--1.2.1--1.2.2.sql plx--1.2.2--1.3.0.sql plx--1.3.0--1.3.1.sql plx--1.3.1--1.3.2.sql plx--1.3.2--2.0.0.sql plx--2.0.0--2.1.0.sql

# pg_regress suite (make installcheck). test/run_corpus.py is an additional
# Ruby corpus runner, and test/differential.py checks each dialect against a
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ Then, in a database:
CREATE EXTENSION plx;
```

plx cannot be moved to another schema once created, so choose the schema at
`CREATE EXTENSION plx SCHEMA ...` time. A function that appends to a string
(`s << x`, `$s .= x`, `s += x`, `STRING-APPEND`) stores plx's schema name in its
generated code. If such a function is restored into a database where plx is in
a different schema (from a `pg_dump -n` of one schema, say), it fails when it
runs with `schema "..." does not exist`. Install plx in the same schema in the
target database, or run the function's DDL again there; its original source
can be recovered from the function itself (see
[DEBUGGING.md](doc/DEBUGGING.md#recover-your-original-source)).

## Documentation

- Per-dialect chapters: [plxruby](doc/plxruby.md), [plxphp](doc/plxphp.md),
Expand Down
17 changes: 17 additions & 0 deletions plx--2.0.0--2.1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* plx 2.0.0 -> 2.1.0: no catalog changes.
*
* 2.1.0 makes the extension non-relocatable (relocatable = false in
* plx.control). Generated code now names the extension's own objects
* (plx_strbuild, plx_sb_append) qualified with the schema plx is installed in,
* so a function with its own SET search_path still finds them and an object of
* the same name earlier on the caller's path cannot stand in for them. Moving
* the extension with ALTER EXTENSION ... SET SCHEMA would leave those stored
* names pointing at the old schema, so it is no longer allowed.
*
* PostgreSQL records relocatability in pg_extension when the extension is
* created or updated, which is why this otherwise empty update exists: running
* it is what records the new setting for an existing installation.
*
* Functions already in the catalog keep the unqualified names they were
* generated with; they pick up the qualified names the next time their DDL is
* run. See the 2.1.0 entry in CHANGELOG.md. */
209 changes: 209 additions & 0 deletions plx--2.1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/* plx 2.1.0: registers the plx dialect languages and the string-builder type */

\echo Use "CREATE EXTENSION plx" to load this file. \quit

/*
* plx_strbuild: an expanded-object string builder. Its flattened form is a
* text-compatible varlena, so it casts to and from text without a function.
* plx_sb_append grows the buffer in place across a loop (amortized O(1)),
* avoiding the quadratic cost of `s := s || 'x'` on plain text.
*/
CREATE TYPE plx_strbuild;

CREATE FUNCTION plx_sb_in(cstring) RETURNS plx_strbuild
AS '$libdir/plx', 'plx_sb_in' LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION plx_sb_out(plx_strbuild) RETURNS cstring
AS '$libdir/plx', 'plx_sb_out' LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE plx_strbuild (
INPUT = plx_sb_in,
OUTPUT = plx_sb_out,
LIKE = text,
STORAGE = extended
);

CREATE FUNCTION plx_sb_append_support(internal) RETURNS internal
AS '$libdir/plx', 'plx_sb_append_support' LANGUAGE C;

CREATE FUNCTION plx_sb_append(plx_strbuild, text) RETURNS plx_strbuild
AS '$libdir/plx', 'plx_sb_append' LANGUAGE C IMMUTABLE
SUPPORT plx_sb_append_support;

-- plx_strbuild is implicitly usable as text (so it works in RETURN, string
-- functions, and concatenation). text becomes a builder only in assignment
-- context, which avoids operator ambiguity such as `builder || text`.
CREATE CAST (plx_strbuild AS text) WITHOUT FUNCTION AS IMPLICIT;
CREATE CAST (text AS plx_strbuild) WITHOUT FUNCTION AS ASSIGNMENT;

/*
* Runtime call handler = plpgsql's OWN handler. Binding a fresh pg_proc row to
* plpgsql.so's exported symbol is legal (CreateProceduralLanguage only checks
* the handler RETURNS language_handler). Execution is therefore 100% stock
* plpgsql; plx contributes zero call-path C.
*/
CREATE FUNCTION plx_call_handler()
RETURNS language_handler
AS '$libdir/plpgsql', 'plpgsql_call_handler'
LANGUAGE C;

/* Ruby dialect validator + inline handler live in plx.so */
CREATE FUNCTION plx_ruby_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_ruby_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_ruby_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_ruby_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxruby
HANDLER plx_call_handler
INLINE plx_ruby_inline_handler
VALIDATOR plx_ruby_validator;

COMMENT ON LANGUAGE plxruby IS 'plx Ruby dialect (transpiles to plpgsql)';

/* PHP dialect */
CREATE FUNCTION plx_php_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_php_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_php_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_php_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxphp
HANDLER plx_call_handler
INLINE plx_php_inline_handler
VALIDATOR plx_php_validator;

COMMENT ON LANGUAGE plxphp IS 'plx PHP dialect (transpiles to plpgsql)';

/* JavaScript dialect */
CREATE FUNCTION plx_js_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_js_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_js_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_js_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxjs
HANDLER plx_call_handler
INLINE plx_js_inline_handler
VALIDATOR plx_js_validator;

COMMENT ON LANGUAGE plxjs IS 'plx JavaScript dialect (transpiles to plpgsql)';

/* Python dialect */
CREATE FUNCTION plx_py_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_py_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_py_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_py_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxpython3
HANDLER plx_call_handler
INLINE plx_py_inline_handler
VALIDATOR plx_py_validator;

COMMENT ON LANGUAGE plxpython3 IS 'plx Python dialect (transpiles to plpgsql)';

/* COBOL dialect (ISO/IEC 1989:2023) validator + inline handler live in plx.so */
CREATE FUNCTION plx_cob_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_cob_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_cob_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_cob_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxcobol
HANDLER plx_call_handler
INLINE plx_cob_inline_handler
VALIDATOR plx_cob_validator;

COMMENT ON LANGUAGE plxcobol IS 'plx COBOL dialect (ISO/IEC 1989:2023, transpiles to plpgsql)';

/* PL/SQL dialect (Oracle) validator + inline handler live in plx.so */
CREATE FUNCTION plx_plsql_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_plsql_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_plsql_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_plsql_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxplsql
HANDLER plx_call_handler
INLINE plx_plsql_inline_handler
VALIDATOR plx_plsql_validator;

COMMENT ON LANGUAGE plxplsql IS 'plx Oracle PL/SQL dialect (transpiles to plpgsql)';

/* TypeScript dialect validator + inline handler live in plx.so */
CREATE FUNCTION plx_ts_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_ts_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_ts_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_ts_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxts
HANDLER plx_call_handler
INLINE plx_ts_inline_handler
VALIDATOR plx_ts_validator;

COMMENT ON LANGUAGE plxts IS 'plx TypeScript dialect (transpiles to plpgsql)';

/* T-SQL dialect (Transact-SQL) validator + inline handler live in plx.so */
CREATE FUNCTION plx_tsql_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_tsql_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_tsql_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_tsql_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxtsql
HANDLER plx_call_handler
INLINE plx_tsql_inline_handler
VALIDATOR plx_tsql_validator;

COMMENT ON LANGUAGE plxtsql IS 'plx Transact-SQL (SQL Server) dialect (transpiles to plpgsql)';

/* Go dialect validator + inline handler live in plx.so */
CREATE FUNCTION plx_go_validator(oid)
RETURNS void
AS '$libdir/plx', 'plx_go_validator'
LANGUAGE C STRICT;

CREATE FUNCTION plx_go_inline_handler(internal)
RETURNS void
AS '$libdir/plx', 'plx_go_inline_handler'
LANGUAGE C;

CREATE TRUSTED LANGUAGE plxgo
HANDLER plx_call_handler
INLINE plx_go_inline_handler
VALIDATOR plx_go_validator;

COMMENT ON LANGUAGE plxgo IS 'plx Go dialect (transpiles to plpgsql)';
4 changes: 2 additions & 2 deletions plx.control
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
comment = 'plx (plExcellent): dialect-pluggable procedural language over plpgsql'
default_version = '2.0.0'
default_version = '2.1.0'
module_pathname = '$libdir/plx'
relocatable = true
relocatable = false
8 changes: 8 additions & 0 deletions src/plx.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ extern Datum plx_generic_inline_handler(FunctionCallInfo fcinfo, const PlxDialec
/* True if prosrc already carries the plx sentinel (already transpiled). */
extern bool plx_has_sentinel(const char *prosrc);

/*
* Schema the plx extension is installed in, set by the validator and inline
* handler before they transpile. Generated code names the extension's own
* objects (plx_strbuild, plx_sb_append) qualified with it, so they resolve
* the same under any search_path. InvalidOid means "emit unqualified".
*/
extern Oid plx_ext_namespace;

/* (plx_transpile + PlxSurface live in plx_int.h, shared with the transpiler.) */

#endif /* PLX_H */
6 changes: 6 additions & 0 deletions src/plx_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "commands/proclang.h"
#include "nodes/parsenodes.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/syscache.h"

Expand All @@ -36,6 +37,8 @@ PG_MODULE_MAGIC;

#define PLX_MAX_DIALECTS 32
static const PlxDialect *plx_dialects[PLX_MAX_DIALECTS];

Oid plx_ext_namespace = InvalidOid;
static int plx_ndialects = 0;

void
Expand Down Expand Up @@ -126,6 +129,8 @@ plx_generic_validator(FunctionCallInfo fcinfo, const PlxDialect *d)
bool repl[Natts_pg_proc];

build_meta(&meta, tuple, funcoid);
/* the validator is an extension member, so it lives in our schema */
plx_ext_namespace = get_func_namespace(fcinfo->flinfo->fn_oid);
plpgsql = d->transpile(rubysrc, &meta);

memset(nulls, false, sizeof(nulls));
Expand Down Expand Up @@ -162,6 +167,7 @@ plx_generic_inline_handler(FunctionCallInfo fcinfo, const PlxDialect *d)
Form_pg_language langform;
Oid inline_oid;

plx_ext_namespace = get_func_namespace(fcinfo->flinfo->fn_oid);
transpiled = d->transpile(codeblock->source_text, NULL);

plpgsql_lang = get_language_oid("plpgsql", false);
Expand Down
6 changes: 3 additions & 3 deletions src/plx_dialect_cobol.c
Original file line number Diff line number Diff line change
Expand Up @@ -2089,10 +2089,10 @@ cob_string_append(Cb *cb, int ind)
cb->pos++;
l = plx_local_find(cb->cx, tgt, strlen(tgt));
if (l && !l->is_record)
l->typ = pstrdup("plx_strbuild"); /* the accumulator becomes a builder */
l->typ = plx_ext_object("plx_strbuild"); /* the accumulator becomes a builder */
plx_indent(&cb->cx->out, ind);
appendStringInfo(&cb->cx->out, "%s := plx_sb_append(%s, (%s )::text);\n",
tgt, tgt, v.data);
appendStringInfo(&cb->cx->out, "%s := %s(%s, (%s )::text);\n",
tgt, plx_ext_object("plx_sb_append"), tgt, v.data);
}

static void
Expand Down
1 change: 1 addition & 0 deletions src/plx_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ int plx_stmt_end(Ctx *cx, int from);
void plx_indent(StringInfo o, int n);
PlxLocal2 *plx_local_add(Ctx *cx, const char *name, int len);
PlxLocal2 *plx_local_find(Ctx *cx, const char *name, int len);
char *plx_ext_object(const char *name);
bool plx_is_param(Ctx *cx, const char *name, int len);

/* ---------------------------------------------- dialect front ends (vtable) */
Expand Down
Loading