plxjs: the JavaScript dialect¶
plxjs lets you write PostgreSQL functions with JavaScript syntax (current
ECMAScript). At CREATE FUNCTION time plx transpiles the body to plpgsql and
stores the plpgsql in pg_proc.prosrc. The function runs on the standard plpgsql
interpreter.
The language name is plxjs.
Setup¶
Function basics¶
Blocks are brace-delimited and statements end with ;. A scalar function ends
with an explicit return.
Function arguments are referenced by name. Any SQL expression is valid, because expressions are passed through to plpgsql and SQL unchanged.
Local variables and types¶
Declare locals with let, const, or var; the keyword is removed in the
generated plpgsql. plx infers the type from the first value when it is an
integer, numeric, text, or boolean literal. Otherwise annotate the variable with
a /*:: type */ comment before the ;.
let n = 42; // inferred integer
let label = "count"; // inferred text
let amount /*:: numeric */;
amount = principal * rate;
The annotation text is emitted verbatim, so %TYPE and %ROWTYPE work:
A constant uses the const suffix in the annotation:
Control flow¶
if / else if / else¶
switch¶
switch lowers to plpgsql CASE. Stacked labels become a value list. Each arm
must end with break or a terminating statement (return/throw); genuine
fall-through is rejected.
Loops¶
Counting for and while:
continue and break map to CONTINUE and EXIT.
Loop labels¶
outer: for (let i = 1; i <= n; i++) {
for (let j = 1; j <= n; j++) {
if (i + j >= limit) { break outer; }
}
}
Working with data¶
Iterating a query¶
for (const row of query(`SELECT id, amount FROM orders WHERE grp = ${g}`)) {
total = total + row.amount;
}
The row variable is a record. Field access is row.col. For bind parameters or
non-literal SQL, pass extra arguments: query(sql, a) of ....
Iterating an array¶
The loop variable must be annotated with its element type.
Fetching one row¶
fetch_one returns all-NULL on no row. fetch_one! raises on zero or more than
one row.
Running SQL¶
perform(`UPDATE counters SET n = n + 1 WHERE id = ${cid}`);
execute("INSERT INTO t(msg) VALUES ($1)", note);
return_query(`SELECT id FROM vip ORDER BY id`); // set-returning function
Use bind arguments in execute for untrusted input.
Cursors¶
let c = open_cursor(`SELECT v FROM t ORDER BY v`);
let row = fetch_from(c);
while (found()) {
total = total + row.v;
row = fetch_from(c);
}
close_cursor(c);
move_cursor(c) and move_cursor(c, n) map to MOVE.
Diagnostics¶
Set-returning functions¶
CREATE FUNCTION squares(n int) RETURNS SETOF int LANGUAGE plxjs AS $$
for (let i = 1; i <= n; i++) { return_next(i * i); }
return;
$$;
Errors¶
Throwing¶
The call form raise("notice", "message") emits a RAISE at the named level
(notice, warning, info, log, debug, exception).
Handling¶
try {
return 100 / d;
} catch (e) {
raise("notice", `caught: ${e.message}`);
return -1;
} finally {
perform("INSERT INTO log(msg) VALUES ('done')");
}
catch (e) maps to WHEN OTHERS. Accessors: e.message to SQLERRM,
e.sqlstate to SQLSTATE, and e.detail, e.hint, e.constraint, e.column,
e.table, e.schema, e.datatype to the matching GET STACKED DIAGNOSTICS
fields.
Assertions¶
Expressions¶
- Interpolation: template literals,
`total is ${amount}`. Plain"..."and'...'strings do not interpolate. Use template literals for concatenation. - Comparison:
==,===,!=,!==map to=and<>.x === nullbecomesx IS NULL. - Boolean:
&&,||,!map toAND,OR,NOT. - Ternary:
c ? a : bbecomesCASE WHEN c THEN a ELSE b END. nullandundefinedbecomeNULL.
Building strings in a loop¶
Concatenating onto a string in a loop is slow in plpgsql: s := s || 'x' is
O(n^2) because text is immutable and each step copies the whole string. Use +=
on a string variable, which plx lowers to its string builder (plx_strbuild):
let s = "" /*:: text */;
for (const row of query(`SELECT name FROM t ORDER BY id`)) {
s += row.name;
s += ",";
}
return s;
On PostgreSQL 18 this is amortized O(1) per append. On PostgreSQL 13 to 17 it is
correct but not accelerated (the in-place optimization needs a PostgreSQL 18
feature). Note that += is treated as string append only when the variable is a
string; on a numeric variable it is numeric addition.
Trigger functions¶
A function returning trigger can be used as a trigger. Assign to NEW fields
and return NEW:
CREATE FUNCTION stamp() RETURNS trigger LANGUAGE plxjs AS $$
NEW.tag = `row ${NEW.id}`;
return NEW;
$$;
NEW, OLD, and the TG_ variables are available. Assigning to a record field
(NEW.col = e) or an array element (arr[i] = e) is supported.
Semantic differences¶
These are intentional. plx pins semantics to SQL and plpgsql.
- Decimal literals infer
numeric, not a floating-point type. Arithmetic is exact, not IEEE 754. - Comparisons use SQL three-valued logic:
==,===,!=,!==map to=and<>, so a comparison involving NULL is unknown. Only a comparison with the literalnullbecomesIS NULL/IS NOT NULL. A positiveif/whilecondition treats NULL as false. ===and!==do not preserve JavaScript strict-type semantics; they behave like==and!=and use SQL type resolution.1 === "1"is false in JavaScript but compares as equal here. Compare like-typed values.+is numeric addition; use template literals for string building.- Interpolating a NULL in a template literal renders as an empty string, not the
JavaScript string
"null"; the whole string is never made NULL. - Integer division and modulo follow SQL (truncate toward zero).
- JavaScript truthiness is not emulated. A condition must be a boolean expression. A non-boolean condition is an error reported by plpgsql when the function runs.
- Locals are function-scoped, matching JavaScript
varscope.
Not supported¶
Rejected at CREATE FUNCTION time with a line number:
- Function definitions, arrow functions, classes,
import. - Object and array literals as general values.
for...in, andfor...ofover anything other thanquery(...)or an array.switchfall-through (end each case withbreakorreturn).- Per-block local
DECLARE(locals are function-scoped by design).
See PARITY.md for the full plpgsql construct matrix and ARCHITECTURE.md for how plx maps to the plpgsql engine.