Skip to content

JavaScript cookbook

Practical recipes for the plxjs dialect. Every recipe here was run on PostgreSQL; plx transpiles the body to plpgsql and the standard interpreter executes it. See the plxjs chapter for the full language reference.

Scalar function with branching

Assign to a result variable through an if / else if / else chain and return it. The variable is annotated with its type because it has no initializing literal.

CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxjs AS $$
let g /*:: text */;
if (score >= 90) { g = "A"; }
else if (score >= 80) { g = "B"; }
else if (score >= 70) { g = "C"; }
else { g = "F"; }
return g;
$$;
SELECT grade(95) AS a, grade(85) AS b, grade(72) AS c, grade(40) AS f;
 a | b | c | f 
---+---+---+---
 A | B | C | F
(1 row)

Accumulating loop

A C-style for loop accumulates into a variable. The accumulator is typed bigint from its literal initializer plus the annotation.

CREATE FUNCTION factorial(n int) RETURNS bigint LANGUAGE plxjs AS $$
let result = 1 /*:: bigint */;
for (let i = 2; i <= n; i++) {
  result = result * i;
}
return result;
$$;
SELECT factorial(10);
 factorial 
-----------
   3628800
(1 row)

Building a string in a loop

Use += on a string variable. plx lowers this to its string builder, which is amortized O(1) per append on PostgreSQL 18 instead of the O(n^2) copy that s = s || x would produce.

CREATE FUNCTION csv_repeat(word text, n int) RETURNS text LANGUAGE plxjs AS $$
let s = "" /*:: text */;
for (let i = 1; i <= n; i++) {
  s += word;
  if (i < n) { s += ","; }
}
return s;
$$;
SELECT csv_repeat('ab', 4);
 csv_repeat  
-------------
 ab,ab,ab,ab
(1 row)

Looping over a query

for (const r of query(...)) iterates a result set. The row is a record and columns are read as r.col. Use a template literal to interpolate the argument into the SQL text.

CREATE FUNCTION order_total(g int) RETURNS bigint LANGUAGE plxjs AS $$
let total = 0 /*:: bigint */;
for (const r of query(`SELECT amount FROM orders WHERE grp = ${g}`)) {
  total = total + r.amount;
}
return total;
$$;

Setup and call:

CREATE TABLE orders (id int, grp int, amount bigint);
INSERT INTO orders VALUES (1,1,100),(2,1,250),(3,1,75),(4,2,500);
SELECT order_total(1);
 order_total 
-------------
         425
(1 row)

Set-returning function

A function declared RETURNS TABLE(...) streams rows with return_query. Qualify the column names with the table name so they are not read as the output parameters of the same name.

CREATE FUNCTION order_report(g int) RETURNS TABLE(id int, amount bigint) LANGUAGE plxjs AS $$
return_query(`SELECT orders.id, orders.amount FROM orders WHERE grp = ${g} ORDER BY orders.amount DESC`);
return;
$$;
SELECT * FROM order_report(1);
 id | amount 
----+--------
  2 |    250
  1 |    100
  3 |     75
(3 rows)

To emit computed rows one at a time instead, use return_next with RETURNS SETOF:

CREATE FUNCTION evens(lo int, hi int) RETURNS SETOF int LANGUAGE plxjs AS $$
for (let i = lo; i <= hi; i++) {
  if (i % 2 == 0) { return_next(i); }
}
return;
$$;
SELECT array_agg(x) FROM evens(1, 10) AS x;
  array_agg   
--------------
 {2,4,6,8,10}
(1 row)

Error handling with try / catch

try / catch (e) maps to a plpgsql exception block (WHEN OTHERS). e.message reads SQLERRM. The raise call form emits a RAISE at the named level.

CREATE FUNCTION safe_divide(a int, b int) RETURNS int LANGUAGE plxjs AS $$
try {
  return a / b;
} catch (e) {
  raise("notice", `caught: ${e.message}`);
  return -1;
}
$$;
SELECT safe_divide(10, 2) AS ok, safe_divide(10, 0) AS zero;
NOTICE:  caught: division by zero
 ok | zero 
----+------
  5 |   -1
(1 row)

Trigger function

A function returning trigger can be attached to a table. Assign to NEW fields and return NEW. This one normalizes a code and derives a label before the row is written, on both insert and update.

CREATE FUNCTION normalize_product() RETURNS trigger LANGUAGE plxjs AS $$
NEW.code = upper(NEW.code);
NEW.label = `code:${NEW.code}`;
return NEW;
$$;
CREATE TABLE products (id int, code text, label text);
CREATE TRIGGER trg_norm BEFORE INSERT OR UPDATE ON products
  FOR EACH ROW EXECUTE FUNCTION normalize_product();
INSERT INTO products(id, code) VALUES (1, 'abc'), (2, 'xy');
SELECT id, code, label FROM products ORDER BY id;
 id | code |  label   
----+------+----------
  1 | ABC  | code:ABC
  2 | XY   | code:XY
(2 rows)

Dynamic SQL with bind parameters

Use execute(sql, arg, ...) and reference the arguments as $1, $2, and so on. Bind parameters keep untrusted values out of the SQL text, which avoids injection. row_count() reads how many rows the statement affected.

CREATE FUNCTION log_event(msg text) RETURNS bigint LANGUAGE plxjs AS $$
execute("INSERT INTO events(note) VALUES ($1)", msg);
let n = row_count() /*:: bigint */;
return n;
$$;
CREATE TABLE events (id serial PRIMARY KEY, note text);
SELECT log_event('deploy started');
SELECT note FROM events ORDER BY id;
 log_event 
-----------
         1
(1 row)

      note      
----------------
 deploy started
(1 row)

JS idioms: template literals, for...of, and switch

This function combines the common JavaScript idioms plx supports: a for...of loop over a query, a switch that lowers to CASE, and a template literal that interpolates several values into the return string.

CREATE FUNCTION describe_grp(g int) RETURNS text LANGUAGE plxjs AS $$
let count = 0 /*:: int */;
let total = 0 /*:: bigint */;
for (const r of query(`SELECT amount FROM orders WHERE grp = ${g}`)) {
  count = count + 1;
  total = total + r.amount;
}
let size /*:: text */;
switch (count) {
  case 0: size = "empty"; break;
  case 1: size = "single"; break;
  default: size = "many"; break;
}
return `group ${g}: ${count} orders (${size}), total ${total}`;
$$;
SELECT describe_grp(1) AS g1, describe_grp(2) AS g2;
                 g1                  |                  g2                   
-------------------------------------+---------------------------------------
 group 1: 3 orders (many), total 425 | group 2: 1 orders (single), total 500
(1 row)

Single-row lookup with a ternary

fetch_one returns one row, or an all-NULL record when nothing matches. A ternary (c ? a : b, which lowers to CASE) plus a === null test turns the miss into a readable message. x === null becomes IS NULL.

CREATE FUNCTION user_greeting(uid int) RETURNS text LANGUAGE plxjs AS $$
let u = fetch_one(`SELECT name FROM users WHERE id = ${uid}`);
return u.name === null ? `no user #${uid}` : `hello, ${u.name}`;
$$;
CREATE TABLE users (id int, name text);
INSERT INTO users VALUES (1, 'Ada'), (2, 'Grace');
SELECT user_greeting(1) AS found, user_greeting(99) AS missing;
   found    |   missing   
------------+-------------
 hello, Ada | no user #99
(1 row)