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.json listed the tag pl/sql, which PGXN rejects: a Tag may not contain a slash. It is now plsql. 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) validates META.json against 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. So sql server is a valid tag and an invalid term, and only the slash in pl/sql was 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 in COALESCE((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.Sprintf is lowered to a SQL concatenation rather than format(), which is what allows the propagation, since format() 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 through rpad/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.Sprintf in expression position passed its Go format string straight into SQL format(), so any verb other than %s raised unrecognized format() type specifier when the function was called. The transpile succeeded and the failure only appeared at run time, which made fmt.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 %s that format() understands, keeping a - flag and a width and dropping the flags and precision format() has no equivalent for. A % that starts no directive is escaped rather than passed through, since a lone % is itself an error to format(). Found by the new differential check.

    Note that the verbs which change an operand’s representation rather than its padding render what %s renders, so fmt.Sprintf("%x", 255) yields 255 and not ff. Convert explicitly where the representation matters. This is recorded in doc/plxgo.md and doc/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_body vtable (#2). The single ~11k-line src/plx_transpile.c is split into a dialect-neutral engine (declared in the new src/plx_engine.h) plus per-dialect front ends in src/plx_dialect_*.c and the shared src/plx_parse_brace.c; the hardcoded block_style dispatch is replaced by a per-dialect parse_body function 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 of src/plx_dialect_ruby.c and 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_body front ends are now static (only the shared plx_brace_parse_body keeps a prototype in plx_engine.h); dropped the write-only Ctx.nt field; 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_EOF token write in lex(): a source that lexed to exactly cap-1 tokens overflowed the token array by one (heap overflow / SIGSEGV). All token writes are now guarded.
  • plx_strbuild sb_ensure() doubled its capacity in int32, which could wrap negative near ~1GB and spin forever while passing a bogus size to repalloc. Growth is now computed in 64-bit and clamped to MaxAllocSize, 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 honoring max_stack_depth instead 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_INDENT that 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 accept 0x/0o/0b only 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-declarations across 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 NEW fields with SET NEW.col = e, which lowers to NEW.col := e, so a Transact-SQL trigger can rewrite the row and not only validate it. A SET whose target is a qualified name with a top-level = is an assignment; SET NOCOUNT ON and 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 to NEW.col := e, so trigger functions can stamp NEW fields 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_noreturn expands 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 uses var 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, and CURSOR c IS. Function signatures use PostgreSQL types; the body is PL/SQL. See doc/plxplsql.md.
  • plxts, a TypeScript dialect: the plxjs dialect plus let x: T type 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 inline DECLARE into the plpgsql DECLARE block, rewrites SET/SELECT @x = assignments, turns IF/WHILE ... BEGIN ... END into THEN ... END IF / LOOP ... END LOOP, maps TRY/CATCH to an EXCEPTION block, 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 parenless if/for, := short declarations with type inference, for ... range, and no-fallthrough switch differ enough from plpgsql that plxgo is a restructuring front end with its own tokenizer (including Go’s automatic semicolon insertion) and parser. It hoists var/:=/const declarations, rewrites assignment (including a, b = x, y parallel assignment to SELECT ... INTO), turns if/for/switch into IF/WHILE/FOR/FOREACH/IF-ELSIF, maps panic/fmt.Println to RAISE, translates the type and a stdlib library (strings, math, strconv, len, append, type conversions), and provides emit/execute/range query() SQL intrinsics. See doc/plxgo.md.
  • plxcobol tables: OCCURS n maps a WORKING-STORAGE item to a PostgreSQL array, with WS-ARR(i) subscripts as both lvalues and expressions and PERFORM v OVER ARRAY iteration.
  • doc/DEBUGGING.md: correlating runtime errors to your dialect source, and a plx_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 COBOL USAGE clause at end of input stepped past the token-array sentinel. All now error.
  • Backend hangs: a stray )/] (Go) or dangling ELSE (T-SQL) at statement position, and a for/if/else-if recursion 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 UNTIL no longer misfolds into the integer-FOR bound; a JS/PHP switch with no case, only default, or a case after default now errors instead of emitting uncompilable plpgsql; the ?: elvis operator, a value-less Go const, 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' (see plx--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.c included varatt.h unconditionally, but that header was only split out of postgres.h in 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 c and other multi-addend ADD/SUBTRACT forms were rejected; parse an operand list. MULTIPLY/DIVIDE remain 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 EQUAL comparison at the end of a condition dropped the “OR EQUAL”; a PICTURE repeat-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-STORAGE declarations with PICTURE/TYPE/CONSTANT mapped to SQL types; MOVE/COMPUTE and the ADD/SUBTRACT/MULTIPLY/ DIVIDE verbs; IF/END-IF; EVALUATE (simple and EVALUATE TRUE); PERFORM in the UNTIL, 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 via GET; 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 the plx_strbuild string builder (the COBOL counterpart of the other dialects' append operators), and % as the modulo operator in expressions.
  • Regression suite plxcobol added to make 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 because EVALUATE lowers to CASE. A PERFORM VARYING v FROM a BY 1 UNTIL v > b counting loop lowers to a plpgsql integer FOR loop (other PERFORM VARYING forms use WHILE).

Upgrading

  • Existing 1.0 installations upgrade in place with ALTER EXTENSION plx UPDATE TO '1.1' (see plx--1.0--1.1.sql).

1.0 - 2026-07-14

Initial release.

Added

  • Dialect-pluggable front end that transpiles to plpgsql at CREATE FUNCTION time. The generated plpgsql is stored in pg_proc.prosrc and 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 plx prefix, 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 the TG_ 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 += x on a string) onto it. On PostgreSQL 18 this is about 170x faster than the s := s || 'x' idiom and faster than the native PLs' own append. The acceleration relies on the PostgreSQL 18 planner support request SupportRequestModifyInPlace; 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 in bench/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.