Python cookbook¶
Practical recipes for the plxpython3 dialect. Every recipe here was run on PostgreSQL; plx transpiles the body to plpgsql and the standard interpreter executes it. See the plxpython3 chapter for the full language reference.
Scalar function with branching¶
An if / elif / else chain selects a value. Locals are declared by
annotating them with #:: type and assigned with =.
CREATE FUNCTION letter_grade(score int) RETURNS text LANGUAGE plxpython3 AS $$
result #:: text
if score >= 90:
result = "A"
elif score >= 80:
result = "B"
elif score >= 70:
result = "C"
else:
result = "F"
return result
$$;
Accumulating loop with range¶
for i in range(a, b) counts from a to b - 1. Give the accumulator an
explicit type so the arithmetic does not overflow int.
CREATE FUNCTION factorial(n int) RETURNS bigint LANGUAGE plxpython3 AS $$
result = 1 #:: bigint
for i in range(1, n + 1):
result = result * i
return result
$$;
Building a string in a loop¶
Use += on a text variable. plx lowers this to its string builder, so each
append is amortized O(1) on PostgreSQL 18 instead of the O(n^2) copy that
s = s || x would cost. The f"{i}" converts the integer to text before it is
appended.
CREATE FUNCTION int_csv(n int) RETURNS text LANGUAGE plxpython3 AS $$
s = "" #:: text
for i in range(1, n + 1):
if i > 1:
s += ", "
s += f"{i}"
return s
$$;
Looping over a query¶
for row in query(...) iterates the rows of a result set. Each row is a
record; access columns with row.col. The f-string interpolates the argument
g into the SQL text.
CREATE FUNCTION order_total(g int) RETURNS bigint LANGUAGE plxpython3 AS $$
total = 0 #:: bigint
for row in query(f"SELECT amount FROM orders WHERE grp = {g}"):
total = total + row.amount
return total
$$;
CREATE TABLE orders(id int, grp int, amount int);
INSERT INTO orders VALUES (1,1,100),(2,1,250),(3,2,40),(4,1,10);
SELECT order_total(1) AS grp1, order_total(2) AS grp2;
Set-returning function¶
A function returning SETOF emits rows with return_next(...). A bare return
ends the function.
CREATE FUNCTION divisors(n int) RETURNS SETOF int LANGUAGE plxpython3 AS $$
for i in range(1, n + 1):
if n % i == 0:
return_next(i)
return
$$;
Error handling with try / except¶
try / except maps to a plpgsql exception block. Name a specific condition
with the PG:: spelling; PG::DivisionByZero catches division_by_zero. The
e.message accessor reads SQLERRM.
CREATE FUNCTION safe_divide(a int, b int) RETURNS int LANGUAGE plxpython3 AS $$
try:
return a / b
except PG::DivisionByZero as e:
raise("notice", f"caught: {e.message}")
return -1
$$;
Trigger function¶
A function returning trigger can back a trigger. Assign to NEW fields and
return NEW. Here a BEFORE INSERT trigger fills in a default label and
normalizes the code to upper case.
CREATE FUNCTION stamp_widget() RETURNS trigger LANGUAGE plxpython3 AS $$
if NEW.label is None:
NEW.label = f"row {NEW.id}"
NEW.code = upper(NEW.code)
return NEW
$$;
CREATE TABLE widget(id int primary key, label text, code text);
CREATE TRIGGER widget_stamp BEFORE INSERT ON widget
FOR EACH ROW EXECUTE FUNCTION stamp_widget();
INSERT INTO widget(id, label, code) VALUES (1, NULL, 'ab'), (2, 'named', 'cd');
SELECT id, label, code FROM widget ORDER BY id;
Dynamic SQL with bind parameters¶
Interpolate identifiers you control (a table name) with an f-string, but pass
untrusted values as bind parameters: $1 in the SQL text and an extra argument
to fetch_one. This keeps the value out of the SQL string and avoids injection.
CREATE FUNCTION count_at_least(tbl text, threshold int) RETURNS bigint LANGUAGE plxpython3 AS $$
r = fetch_one(f"SELECT count(*) AS c FROM {tbl} WHERE amount >= $1", threshold)
return r.c
$$;
Python idioms: f-strings, range, while¶
f-strings interpolate expressions with f"{expr}", for i in range(a, b) is the
integer loop, and while runs until its condition is false. This function lists
the integers up to n and then uses a while loop to find the largest power of
two that does not exceed n.
CREATE FUNCTION number_report(n int) RETURNS text LANGUAGE plxpython3 AS $$
s = "" #:: text
for i in range(2, n + 1):
s += f"{i} "
p = 1 #:: int
while p * 2 <= n:
p = p * 2
s += f"| largest power of two <= {n} is {p}"
return s
$$;
report
------------------------------------------------------
2 3 4 5 6 7 8 9 10 | largest power of two <= 10 is 8
(1 row)
Iterating an array¶
for v in values iterates an array argument. The loop variable is annotated
with the element type. The annotation v #:: int comes before the loop.