Contents
Changelog
All notable changes to plx are recorded here. The format follows
Keep a Changelog, and plx uses the extension
version in plx.control (currently 1.0).
[2.0.1] - 2026-08-25
Packaging only. The extension is unchanged at 2.0.0, so there is no
ALTER EXTENSION plx UPDATE for this release and nothing to install if you are
already on 2.0.0. Only the distribution metadata changed, which is why the
distribution version moved and the extension version did not.
Fixed
META.jsonlisted the tagpl/sql, which PGXN rejects: a Tag may not contain a slash. It is nowplsql. This was found only when PGXN refused the 2.0.0 upload, since the check happens at upload time, after a release has been tagged and published.
Added
make metacheck(test/check_meta.py) validatesMETA.jsonagainst the PGXN Meta Specification v1 before a release rather than at upload time, and runs in CI. It reproduces the rejection above.Worth recording, because the prose and the schema disagree and the prose is the trap: the spec text says a Tag may contain no “slash, backslash, control, or space” characters, but that sentence describes a Term. The Tag schema is
^[^/\\\p{Cntrl}]{2,}$, which permits spaces. Sosql serveris a valid tag and an invalid term, and only the slash inpl/sqlwas ever a problem.
[2.0.0] - 2026-08-24
Major release for one behaviour change: interpolating a NULL now propagates it
instead of rendering an empty string. Upgrade with
ALTER EXTENSION plx UPDATE TO '2.0.0' after installing the new module.
Changed
- Interpolating a NULL propagates it. An interpolated value is concatenated
as-is, so the whole string becomes NULL the way SQL
||does. Previously every interpolated value was wrapped inCOALESCE((x)::text, ''), so a missing value silently became a plausible-looking string:
-- return "#{country}-#{region}-#{code}" with a NULL region
-- before: 'US--123' a string that inserts, indexes and joins like data
-- now: NULL
This affects plxruby, plxphp, plxjs, plxts, plxpython3 and
plxgo. plxplsql and plxtsql already propagated and are unchanged.
plxcobol builds strings through the plx_strbuild accumulator, whose
append treats a NULL as nothing to append by design, and is unchanged.
A message built for RAISE is the exception and keeps each interpolated
value’s empty-string fallback, so one NULL cannot swallow a diagnostic and
the literal text of the message survives.
plxgo:fmt.Sprintfis lowered to a SQL concatenation rather thanformat(), which is what allows the propagation, sinceformat()renders a NULL operand as empty and cannot be made to do otherwise. Observable output is otherwise unchanged: verbs render their operand as text, a-flag and a width still pad (now throughrpad/lpad), other flags and the precision field are still dropped, and%%is still a literal percent.
Upgrading
ALTER EXTENSION plx UPDATE TO '2.0.0' does not change any function that
already exists. plx transpiles at CREATE FUNCTION time and stores the result
in pg_proc.prosrc, so a function in the catalog keeps the plpgsql it was
created with. The new behaviour arrives the next time that function’s DDL is
run, which for most installations means the next deployment.
To find the functions a redeployment will change:
SELECT p.oid::regprocedure
FROM pg_proc p JOIN pg_language l ON l.oid = p.prolang
WHERE l.lanname LIKE 'plx%'
AND EXISTS (SELECT 1
FROM regexp_split_to_table(p.prosrc, E'\n') AS ln
WHERE ln LIKE '%COALESCE((%)::text, ''''%'
AND ln NOT LIKE '%RAISE %')
ORDER BY 1;
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.
[1.3.2] - 2026-08-24
Code-only patch release (no catalog changes) carrying a plxgo fix for
fmt.Sprintf and the differential check that found it. Upgrade with
ALTER EXTENSION plx UPDATE TO '1.3.2' after installing the new module.
Fixed
plxgo:fmt.Sprintfin expression position passed its Go format string straight into SQLformat(), so any verb other than%sraisedunrecognized format() type specifierwhen the function was called. The transpile succeeded and the failure only appeared at run time, which madefmt.Sprintf("%d", n), the ordinary way to format an integer in Go, produce a function that could not run. Go’s verbs are now rewritten to the%sthatformat()understands, keeping a-flag and a width and dropping the flags and precisionformat()has no equivalent for. A%that starts no directive is escaped rather than passed through, since a lone%is itself an error toformat(). Found by the new differential check.Note that the verbs which change an operand’s representation rather than its padding render what
%srenders, sofmt.Sprintf("%x", 255)yields255and notff. Convert explicitly where the representation matters. This is recorded indoc/plxgo.mdanddoc/LIMITATIONS.md.
Added
make differentialcheck(test/differential.py): each case is one program written as a plpgsql reference and once per dialect, called with the same arguments and required to agree with the reference on both values and error SQLSTATEs. Intended divergences are recorded per case with a reason and reported separately, and the check fails if a recorded divergence stops happening, so a documented limitation cannot outlive its documentation.doc/MIGRATION.md, a migration-led page comparing plx against rewriting by hand, an embedded PL,ora2pg, and leaving the logic in the application, including how to leave plx while keeping the generated plpgsql.
Changed
- Internal refactor of the transpiler behind a
PlxSurface.parse_bodyvtable (#2). The single ~11k-linesrc/plx_transpile.cis split into a dialect-neutral engine (declared in the newsrc/plx_engine.h) plus per-dialect front ends insrc/plx_dialect_*.cand the sharedsrc/plx_parse_brace.c; the hardcodedblock_styledispatch is replaced by a per-dialectparse_bodyfunction pointer. No functional change; generated plpgsql is byte-identical and all regression tests pass. Adding a dialect no longer touches shared code. - Follow-up to the above: relocated the dialect-neutral
plx_diag_prefix()helper out ofsrc/plx_dialect_ruby.cand back into the engine (src/plx_transpile.c), where its other callers (Python and the brace front end) already live. No functional change. - Tightened linkage and removed dead code left by the refactor: the six
single-dialect
plx_*_parse_bodyfront ends are nowstatic(only the sharedplx_brace_parse_bodykeeps a prototype inplx_engine.h); dropped the write-onlyCtx.ntfield; and corrected a stale COBOL comment about a keyword table the surface does not carry. No functional change.
[1.3.1] - 2026-07-16
Code-only patch release (no catalog changes) carrying the memory-safety and
robustness fixes from the full-repo transpiler audit (#1). Upgrade with
ALTER EXTENSION plx UPDATE TO '1.3.1' after installing the new module.
Fixed
- Missing capacity guard on the trailing
T_EOFtoken write inlex(): a source that lexed to exactlycap-1tokens overflowed the token array by one (heap overflow / SIGSEGV). All token writes are now guarded. plx_strbuildsb_ensure()doubled its capacity inint32, which could wrap negative near ~1GB and spin forever while passing a bogus size torepalloc. Growth is now computed in 64-bit and clamped toMaxAllocSize, so an oversized request raises the standard allocation error cleanly.- The recursion-guarded transpiler entry points now call
check_stack_depth(), so deeply nested hostile input raises a clean error honoringmax_stack_depthinstead of overflowing the C stack and crashing the backend. - The Python lexer raises a clean “indentation nested too deeply” error at the
indent-stack limit instead of emitting an unbalanced
T_INDENTthat mis-nested every enclosing block. - Raw single-quoted strings in Ruby and PHP keep backslashes literal (
'\n','C:\temp'); only\\and\'are special. Double-quoted and Python/JS single-quoted escapes are unchanged. - PHP
${name}curly interpolation in double-quoted strings is now recognized (was emitted verbatim). - Non-decimal integer literals (
0x/0o/0b, with_group separators) are lexed as a single token and rewritten to decimal, keeping generated plpgsql portable to PG13-15 (which accept0x/0o/0bonly on PG16+). A literal that overflows 64 bits raises a clean “integer literal out of range” error rather than emitting invalid SQL.
Other
- Declare the built-in dialect descriptors in
plx.h, clearing-Wmissing-variable-declarationsacross the dialect translation units.
[1.3.0] - 2026-07-15
Trigger row mutation across dialects, plus the cookbook and limitations
documentation set. Upgrade with ALTER EXTENSION plx UPDATE TO '1.3.0' after
installing the new module (no catalog changes).
Added
- plxtsql: a trigger can now assign to
NEWfields withSET NEW.col = e, which lowers toNEW.col := e, so a Transact-SQL trigger can rewrite the row and not only validate it. ASETwhose target is a qualified name with a top-level=is an assignment;SET NOCOUNT ONand other session options are still ignored. Qualified names also emit without a stray space around the dot. Covered by a new plxtsql trigger regression test. - A verified cookbook for each of the nine dialects (
doc/cookbook/) and a consolidated gaps-and-limitations page (doc/LIMITATIONS.md), both on the documentation site.
With the plxphp arrow assignment (1.2.2) and this plxtsql SET NEW.col, assigning
to a trigger’s NEW fields is supported across the dialects in the dialect’s own
idiom. See each dialect’s cookbook trigger recipe.
[1.2.2] - 2026-07-15
Code-only patch release (no catalog changes). Upgrade with
ALTER EXTENSION plx UPDATE TO '1.2.2' after installing the new module.
Fixed
- plxphp: assigning to a record field with the arrow form (
$NEW->col = e, the documented and idiomatic PHP spelling) raised “unsupported operator in statement”; only the array-element form worked. The arrow lvalue now lowers toNEW.col := e, so trigger functions can stampNEWfields with$NEW->col. Covered by a new plxphp trigger regression test.
[1.2.1] - 2026-07-15
Code-only patch release (no catalog changes). Upgrade with
ALTER EXTENSION plx UPDATE TO '1.2.1' after installing the new module.
Fixed
- Build on PostgreSQL 19 and 20 with a C23 toolchain (for example gcc 15). There,
pg_noreturnexpands to the standard[[noreturn]]attribute, whose placement is strict; plx wrote it after the storage class (static pg_noreturn void), which C23 rejects. It is now the first token of the declaration (pg_noreturn static void), matching PostgreSQL’s own convention, and still compiles on 13 through 18. The full suite passes on PostgreSQL 13 through 18 plus 19beta and 20devel built from source. See doc/COMPATIBILITY.md. - A plxgo regression test (0-based indexing) declared its accumulator with
:=from a non-inferable expression, so the function failed to create and the test silently checked the failure rather than indexing; it now usesvar sum int.
[1.2] - 2026-07-15
Added
plxplsql, an Oracle PL/SQL dialect. PL/SQL and plpgsql are both Ada-descended, so most of the language (DECLARE/BEGIN/EXCEPTION/END,IF/ELSIF,LOOP/WHILE/FOR,CASE,:=,||, cursors,%TYPE) passes through unchanged. plxplsql is a layout-preserving rewriter that translates the Oracle spellings:NUMBER/VARCHAR2/PLS_INTEGER/… types,DBMS_OUTPUT.PUT_LINE,RAISE_APPLICATION_ERROR,EXECUTE IMMEDIATE,FROM DUAL,NVL,seq.NEXTVAL,SYSDATE, andCURSOR c IS. Function signatures use PostgreSQL types; the body is PL/SQL. See doc/plxplsql.md.plxts, a TypeScript dialect: the plxjs dialect pluslet x: Ttype annotations, which map TypeScript types (number,string,boolean,bigint,T[],T | null) to SQL types and otherwise accept a SQL type name verbatim. See doc/plxts.md.plxtsql, a Transact-SQL (SQL Server) dialect. T-SQL is not Ada-descended, so plxtsql is a restructuring front end with its own tokenizer and parser: it hoists@-variables and inlineDECLAREinto the plpgsqlDECLAREblock, rewritesSET/SELECT @x =assignments, turnsIF/WHILE ... BEGIN ... ENDintoTHEN ... END IF/LOOP ... END LOOP, mapsTRY/CATCHto anEXCEPTIONblock, and translates the type and function libraries (INT,NVARCHAR(MAX),DATETIME,ISNULL,IIF,CONVERT,LEN,GETDATE,PRINT,RAISERROR,THROW, …). See doc/plxtsql.md.plxgo, a Go dialect. Go’s parenlessif/for,:=short declarations with type inference,for ... range, and no-fallthroughswitchdiffer enough from plpgsql that plxgo is a restructuring front end with its own tokenizer (including Go’s automatic semicolon insertion) and parser. It hoistsvar/:=/constdeclarations, rewrites assignment (includinga, b = x, yparallel assignment toSELECT ... INTO), turnsif/for/switchintoIF/WHILE/FOR/FOREACH/IF-ELSIF, mapspanic/fmt.PrintlntoRAISE, translates the type and a stdlib library (strings,math,strconv,len,append, type conversions), and providesemit/execute/range query()SQL intrinsics. See doc/plxgo.md.plxcoboltables:OCCURS nmaps aWORKING-STORAGEitem to a PostgreSQL array, withWS-ARR(i)subscripts as both lvalues and expressions andPERFORM v OVER ARRAYiteration.doc/DEBUGGING.md: correlating runtime errors to your dialect source, and aplx_source()helper that recovers the embedded original body.
Hardened
A whole-project adversarial audit (fresh-eyes review of every front end plus mutation fuzzing of all nine dialects) fixed a set of transpiler defects, all now covered by tests:
- Backend crashes: an unbounded intrinsic argument list read past a fixed stack
array (17+ arguments to
call/query/execute); the Python parser had no recursion-depth guard (deep nesting exhausted the C stack); a COBOLUSAGEclause at end of input stepped past the token-array sentinel. All now error. - Backend hangs: a stray
)/](Go) or danglingELSE(T-SQL) at statement position, and afor/if/else-ifrecursion in Go, could spin or overflow; all now error cleanly. - Wrong or invalid output: Go slice subscripts are 0-based (rewritten to
PostgreSQL’s 1-based arrays); a COBOL compound
UNTILno longer misfolds into the integer-FORbound; a JS/PHPswitchwith no case, onlydefault, or acaseafterdefaultnow errors instead of emitting uncompilable plpgsql; the?:elvis operator, a value-less Goconst, and several empty-argument forms (fmt.Println(),panic(),PRINT;,RAISERROR()) are handled or rejected.
The mutation fuzzer (test/fuzz.py) and corpus now cover all nine dialects.
Upgrading
ALTER EXTENSION plx UPDATE TO '1.2'(seeplx--1.1.1--1.2.sql).
1.1.1 - 2026-07-15
Code-only patch release (no catalog changes). Upgrade with
ALTER EXTENSION plx UPDATE TO '1.1.1' after installing the new module.
Fixed
- Compilation on PostgreSQL 13, 14, and 15:
plx_strbuild.cincludedvaratt.hunconditionally, but that header was only split out ofpostgres.hin PostgreSQL 16, so plx 1.1 did not build on 13-15. Guard the include. The full regression suite now passes on PostgreSQL 13 through 18 (verified in CI). - plxcobol: a crash (out-of-bounds read) on a body truncated at
PERFORM VARYING ... UNTIL; it now errors cleanly. - plxcobol:
ADD a b GIVING cand other multi-addendADD/SUBTRACTforms were rejected; parse an operand list.MULTIPLY/DIVIDEremain single-source. - plxcobol: multi-argument SQL function calls in expressions (
mod(a, b)) were broken because the tokenizer stripped commas everywhere; keep commas inside parentheses. - plxcobol: a
GREATER/LESS ... OR EQUALcomparison at the end of a condition dropped the “OR EQUAL”; aPICTURErepeat-count integer overflow; and an unterminated string literal silently lost its last character.
Added
- Continuous integration (GitHub Actions) running the full 9-suite regression on a PostgreSQL 13 through 18 matrix.
- plxcobol coverage in the fuzzer and the corpus runner, and plxcobol rejection tests in the error suite.
1.1 - 2026-07-14
Added
plxcobol, a COBOL dialect (ISO/IEC 1989:2023, COBOL 2023, free format), at full plpgsql construct parity. It has its own front end (verb-driven tokenizer and parser):WORKING-STORAGEdeclarations withPICTURE/TYPE/CONSTANTmapped to SQL types;MOVE/COMPUTEand theADD/SUBTRACT/MULTIPLY/DIVIDEverbs;IF/END-IF;EVALUATE(simple andEVALUATE TRUE);PERFORMin theUNTIL,VARYING,TIMES, inline, query (OVER), and array (OVER ARRAY) forms;GOBACK RETURNING,RETURN-NEXT,RETURN-QUERY;EXECUTE; cursors (OPEN-CURSOR/FETCH-CURSOR/MOVE-CURSOR/CLOSE-CURSOR); exception handling (BEGIN-TRY/WHEN/END-TRY) with stacked diagnostics viaGET;RAISE,DISPLAY,ASSERT,CALL,COMMIT/ROLLBACK. Data names are mapped to plpgsql identifiers (lower-cased, hyphens to underscores). See doc/plxcobol.md.- plxcobol:
STRING-APPEND <expr> TO <var>, which lowers to theplx_strbuildstring builder (the COBOL counterpart of the other dialects' append operators), and%as the modulo operator in expressions. - Regression suite
plxcoboladded tomake installcheck. - Benchmarks now cover plxcobol (
bench/BENCHMARKS.md): it matches plpgsql on arith, strbuild, iter, and call, and is about 1.3x on the branch workload becauseEVALUATElowers toCASE. APERFORM VARYING v FROM a BY 1 UNTIL v > bcounting loop lowers to a plpgsql integerFORloop (otherPERFORM VARYINGforms useWHILE).
Upgrading
- Existing 1.0 installations upgrade in place with
ALTER EXTENSION plx UPDATE TO '1.1'(seeplx--1.0--1.1.sql).
1.0 - 2026-07-14
Initial release.
Added
- Dialect-pluggable front end that transpiles to plpgsql at
CREATE FUNCTIONtime. The generated plpgsql is stored inpg_proc.prosrcand executed by the standard plpgsql handler, with no separate language runtime in the backend. - Dialects, each at full plpgsql construct parity:
plxruby, a Ruby dialect.plxphp, a PHP dialect.plxjs, a JavaScript dialect.plxpython3, a Python dialect.
- Language names carry a
plxprefix, so the extension coexists with the native PL/Ruby and PL/PHP in the same database. - Coverage of the plpgsql surface from every dialect: typed local declarations
and inference, control flow, loops with labels, query iteration, array
iteration, single-row fetch, dynamic SQL, cursors, diagnostics, set-returning
functions, error raising and handling, assertions, and trigger functions with
NEW,OLD, and theTG_variables. - Idempotent re-transpile: a sentinel comment records the transpiler version and embeds the original source, so re-processing a stored function is a no-op.
plx_strbuild, an expanded-object string builder with amortized-O(1) append, addressing plpgsql’s O(n2) in-loop string concatenation. The transpiler lowers the dialect append operators (s << x,$s .= x,s += xon a string) onto it. On PostgreSQL 18 this is about 170x faster than thes := s || 'x'idiom and faster than the native PLs' own append. The acceleration relies on the PostgreSQL 18 planner support requestSupportRequestModifyInPlace; on PostgreSQL 13 to 17 the builder is correct but not accelerated.- Benchmark harness (
bench/run_bench.py) covering five workloads against plpgsql, the plx dialects, PL/Perl, PL/Python3, and the native PL/Ruby and PL/PHP, with results inbench/BENCHMARKS.md. - Regression suite (
make installcheck) across the dialects, features, output, errors, and the string builder, plus a corpus runner and a fuzzer. - Documentation: per-dialect chapters, a plpgsql parity matrix, a user guide drawn from the PL/pgSQL manual, an architecture document, a transpiler specification, and a compatibility note.
Compatibility
- Tested against PostgreSQL 13, 14, 15, 16, 17, and 18. The full regression suite passes on each.