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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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`).
version in `plx.control` (currently `2.0.0`).

## [2.0.1] - 2026-08-25

Expand Down Expand Up @@ -90,6 +90,10 @@ Each result interpolates a value that will become NULL when the value is NULL.
Where the old rendering was wanted, make it explicit with `coalesce(x, '')` in
the body.

The same applies to a dump taken from an earlier release and restored into
2.0.0: the restored functions keep the plpgsql stored in the dump, including
the empty-string rendering, until their DDL is run again.

## [1.3.2] - 2026-08-24

Code-only patch release (no catalog changes) carrying a plxgo fix for
Expand Down
65 changes: 49 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ is plpgsql. See [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md) and

## Example

One function, written in three dialects, each producing the same plpgsql:
One function, written in three dialects, each producing equivalent plpgsql:

```sql
CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxruby AS $$
CREATE FUNCTION grade_rb(score int) RETURNS text LANGUAGE plxruby AS $$
grade #:: text
if score >= 90
grade = "A"
Expand All @@ -121,14 +121,14 @@ CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxruby AS $$
return grade
$$;

CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxphp AS $$
CREATE FUNCTION grade_php(score int) RETURNS text LANGUAGE plxphp AS $$
if ($score >= 90) { $grade = "A"; }
elseif ($score >= 80) { $grade = "B"; }
else { $grade = "F"; }
return $grade;
$$;

CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxjs AS $$
CREATE FUNCTION grade_js(score int) RETURNS text LANGUAGE plxjs AS $$
let grade = "F";
if (score >= 90) { grade = "A"; }
else if (score >= 80) { grade = "B"; }
Expand All @@ -137,26 +137,41 @@ CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxjs AS $$
$$;
```

The stored plpgsql (in `pg_proc.prosrc`) for each is:
The stored plpgsql (in `pg_proc.prosrc`) for the plxruby and plxphp versions is
shown below. The plxjs version is the same except that it initializes `grade`
in its `DECLARE` (`grade text := 'F';`).

```plpgsql
/*plx:v1:plxruby:b04416a1*/
DECLARE
grade text;
BEGIN
IF score >= 90 THEN grade := 'A';
ELSIF score >= 80 THEN grade := 'B';
ELSE grade := 'F';
IF score >= 90 THEN
grade := 'A';
ELSIF score >= 80 THEN
grade := 'B';
ELSE
grade := 'F';
END IF;
RETURN grade;
END;
/*plx-orig:b64$...$plx-orig*/
```

The first line is a sentinel naming the dialect, and the last line is a
comment carrying your original source in base64, so it survives dump and
restore and can be recovered (see [doc/DEBUGGING.md](doc/DEBUGGING.md)).
Everything between them is the plpgsql that runs.

## Performance

Because functions execute as plpgsql, the plx dialects match plpgsql (within about
11 percent across five workloads) and inherit its performance profile: several
times faster than the embedded-interpreter PLs on row iteration, and competitive
on arithmetic, branching, and call overhead.
Because functions execute as plpgsql, the plx dialects match plpgsql within
about 10 percent on most workloads (the worst cases measured are plxruby row
iteration at 1.17x and plxcobol's `EVALUATE` branch at 1.32x) and inherit its
performance profile: several times faster than the embedded-interpreter PLs on
row iteration, fastest on call overhead, and between the embedded PLs on
arithmetic. On a pure-CPU branching loop the embedded PLs are faster (native
PL/PHP runs it in 0.40x of plpgsql's time).

The one place plpgsql is weak is building a string in a loop: `s := s || 'x'` is
O(n^2), because text is immutable and each step copies the whole string. plx
Expand Down Expand Up @@ -185,12 +200,23 @@ make PG_CONFIG=/path/to/pg_config
make install PG_CONFIG=/path/to/pg_config
```

Then, in a database:
Then, in a database (installing the extension requires a superuser):

```sql
CREATE EXTENSION plx;
```

