Contents
plx Architecture
plx lets a function body be written in a Ruby, PHP, JavaScript, TypeScript, Python, Go, COBOL, Oracle PL/SQL, or Transact-SQL dialect and executed by the standard plpgsql interpreter. This document describes how that works in the extension as built.
The core idea
A plx language is a PostgreSQL procedural language whose call handler is
plpgsql’s own call handler. The dialect body never runs directly. At
CREATE FUNCTION time plx transpiles the body to plpgsql text and stores that
text in pg_proc.prosrc. At run time plpgsql compiles and executes that stored
text like any other plpgsql function.
There is no separate language runtime. plx is a C extension that parses the 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:
HANDLERisplx_call_handler, which is bound to plpgsql’s exportedplpgsql_call_handlersymbol (AS '$libdir/plpgsql', 'plpgsql_call_handler'). Binding a freshpg_procrow to plpgsql’s symbol is legal becauseCreateProceduralLanguageonly checks that the handler returnslanguage_handler.VALIDATORis the dialect’s validator (for exampleplx_ruby_validator), which does the transpilation.INLINEis the dialect’s inline handler, forDOblocks.
Because the call handler is plpgsql’s own, run-time execution is plpgsql with no
plx code on the hot path. plx inherits SPI setup, plan caching and invalidation,
polymorphic argument resolution, OUT/SETOF handling, and trigger dispatch
from plpgsql automatically.
The validator: transpile at DDL time
ProcedureCreate calls the language validator after inserting the pg_proc row,
on every CREATE and CREATE OR REPLACE, and on pg_restore. The dialect
validator (in plx_core.c, plx_generic_validator):
- Reads the raw dialect body from
pg_proc.prosrc. - If the text already begins with the plx sentinel (
/*plx:v1:...*/), it is already plpgsql (a restore or a repeated validation), so it returns without change. This makes the pass idempotent and keeps dump and restore correct. - Otherwise it builds a
PlxFuncMetafrom the pg_proc row (argument names, types, and modes viaget_func_arg_info; return type;proretset; prokind), calls the dialect’stranspilefunction, and rewritespg_proc.prosrcwith the resulting plpgsql viaCatalogTupleUpdate.
The stored text is:
/*plx:v1:<dialect>:<hash>*/
[DECLARE ...]
BEGIN
<emitted plpgsql>
END;
/*plx-orig:b64$<base64 of the original dialect body>$plx-orig*/
The sentinel gates idempotency. The base64 trailer preserves the original source so it survives dump and restore and can be recovered.
DO blocks have no pg_proc row. The inline handler transpiles the block text
and delegates to plpgsql’s own inline handler with a rebuilt InlineCodeBlock.
Why the binding holds across versions
The only symbol that fmgr resolves from another module is
plpgsql_call_handler, which is a global fmgr entry point in every supported
release (PostgreSQL 13 to 18). plx does not call any plpgsql-internal symbol
(the compiler, executor, or scanner functions), so it does not depend on the
symbol visibility of those internals. See COMPATIBILITY.md.
The transpiler
The transpiler does not parse the source language’s expression grammar.
plpgsql expressions are SQL expressions, so the transpiler is a statement-level
restructurer: it finds statement and block boundaries, hoists typed DECLAREs,
rewrites a fixed set of operators and interpolations, and passes the remaining
expression text through to plpgsql and SQL unchanged.
It is dialect-pluggable through a PlxSurface (in plx_int.h) that each dialect
supplies. The surface describes what varies between languages:
- the keyword table, mapping each dialect’s spellings to canonical keywords;
- the block style, used as a lexer hint (indentation-tokenized vs. newline-tokenized);
- comment syntax, the variable sigil (for example
$), the string-concatenation operator, and how string interpolation is written (#{},$varand{$e},${}template literals, or f-strings); parse_body, the front-end entry point (see below), andself_contained_block, a flag for dialects that emit their ownDECLARE/BEGIN/END(PL/SQL).
Engine and front ends
The transpiler is split into a dialect-neutral engine and per-dialect
front ends, selected through the parse_body function pointer on the
surface (a vtable method). plx_transpile() just calls
cx->surf->parse_body(cx) and then runs one assemble tail; there is no
per-dialect branching in the driver.
- The engine lives in
plx_transpile.cand is declared to the front ends throughplx_engine.h: the shared byte lexer (plx_lex), the expression rewriter (plx_rewrite_expr), the leaf-statement emitter and intrinsics (query,fetch_one,perform,execute,return_query, cursors, and so on), the symbol table, string/interpolation decoding, and the final DECLARE-hoisting + assemble. It contains no dialect-specific code. - Each front end owns its dialect’s tokenizer, parser, and statement
lowering, and implements
parse_bodyby transformingcx->bodyintocx->out. The text-family dialects (php/js/ts) share a brace parser inplx_parse_brace.c; Ruby (keyword-end) and Python (indentation) parse on top of the shared lexer inside their own translation units; and the standalone dialects (COBOL, PL/SQL, T-SQL, Go) run their own tokenizer and emitter, calling back into the engine only for shared services.
Files
plx.control, plx--1.0.sql extension control and install SQL
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
plx_* entry points the front ends call
src/plx_core.c PL handler binding, registry, generic validator
and inline handler
src/plx_transpile.c the dialect-neutral engine + plx_transpile() driver
src/plx_strbuild.c string-builder intrinsic helpers
src/plx_parse_brace.c shared brace front end (php/js/ts) + TS preprocess
src/plx_dialect_ruby.c the plxruby surface + Ruby front end
src/plx_dialect_php.c the plxphp surface (parse_body -> brace front end)
src/plx_dialect_js.c the plxjs surface (parse_body -> brace front end)
src/plx_dialect_ts.c the plxts surface (parse_body -> brace front end)
src/plx_dialect_python.c the plxpython3 surface + Python front end
src/plx_dialect_go.c the plxgo surface + Go front end
src/plx_dialect_cobol.c the plxcobol surface + COBOL front end
src/plx_dialect_plsql.c the plxplsql surface + PL/SQL front end
src/plx_dialect_tsql.c the plxtsql surface + T-SQL front end
Everything links into a single plx.so. A dialect is a PlxSurface (including
its parse_body front end) plus small trampolines (validator, inline handler,
and the shared call-handler binding), registered in _PG_init. Adding a dialect
is a new plx_dialect_X.c (a surface with its parse_body, plus a few
CREATE LANGUAGE lines), and does not touch the shared engine.
Trust
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
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).
Testing
Four layers, each answering a different question.
make installcheck runs the pg_regress suite in test/sql, one file per
dialect plus the shared feature, output, string-builder and error files. This
is the record of what each dialect accepts and what plpgsql it produces.
test/run_corpus.py replays a corpus of Ruby programs and compares their
output to a recorded expectation, including the programs that are supposed to
be rejected.
test/fuzz.py feeds mutated and pathological sources to every front end. It
asserts nothing about the output; it asserts that the transpiler does not
crash, hang, or exhaust memory on input written to break it.
make differentialcheck runs test/differential.py, which answers the
question the other three cannot. Each case is one small program written once
as a plpgsql reference and once per dialect, then called with the same
arguments. The reference is the plpgsql a PostgreSQL developer would have
written for the same logic, so it acts as an oracle: plx claims a dialect body
transpiles to plpgsql that behaves the same way, and a disagreement is a defect
rather than a difference of opinion. Values are compared by their text form,
with NULL distinguished from the empty string, and a case that raises must
raise the same SQLSTATE.
Some divergences are intended, and a case records them in a documented entry
naming the dialects, the calls, and the reason. Those are reported separately
rather than failing. The check also fails when a recorded divergence stops
happening, so a limitation cannot quietly outlive the documentation that
describes it. Where a case cannot cover a dialect, the run prints the gap
instead of passing over it.