To upgrade, install the new build and run, in each database:

```sql
ALTER EXTENSION plx UPDATE;
```

Upgrading changes no existing function. plx transpiles at `CREATE FUNCTION`
time and stores the result, so a function keeps the plpgsql it was created with
until its DDL is run again. See [CHANGELOG.md](CHANGELOG.md) for the notes on
each release.

## Documentation

- Per-dialect chapters: [plxruby](doc/plxruby.md), [plxphp](doc/plxphp.md),
Expand Down Expand Up @@ -218,7 +244,8 @@ CREATE EXTENSION plx;
## Layout

```
plx.control, plx--1.0.sql extension control and install SQL
plx.control, plx--2.0.0.sql extension control and install SQL
plx--X--Y.sql upgrade scripts between releases
Makefile PGXS build
src/ C sources (transpiler, dialect surfaces, PL handler)
doc/ per-dialect chapters, architecture, transpiler, parity
Expand All @@ -231,11 +258,17 @@ bench/ benchmark harness and results

```
make installcheck # pg_regress suite
make differentialcheck # each dialect against a plpgsql reference
make metacheck # META.json against the PGXN spec (no server)
python3 test/run_corpus.py # additional Ruby corpus runner
python3 test/fuzz.py # transpiler fuzzer
```

`make installcheck` runs against an installed plx on a running server. The
expected output is in `test/expected/`.
`make installcheck` and `make differentialcheck` run against an installed plx on
a running server; run `differentialcheck` after `installcheck`, which creates
the `contrib_regression` database it uses. The expected output is in
`test/expected/`. CI runs installcheck, differentialcheck, and metacheck on
PostgreSQL 13 through 18.

## License

Expand Down
29 changes: 16 additions & 13 deletions bench/BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ plx uses plx-prefixed language names, so it coexists with the native plruby,
plphp, plperl, and plpython3u.

plxruby, plxphp, plxjs, plxpython3, and plxcobol transpile to plpgsql at
`CREATE FUNCTION` time, so at run time they execute as plpgsql. plperl and
`CREATE FUNCTION` time, so at run time they execute as plpgsql. plxts, plxgo,
plxplsql, and plxtsql were not benchmarked; they also execute as plpgsql. plperl and
plpython3u run their own embedded interpreters and retrieve rows through SPI.

## Results
Expand Down Expand Up @@ -56,7 +57,7 @@ In the strbuild column, native plpgsql is the `s := s || 'x'` baseline and the
plx dialects use the builder. This is the intended native-versus-plx comparison:
the same accumulation idiom is 1743 ms in stock plpgsql and about 10 ms in the
plx dialects. The plx builder is also faster than the native PLs' own in-language
append on this workload (native plruby 53 ms, native plphp 14 ms), because it
append on this workload (native plruby 53 ms, native plphp 15 ms), because it
mutates one expanded-object buffer in place rather than materializing an
interpreter string per step. These numbers are on PostgreSQL 18; the builder's
amortized-O(1) append requires PostgreSQL 18 (see the version note under String
Expand All @@ -65,9 +66,9 @@ building). On PostgreSQL 13 to 17 the plx strbuild column would match plpgsql

## Analysis

- The plx dialects match plpgsql within about 10 percent on the arith, iter, and
call workloads, because the stored function body is plpgsql. There is no
run-time translation cost; the translation happens once at `CREATE FUNCTION`.
- The plx dialects match plpgsql within about 10 percent on the arith and call
workloads, and within 17 percent on iter, because the stored function body is
plpgsql. There is no run-time translation cost; the translation happens once at `CREATE FUNCTION`.
The small spread among the dialects is measurement noise.

- plxcobol (branch): the one place plxcobol trails is the four-way branch
Expand All @@ -78,7 +79,7 @@ building). On PostgreSQL 13 to 17 the plx strbuild column would match plpgsql
counting loop lowers to an integer `FOR`, and `STRING-APPEND` lowers to the
string builder).

- Row iteration (iter): plpgsql, and therefore the plx dialects, are 2.8x to 6.0x
- Row iteration (iter): plpgsql, and therefore the plx dialects, are 2.7x to 5.6x
faster than the embedded PLs (plperl, plpython3u, native plruby, native plphp).
plpgsql streams rows through a cursor and reads columns directly, while the
embedded interpreters copy each row into their own data structures.
Expand All @@ -91,21 +92,23 @@ building). On PostgreSQL 13 to 17 the plx strbuild column would match plpgsql
amortized O(1), and the transpiler lowers the dialect append operators
(`<<`, `.=`, `+=` on a string) onto it. The result is about 170x faster than
the stock plpgsql idiom, and faster than the native PLs building the same
string with their own append (native plruby 53 ms, native plphp 14 ms), which
string with their own append (native plruby 53 ms, native plphp 15 ms), which
each materialize a fresh interpreter string per step. In hand-written plpgsql
the same win is available by calling `plx_sb_append`, or by assembling text in
SQL with `string_agg` over a set.

- Arithmetic (arith): the embedded interpreters vary. plperl is fastest (native
integer arithmetic), plpython3u is slowest, and plpgsql with the plx dialects
sits between.
- Arithmetic (arith): the embedded interpreters vary. plperl is fastest (0.79x,
native integer arithmetic), native plruby is slowest (1.79x) followed by
plpython3u (1.31x), and plpgsql with the plx dialects sits between.

- Branching (branch): plpython3u is faster on this pure-CPU integer workload
(0.72x); plperl and plpgsql with the plx dialects are close to each other.
- Branching (branch): on this pure-CPU integer workload the embedded PLs are
faster: native plphp (0.40x), plpython3u (0.68x), native plruby (0.90x) and
plperl (0.95x). plpgsql and the plx dialects are at about 1.0x (plxcobol's
`EVALUATE` at 1.32x).

- Call overhead (call): plpgsql and the plx dialects are fastest. The embedded
PLs pay an SPI round trip per call and are slower even at one tenth the
iteration count (native plruby is the slowest here, at 2.97x).
iteration count (native plruby is the slowest here, at 3.03x).

The transpile-to-plpgsql approach gives the plx dialects the performance profile
of plpgsql: strong on set-oriented and SQL-bound work, competitive on procedural
Expand Down
18 changes: 12 additions & 6 deletions doc/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ dialect syntax and emits plpgsql; the execution engine is plpgsql.

## Catalog wiring

For each dialect, `plx--1.0.sql` creates a language whose parts are:
For each dialect, the install script (`plx--2.0.0.sql` for the current release)
creates a language whose parts are:

- `HANDLER` is `plx_call_handler`, which is bound to plpgsql's exported
`plpgsql_call_handler` symbol (`AS '$libdir/plpgsql', 'plpgsql_call_handler'`).
Expand Down Expand Up @@ -120,7 +121,8 @@ per-dialect branching in the driver.
## Files

```
plx.control, plx--1.0.sql extension control and install SQL
plx.control, plx--2.0.0.sql extension control and install SQL
plx--X--Y.sql upgrade scripts between releases
src/plx.h public ABI (PlxDialect, PlxFuncMeta)
src/plx_int.h internal ABI (PlxSurface, canonical keywords)
src/plx_engine.h engine interface: PlxCtx, tokens, symtab, and the
Expand Down Expand Up @@ -151,12 +153,15 @@ is a new `plx_dialect_X.c` (a surface with its `parse_body`, plus a few

The plx languages are declared `TRUSTED`. A plx function executes as plpgsql
through plpgsql's call handler, so it can do what a plpgsql function can do and
nothing more: no filesystem, no network, no arbitrary native code, and all SQL
runs with the caller's privileges. plx embeds no language runtime, which is the
nothing more: no filesystem, no network, no arbitrary native code, and SQL runs
with the caller's privileges (or the owner's, for a `SECURITY DEFINER`
function), exactly as in plpgsql. plx embeds no language runtime, which is the
difference from the native PL/Ruby and PL/PHP, which are untrusted because they
load a full interpreter into the backend. The transpiler is C that parses
untrusted input at `CREATE FUNCTION` time; it has a recursion-depth limit and is
fuzzed (see `test/fuzz.py`).
fuzzed (see `test/fuzz.py`). See the user guide's
[Trust and security](USERGUIDE.md#trust-and-security) section for who can use
the languages and how to restrict them.

## Testing

Expand Down Expand Up @@ -193,7 +198,8 @@ instead of passing over it.

## Related documents

- [TRANSPILER.md](TRANSPILER.md): the original transpiler design specification.
- [TRANSPILER.md](TRANSPILER.md): the original transpiler design specification
(historical; the dialect chapters are authoritative where they differ).
- [PARITY.md](PARITY.md): the plpgsql construct parity matrix.
- The per-dialect chapters: [plxruby](plxruby.md), [plxphp](plxphp.md),
[plxjs](plxjs.md), [plxts](plxts.md), [plxpython3](plxpython3.md),
Expand Down
17 changes: 11 additions & 6 deletions doc/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
## Supported versions

plx supports PostgreSQL 13 through 18 (released), and builds and passes on the
19 and 20 development lines. The full pg_regress suite (plxruby, plxphp, plxjs,
plxpython3, plxgo, plxcobol, plxplsql, plxts, plxtsql, and the rejection tests)
passes on each of PostgreSQL 13 through 18, plus 19 and 20 built from source.
19 and 20 development lines. The full pg_regress suite (one file per dialect
plus plx_features, plx_output, plx_strbuild, and plx_errors) passes on each of
PostgreSQL 13 through 18. CI (`.github/workflows/ci.yml`) runs `make
installcheck`, `make differentialcheck`, and `make metacheck` on PostgreSQL 13
through 18 from the PGDG packages on every push and pull request. The 19 and 20
results were checked manually against source builds and are not part of CI.

| PostgreSQL | Status | string builder |
|------------|--------|----------------|
Expand All @@ -15,8 +18,8 @@ passes on each of PostgreSQL 13 through 18, plus 19 and 20 built from source.
| 16 | pass | correct, not accelerated |
| 17 | pass | correct, not accelerated |
| 18 | pass | accelerated (amortized O(1)) |
| 19 (beta) | pass (from source) | accelerated (amortized O(1)) |
| 20 (devel) | pass (from source) | accelerated (amortized O(1)) |
| 19 (beta) | pass (manual, from source) | accelerated (amortized O(1)) |
| 20 (devel) | pass (manual, from source) | accelerated (amortized O(1)) |

PostgreSQL 19 and 20 compile with a C23 toolchain (for example gcc 15), where
`pg_noreturn` becomes the standard `[[noreturn]]` attribute; plx writes it as the
Expand Down Expand Up @@ -59,7 +62,9 @@ Two server APIs differ across the supported majors; plx handles both:

## Reproducing

`test/pg_matrix.sh` builds PostgreSQL 13 to 17 from source, builds plx against
CI is the reference: `.github/workflows/ci.yml` installs each PostgreSQL major
from PGDG, builds plx, and runs the suites listed above. To reproduce locally
from source, `test/pg_matrix.sh` builds PostgreSQL 13 to 17 from source, builds plx against
each `pg_config`, and runs the regression suite per version. Run it as root in a
build environment with the standard PostgreSQL build dependencies installed.

Expand Down
2 changes: 1 addition & 1 deletion doc/DEBUGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Then:
SELECT plx_source('my_func(int)');
```

returns exactly the Ruby / PHP / JavaScript / Python / COBOL body you wrote.
returns exactly the dialect body you wrote.

## Why the error line differs from your source line

Expand Down
Loading
Loading