39. PostgreSQL: Routines and triggers (PL/pgSQL)
A PostgreSQL-language instance accepts functions and procedures written in LANGUAGE plpgsql or LANGUAGE sql, and triggers CREATE TRIGGER … EXECUTE FUNCTION f(). MIRAJ supports a subset of them, described in this chapter: functions that return a value, functions that return rows (RETURNS SETOF, RETURNS TABLE), procedures, anonymous DO blocks, row-level triggers, and most of the PL/pgSQL language (variables, conditions, loops, queries, cursors, RAISE, exception handling).
Anything outside this subset is rejected at creation, with SQLSTATE 0A000 and a message that names the construct: an accepted routine runs, and nothing is silently kept in a wrong state. The list of what is rejected is in 39.9.
The examples use the ventes schema (tables client, article, commande, ligne_commande) from chapter 31. Getting started, with its starting data (articles STY-01 at 1.20, CAH-96 at 2.50, CLA-A4 at 4.90).
39.1 Functions#
Create a function#
CREATE [OR REPLACE] FUNCTION [schema.]name ( [parameter [, …]] )
RETURNS {type | SETOF type | SETOF table | TABLE (column type [, …])}
LANGUAGE {plpgsql | sql}
[IMMUTABLE | STABLE | VOLATILE]
[STRICT | RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT]
[SECURITY {INVOKER | DEFINER}]
AS $$ body $$;where parameter is [IN | OUT | INOUT] [name] type [{DEFAULT | =} value].
CREATE FUNCTION prix_ttc(prix numeric(10,2), taux numeric(4,2) DEFAULT 0.20)
RETURNS numeric(10,2) LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
resultat numeric(10,2);
BEGIN
resultat := prix * (1 + taux);
RETURN round(resultat, 2);
END;
$$;
SELECT prix_ttc(10), prix_ttc(10, 0.055);
SELECT code, prix, prix_ttc(prix) AS ttc FROM article ORDER BY id; prix_ttc(10) | prix_ttc(10, 0.055)
--------------+---------------------
12.00 | 10.60
code | prix | ttc
--------+------+------
STY-01 | 1.20 | 1.44
CAH-96 | 2.50 | 3.00
CLA-A4 | 4.90 | 5.88A function can be used anywhere an expression is allowed: SELECT list, WHERE, values of an INSERT or an UPDATE, query of a view.
Header elements#
| Element | Supported |
|---|---|
CREATE OR REPLACE | replaces the function of the same name; without it, an existing function gives 42723 |
schema.name | the function belongs to a schema; without a schema, to the current schema |
| parameters | IN (default), OUT, INOUT; without a name, they are read as $1, $2…; types from 32. Data types and table.column%TYPE |
| default values | DEFAULT value or = value; the last arguments of a call can then be omitted |
RETURNS type | a base type, void, or trigger (39.6) |
RETURNS SETOF …, RETURNS TABLE (…) | function that returns rows, called in a FROM (see below) |
an OUT or INOUT parameter | the function returns its final value (RETURNS can be omitted) |
LANGUAGE plpgsql | sql | required (42P13 otherwise), before or after the body |
| body | AS $$…$$, AS $tag$…$tag$, AS '…'; for LANGUAGE sql, also the form RETURN expression |
IMMUTABLE, STABLE, VOLATILE | kept and displayed (pg_proc.provolatile) |
STRICT | if an argument is NULL, the function returns NULL without running its body |
SECURITY INVOKER | DEFINER | rights of the caller (default) or of the creator, see 39.8 |
LEAKPROOF, PARALLEL …, COST n, ROWS n | accepted with no effect |
There is no overloading: a schema holds only one function per name, whatever its parameters. Names are case-insensitive (PRIX_TTC(1) calls prix_ttc).
LANGUAGE sql functions#
The body is a series of SQL statements; the function returns the result of its last SELECT (first row, NULL if it returns no row). The statements before it are writes.
CREATE FUNCTION libelle_article(p_id integer) RETURNS text LANGUAGE sql STABLE AS $$
SELECT code || ' - ' || libelle FROM article WHERE id = p_id
$$;
CREATE FUNCTION double(integer) RETURNS integer LANGUAGE sql AS 'SELECT $1 * 2';
CREATE FUNCTION triple(x integer) RETURNS integer LANGUAGE sql RETURN x * 3;
SELECT libelle_article(1), libelle_article(99), double(21), triple(7); libelle_article(1) | libelle_article(99) | double(21) | triple(7)
----------------------+---------------------+------------+-----------
STY-01 - Stylo bille | | 42 | 21Functions that read tables#
CREATE FUNCTION total_commande(p_commande integer) RETURNS numeric(12,2)
LANGUAGE plpgsql STABLE AS $$
DECLARE
v_total numeric(12,2);
BEGIN
SELECT sum(quantite * prix_unit) INTO v_total
FROM ligne_commande WHERE commande_id = p_commande;
RETURN coalesce(v_total, 0);
END;
$$;
SELECT total_commande(1), total_commande(42); total_commande(1) | total_commande(42)
-------------------+--------------------
22.00 | 0.00Function as a default value#
A stored function can provide the default value of a column, alone or in an expression:
CREATE TABLE remise (taux numeric(5,2), depuis date);
INSERT INTO remise VALUES (0.05, '2026-01-01'), (0.10, '2026-09-01');
CREATE FUNCTION remise_courante() RETURNS numeric(5,2) LANGUAGE sql STABLE AS $$
SELECT taux FROM remise ORDER BY depuis DESC LIMIT 1
$$;
CREATE TABLE devis (
id serial PRIMARY KEY,
client integer NOT NULL,
remise numeric(5,2) DEFAULT remise_courante(),
plafond numeric(12,2) DEFAULT (remise_courante() * 1000)
);
INSERT INTO devis (client) VALUES (1), (2); -- discount read from the remise table, for each row
SELECT * FROM devis; id | client | remise | plafond
----+--------+--------+---------
1 | 1 | 0.10 | 100.00
2 | 2 | 0.10 | 100.00- The function must exist when the table is created (
CREATE TABLE,ALTER TABLE … ADD COLUMN,ALTER COLUMN … SET DEFAULT): otherwise42883. It is not run at that time. - It is run for each written row that does not supply the column:
INSERTof one or more rows, column omitted orDEFAULTwritten,INSERT … SELECT,COPY … FROM STDIN, andUPDATE … SET column = DEFAULT,ON CONFLICT DO UPDATE SET column = DEFAULT. A volatile function (random(), a counter read from a table) therefore returns one value per row, as in PostgreSQL. The value is computed before theBEFORE INSERTtriggers, which see it inNEW, and before the generated columns, which can reference it. ALTER TABLE … ADD COLUMN … DEFAULT f()on a table that already has rows runs the function for each of them; if it reads the table being modified, it sees the table as it was before the statement.- The tables read by the function body (and by the functions it calls) are read-locked with the statement that writes, like those of a function called in the query: a concurrent write to these tables waits for the end of the statement. The function can read the written table itself: it sees the rows already written by the statement (
DEFAULT (SELECT max(id) + 1 …)written in a function numbers the rows of a multi-rowINSERT). - The session that writes without supplying the column must have the
EXECUTEprivilege on the function (42501,permission denied for function f), as in PostgreSQL; a row that supplies the column does not call it. - The function is run read-only, like any row-by-row call: a function that writes (
UPDATE compteur …) makes the row write fail (0A000). Numbering is done with a sequence (DEFAULT nextval('…'), 3.8) or an identity column. pg_attrdef,pg_get_expr,information_schema.columns.column_defaultandSHOW CREATE TABLEreturn the call (remise_courante()); a function called this way cannot be dropped on its own (2BP01, see below).- The MIRAJ language rejects a stored function in a default value, like the reference (
3770,Default value expression of column 'b' contains a disallowed function); a table created in the PostgreSQL language keeps its default value, which is also computed for writes from a session in the MIRAJ language.
Result, void, OUT parameter#
- The returned value is converted to the declared type, precision included: a function
RETURNS numeric(10,2)returns two decimals.numericwithout precision isnumeric(38,18):SELECT num_libre(1.5)of a functionRETURNS numericthat doubles its argument displays3.000000000000000000. - A function
RETURNS voidreturns NULL. - Reaching the end of a PL/pgSQL function without
RETURN:2F005(control reached end of function without RETURN).
CREATE FUNCTION f_out(a integer, OUT carre integer) LANGUAGE plpgsql AS $$
BEGIN
carre := a * a;
END $$;
SELECT f_out(5); -- 25Functions that return rows (SETOF, TABLE)#
A function declared RETURNS SETOF type, RETURNS SETOF table (one row of the table) or RETURNS TABLE (column type, …) returns a set of rows; it is called in the FROM of a query, like a table:
| Form | Rows returned |
|---|---|
LANGUAGE sql | the rows of the last query of the body |
RETURN QUERY query; (PL/pgSQL) | adds the rows of the query; can be written several times |
RETURN NEXT expression; | adds one row (single-column function) |
RETURN NEXT; | adds one row made of the current values of the OUT parameters or of the TABLE (…) columns |
RETURN; | ends the function; the rows already added are returned |
CREATE FUNCTION articles_sous(p_prix numeric) RETURNS SETOF article LANGUAGE sql STABLE AS $$
SELECT * FROM article WHERE prix < p_prix ORDER BY prix
$$;
SELECT id, code, prix FROM articles_sous(3); id | code | prix
----+--------+------
1 | STY-01 | 1.20
2 | CAH-96 | 2.50CREATE FUNCTION commandes_client(p_client integer)
RETURNS TABLE (commande integer, passee date, montant numeric(12,2))
LANGUAGE plpgsql STABLE AS $$
BEGIN
RETURN QUERY
SELECT o.id, o.passee_le, sum(l.quantite * l.prix_unit)
FROM commande o JOIN ligne_commande l ON l.commande_id = o.id
WHERE o.client_id = p_client
GROUP BY o.id, o.passee_le
ORDER BY o.id;
END $$;
SELECT * FROM commandes_client(1);
SELECT * FROM commandes_client(1) WHERE montant > 10; commande | passee | montant
----------+------------+---------
1 | 2026-10-01 | 22.00
2 | 2026-10-05 | 9.80
commande | passee | montant
----------+------------+---------
1 | 2026-10-01 | 22.00CREATE FUNCTION paliers(n integer) RETURNS SETOF integer LANGUAGE plpgsql AS $$
BEGIN
FOR i IN 1 .. n LOOP
IF i % 2 = 0 THEN
RETURN NEXT i * 10;
END IF;
END LOOP;
RETURN;
END $$;
CREATE FUNCTION carres(n integer, OUT k integer, OUT carre integer) RETURNS SETOF record
LANGUAGE plpgsql AS $$
BEGIN
FOR i IN 1 .. n LOOP
k := i;
carre := i * i;
RETURN NEXT;
END LOOP;
END $$;
SELECT * FROM paliers(7);
SELECT x.p * 2 FROM paliers(5) AS x(p);
SELECT * FROM carres(3); paliers
---------
20
40
60
x.p * 2
---------
40
80
k | carre
---+-------
1 | 1
2 | 4
3 | 9- The result columns take the names of the table columns (
SETOF table), ofTABLE (…)or of theOUTparameters; forSETOF type, the single column takes the name of the function, or the name given by the alias (AS p,AS x(p)). Each value is converted to the declared type. - The result can be filtered, joined and counted like a table (
WHERE,JOIN,count(*)). A function that returns no row gives an empty result. - The arguments are constants or bound parameters.
- A call in an expression (
SELECT paliers(3)) is rejected:0A000,set-valued function called in context that cannot accept a set (call it in FROM). RETURN expressionin such a function is rejected at creation (42804,RETURN cannot have a parameter in function returning set).- From psycopg2,
cursor.callproc("paliers", (7,))sendsSELECT * FROM paliers(7)and returns the three rows.
Drop#
DROP FUNCTION [IF EXISTS] [schema.]name [(types)] [CASCADE | RESTRICT];The type list is accepted with no effect (there is only one function per name). A missing function gives 42883, or a notice with IF EXISTS. A function called by triggers is dropped only with CASCADE, which also drops these triggers (39.6). The same applies to a function called by the default value of a column, in any schema: 2BP01 (cannot drop function f() because other objects depend on it (default value for column b of table t depends on function f())); with CASCADE, these default values are removed, the columns remain. One routine per statement.
39.2 Procedures and CALL#
CREATE [OR REPLACE] PROCEDURE [schema.]name ( [parameter [, …]] )
LANGUAGE {plpgsql | sql}
[SECURITY {INVOKER | DEFINER}]
AS $$ body $$;
CALL [schema.]name ( [arguments] );
DROP PROCEDURE [IF EXISTS] [schema.]name [(types)];A procedure does not return a value; it is called with CALL (using it in an expression gives 42883, just as calling a function with CALL does). Its OUT and INOUT parameters are returned to the client as one row, as in PostgreSQL.
CREATE PROCEDURE passer_commande(p_client integer, p_article integer, p_qte integer,
INOUT p_commande integer DEFAULT NULL)
LANGUAGE plpgsql AS $$
DECLARE
v_prix numeric(10,2);
v_stock integer;
BEGIN
SELECT prix, stock INTO v_prix, v_stock FROM article WHERE id = p_article;
IF NOT FOUND THEN
RAISE EXCEPTION 'article % inconnu', p_article USING ERRCODE = 'no_data_found';
END IF;
IF v_stock < p_qte THEN
RAISE EXCEPTION 'stock insuffisant pour l''article % : % demandé, % disponible',
p_article, p_qte, v_stock
USING ERRCODE = 'P0100';
END IF;
INSERT INTO commande (client_id) VALUES (p_client)
RETURNING id INTO p_commande;
INSERT INTO ligne_commande VALUES (p_commande, p_article, p_qte, v_prix);
UPDATE article SET stock = stock - p_qte WHERE id = p_article;
RAISE NOTICE 'commande % créée pour le client %', p_commande, p_client;
END;
$$;
CALL passer_commande(2, 2, 5);
CALL passer_commande(2, 3, 500);
CALL passer_commande(2, 99, 1);NOTICE: commande 4 créée pour le client 2
p_commande
------------
4
CALL
ERROR: stock insuffisant pour l'article 3 : 500 demandé, 80 disponible (P0100)
ERROR: article 99 inconnu (P0002)- The last arguments can be omitted if they have a default value; otherwise all are required, including those of
OUTparameters, whose given value is ignored:CREATE PROCEDURE p_out(a integer, OUT b integer, OUT c text) LANGUAGE plpgsql AS $$ BEGIN b := a * 2; c := 'x' || a; END $$; CALL p_out(5, NULL, NULL); CALL p_out(5);b | c ----+---- 10 | x5 CALL ERROR: Incorrect number of arguments for PROCEDURE ventes.p_out; expected 3, got 1 (42883) - A
LANGUAGE sqlprocedure runs only writes. - A procedure can call another one (
CALLin the body) and call functions.
Transactions and atomicity of a call#
COMMIT and ROLLBACK cannot be written in a procedure (rejected at creation, 0A000). The procedure runs in the caller's transaction:
BEGIN;
CALL passer_commande(3, 1, 1); -- NOTICE: commande 7 créée pour le client 3
ROLLBACK; -- the order, its line and the stock are undone
SELECT count(*) FROM commande WHERE client_id = 3; -- 0Outside a transaction block, a call is atomic, as in PostgreSQL: the CALL (or the statement that calls a function that writes, SELECT f(…)) runs in an implicit transaction, committed at the end and rolled back entirely if the routine fails, including what it wrote before the error and what the routines it calls wrote.
CREATE PROCEDURE p_ecrit_echoue() LANGUAGE plpgsql AS $$
BEGIN
UPDATE client SET ville = 'Avant-erreur' WHERE id = 3;
RAISE EXCEPTION 'stop';
END $$;
CALL p_ecrit_echoue(); -- ERROR: stop (P0001)
SELECT ville FROM client WHERE id = 3; -- Lyon: the UPDATE is undoneIn a BEGIN … COMMIT block, the client's transaction decides: ROLLBACK undoes the whole call.
A routine that writes can also receive a concurrency conflict (40001): the application calls it again, like any transaction (37.5).
Calling from a driver#
cur.execute("CALL passer_commande(%s, %s, %s)", (1, 1, 1))
print(cur.fetchone()) # (6,) : the value of the INOUT parameter
print(conn.notices) # ['NOTICE: commande 6 créée pour le client 1\n']
cur.execute("SELECT prix_ttc(%s)", (10,))
print(cur.fetchone()) # (Decimal('12.00'),)A function is called in an expression, or in a FROM where a scalar function returns a one-column row (SELECT * FROM prix_ttc(10), the form sent by psycopg2's cursor.callproc; see 35.3), and a set-returning function returns its rows (39.1):
cur.callproc("prix_ttc", (10,))
print(cur.fetchone()) # (Decimal('12.00'),)
cur.callproc("paliers", (7,))
print(cur.fetchall()) # [(20,), (40,), (60,)]callproc on a procedure returns 42883 (psycopg2 calls it with SELECT): write CALL.
Arguments can be named (f(name => value), or the older form f(name := value)), in any order, after any positional arguments; an omitted parameter takes its default value:
CREATE FUNCTION remise(montant numeric, taux numeric DEFAULT 0.1) RETURNS numeric
AS $$ SELECT montant * (1 - taux) $$ LANGUAGE sql;
SELECT remise(100), remise(100, taux => 0.2), remise(taux => 0.5, montant => 100); remise(100) | remise(100, taux => 0.2) | remise(taux => 0.5, montant => 100)
-----------------------+--------------------------+-------------------------------------
90.000000000000000000 | 80.000000000000000000 | 50.000000000000000000A name that is not that of a parameter (no parameter is named "tx"), a parameter given twice or an omitted parameter without a default value return 42883; a positional argument written after a named argument, 42601 (positional argument cannot follow named argument). Named notation applies to stored routines, CALL procedure(name => …) included (same rules, same errors). Among the server's functions, only those whose parameters PostgreSQL names accept it: make_date(year, month, day), make_time(hour, min, sec), make_timestamp(year, month, mday, hour, min, sec); the others have no named parameter (upper(s => 'a'): 42883, like PostgreSQL).
The function name can be written in quotes, with or without a schema (SELECT "prix_ttc"(10), ventes."prix_ttc"(10)).
Anonymous DO block#
DO immediately runs a PL/pgSQL block without creating a routine: it behaves like the body of a procedure with no parameters (same statements, same rejections), run with the session's rights in the current database, then forgotten. The client receives the DO tag, no rows, and the notices from RAISE; an error comes out with its SQLSTATE, and, outside a client transaction block, the whole block is undone, like a CALL:
CREATE TABLE t (i int);
DO $$
DECLARE n int;
BEGIN
INSERT INTO t SELECT i FROM generate_series(1, 3) AS g(i);
SELECT count(*) INTO n FROM t;
RAISE NOTICE '% lignes', n;
END $$;
DO $$ BEGIN INSERT INTO t VALUES (4); RAISE EXCEPTION 'non'; END $$;
SELECT count(*) FROM t;CREATE TABLE
NOTICE: 3 lignes
DO
ERROR: non (P0001)
count(*)
----------
3LANGUAGE plpgsql can be written before or after the body; LANGUAGE sql is rejected (0A000, language "sql" does not support inline code execution, like PostgreSQL), as are COMMIT / ROLLBACK in the block.
39.3 The PL/pgSQL language#
Block structure#
[<<label>>]
[DECLARE
declarations]
BEGIN
statements
[EXCEPTION
WHEN condition THEN statements …]
END [label];Blocks can be nested; a variable of an inner block hides the variable of the same name in the enclosing block. Comments are written -- … and /* … */.
Declarations#
DECLARE
v_total numeric(12,2); -- NULL at the start
v_compte integer := 0; -- or DEFAULT 0, or = 0
c_sep CONSTANT text := ' : ';
v_nn integer NOT NULL := 0;
v_prix article.prix%TYPE; -- type of a column
v_copie v_prix%TYPE; -- type of another variable
a article%ROWTYPE; -- a row of the table: a.code, a.prix…
r RECORD; -- row of a query, in a FOR loop
p ALIAS FOR $1; -- name given to an anonymous parameter
c_art CURSOR FOR SELECT code FROM article ORDER BY id;CONSTANT and NOT NULL require an initial value; they are not checked afterwards.
Assignment and expressions#
v_total := v_total + 1; -- or v_total = v_total + 1
a.stock := 0; -- field of a record
NEW.modifie_le := now(); -- column of a trigger's rowAny SQL expression is allowed, subqueries included: RETURN (SELECT count(*) FROM client);, IF EXISTS (SELECT 1 FROM commande WHERE client_id = p) THEN ….
Conditions#
CREATE FUNCTION categorie_prix(p numeric) RETURNS text LANGUAGE plpgsql AS $$
BEGIN
IF p IS NULL THEN
RETURN 'inconnu';
ELSIF p < 2 THEN
RETURN 'petit';
ELSIF p < 4 THEN
RETURN 'moyen';
ELSE
RETURN 'grand';
END IF;
END $$;
SELECT code, categorie_prix(prix) FROM article ORDER BY id; code | categorie_prix(prix)
--------+----------------------
STY-01 | petit
CAH-96 | moyen
CLA-A4 | grandCASE chooses a branch according to a value, or according to conditions:
CASE n
WHEN 1, 7 THEN r := 'week-end';
WHEN 2, 3, 4, 5, 6 THEN r := 'semaine';
ELSE r := 'invalide';
END CASE;
CASE WHEN n > 100 THEN r := r || '!'; ELSE NULL; END CASE;In a function jour(n) that returns r, SELECT jour(1), jour(3), jour(200) returns week-end | semaine | invalide!.
A CASE where no branch matches and that has no ELSE gives 20000 (Case not found for CASE statement). NULL; is the statement that does nothing.
Loops#
| Form | Role |
|---|---|
LOOP … END LOOP | endless loop, left with EXIT or RETURN |
WHILE condition LOOP … END LOOP | while the condition is true |
FOR i IN [REVERSE] a .. b [BY step] LOOP … END LOOP | integer counter, local to the loop; bounds evaluated once |
FOR r IN query LOOP … END LOOP | one iteration per row; r is a RECORD or a list of variables |
FOR r IN cursor LOOP … END LOOP | same, on a declared cursor |
EXIT [label] [WHEN condition] | leaves the loop (or the labeled loop) |
CONTINUE [label] [WHEN condition] | moves on to the next iteration |
CREATE FUNCTION boucles(n integer) RETURNS text LANGUAGE plpgsql AS $$
DECLARE
s integer := 0;
i integer := 0;
t text := '';
BEGIN
FOR j IN 1 .. n LOOP s := s + j; END LOOP;
FOR j IN REVERSE n .. 1 BY 2 LOOP t := t || j || ' '; END LOOP;
WHILE i < 3 LOOP
i := i + 1;
CONTINUE WHEN i = 2;
t := t || 'w' || i || ' ';
END LOOP;
<<dehors>>
LOOP
i := i + 1;
LOOP
EXIT dehors WHEN i >= 6;
EXIT;
END LOOP;
END LOOP dehors;
RETURN s || ' / ' || t || '/ ' || i;
END $$;
SELECT boucles(5); -- 15 / 5 3 1 w1 w3 / 6CREATE FUNCTION liste_codes(p_prix_min numeric) RETURNS text LANGUAGE plpgsql AS $$
DECLARE
r RECORD;
res text := '';
BEGIN
FOR r IN SELECT code, prix FROM article WHERE prix >= p_prix_min ORDER BY code LOOP
res := res || r.code || '=' || r.prix || ';';
END LOOP;
IF NOT FOUND THEN res := 'aucun'; END IF;
RETURN res;
END $$;
SELECT liste_codes(2), liste_codes(100); -- CAH-96=2.50;CLA-A4=4.90; | aucun39.4 Queries in a routine#
SELECT … INTO#
SELECT prix, stock INTO v_prix, v_stock FROM article WHERE id = p_article;
SELECT * INTO a FROM article WHERE id = p_id; -- a article%ROWTYPE
SELECT nom INTO STRICT v FROM client WHERE ville = p_ville;The query stores its first row in the targets; with no row, the targets are NULL and FOUND is false. With STRICT, the query must return exactly one row: P0002 (no_data_found, query returned no rows) with no row, P0003 (too_many_rows) if there are several.
In a PL/pgSQL body, a SELECT without INTO is rejected at creation (42601, query has no destination for result data): write PERFORM to discard its result.
Writes, FOUND and ROW_COUNT#
INSERT, UPDATE, DELETE and TRUNCATE are written as is in the body. After them, and after SELECT … INTO, PERFORM, FETCH and a FOR loop, the FOUND variable tells whether at least one row was processed, and GET DIAGNOSTICS gives the number:
UPDATE article SET stock = stock WHERE prix > 2;
GET DIAGNOSTICS n = ROW_COUNT; -- 2
PERFORM 1 FROM client WHERE ville = 'Lyon';
IF FOUND THEN … END IF;INSERT … VALUES (…) RETURNING column INTO variable returns the value of a column of the inserted row, in particular the identifier that a serial column has just assigned (see passer_commande, 39.2).
Cursors#
CREATE FUNCTION codes() RETURNS text LANGUAGE plpgsql AS $$
DECLARE
c1 CURSOR FOR SELECT code FROM article ORDER BY id;
v text;
res text := '';
BEGIN
OPEN c1;
LOOP
FETCH c1 INTO v;
EXIT WHEN NOT FOUND;
res := res || v || ' ';
END LOOP;
CLOSE c1;
RETURN res;
END $$;
SELECT codes(); -- STY-01 CAH-96 CLA-A4A cursor is declared without arguments, read forward (FETCH [NEXT | FORWARD] [FROM] c INTO …) and closed with CLOSE. The FOR r IN c1 LOOP loop opens it, runs through it and closes it by itself.
Types resolved at creation#
The %TYPE and %ROWTYPE types, and the fields of a RECORD, are resolved when the routine is created: the tables named must exist, and a table modified afterwards (column added, type changed) is not re-read; recreate the routine with CREATE OR REPLACE. A table named only in an expression is looked up only at run time (42P01 if it is missing then).
39.5 Messages and errors#
RAISE: notices#
DO $$
BEGIN
RAISE NOTICE 'a % b %% c %', 1, NULL;
RAISE WARNING 'stock bas pour %', 'CLA-A4';
RAISE INFO 'étape % terminée', 2;
RAISE DEBUG 'non transmis';
END $$;NOTICE: a 1 b % c <NULL>
WARNING: stock bas pour CLA-A4
INFO: étape 2 terminée
DOEach % in the format is replaced by the next argument; %% writes a %; a NULL argument is written <NULL>. The notice reaches the client with its level (NOTICE, WARNING, INFO), including from a function called in a query or from a trigger; psql displays it, psycopg2 stores it in connection.notices. Notices emitted before an error are also sent, before the error, as in PostgreSQL: DO $$ BEGIN RAISE NOTICE 'a'; RAISE EXCEPTION 'c'; END $$ displays NOTICE: a then the error P0001. RAISE DEBUG and RAISE LOG send nothing.
RAISE: errors#
RAISE EXCEPTION 'erreur simple %', 42; -- P0001
RAISE 'forme courte %', 1; -- P0001
RAISE EXCEPTION 'paramètre invalide' USING ERRCODE = '22023'; -- 22023
RAISE EXCEPTION 'article % inconnu', p USING ERRCODE = 'no_data_found'; -- P0002
RAISE unique_violation USING MESSAGE = 'doublon métier'; -- 23505
RAISE SQLSTATE 'P0042'; -- P0042, message "P0042"An error interrupts the routine and the statement that called it. Its SQLSTATE is P0001 (raise_exception) by default, or the one given by ERRCODE: a five-character code, or a PostgreSQL condition name. Choosing your own codes in class P0 (P0100…) lets the application recognize its business errors.
The DETAIL, HINT, COLUMN, CONSTRAINT, TABLE… options of USING are accepted with no effect: only the message and the SQLSTATE reach the client.
ASSERT condition [, message] raises P0004 when the condition is false or NULL, with the given message or assertion failed.
Catching an error: EXCEPTION#
CREATE FUNCTION ajoute_article(p_code text, p_libelle text, p_prix numeric)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
v_msg text;
v_etat text;
BEGIN
INSERT INTO article (code, libelle, prix) VALUES (p_code, p_libelle, p_prix);
RETURN 'créé';
EXCEPTION
WHEN unique_violation THEN
RETURN 'existe déjà';
WHEN not_null_violation OR check_violation THEN
GET STACKED DIAGNOSTICS v_msg = MESSAGE_TEXT, v_etat = RETURNED_SQLSTATE;
RETURN 'refusé (' || v_etat || ') : ' || v_msg;
WHEN OTHERS THEN
RETURN 'autre : ' || SQLSTATE;
END $$;
SELECT ajoute_article('GOM-04', 'Gomme', 0.80); -- créé
SELECT ajoute_article('GOM-04', 'Gomme', 0.80); -- existe déjà
SELECT ajoute_article('X', NULL, 1); -- refusé (23502) : Column 'libelle' cannot be null
SELECT ajoute_article('Y', 'Prix négatif', -1);
-- refusé (23514) : new row for relation "article" violates check constraint "CONSTRAINT_1"
SELECT ajoute_article('GOM-06', 'Gomme', 99999999999); -- autre : 22003| Condition | Catches |
|---|---|
| condition name | unique_violation (23505), foreign_key_violation (23503), not_null_violation (23502), check_violation (23514), division_by_zero (22012), no_data_found (P0002), too_many_rows (P0003), raise_exception (P0001)… |
| class name | integrity_constraint_violation (class 23)… |
SQLSTATE 'xxxxx' | that exact code, for example a business code P0100 |
OTHERS | any error |
cond1 OR cond2 | either one |
In the handler, SQLERRM is the error message and SQLSTATE its code; GET STACKED DIAGNOSTICS v = MESSAGE_TEXT | RETURNED_SQLSTATE stores them in variables:
CREATE FUNCTION division(a integer) RETURNS integer LANGUAGE plpgsql AS $$
BEGIN
RETURN 10 / a;
EXCEPTION WHEN division_by_zero THEN
RAISE NOTICE 'journalisé : % (%)', SQLERRM, SQLSTATE;
RETURN NULL;
END $$;
SELECT division(0);NOTICE: journalisé : division by zero (22012)
division(0)
-------------
A bare RAISE; in a handler re-raises the caught error with its message and SQLSTATE: with EXCEPTION WHEN OTHERS THEN RAISE;, SELECT f(0) comes out as ERROR: division by zero (22012).
As in PostgreSQL, a block with an EXCEPTION clause is a subtransaction: an implicit savepoint is set on entering the block, and the handler goes back to it before running. What the block wrote before the error is undone; what the statements before the block wrote, and the handler itself, is kept. Variables keep their values.
CREATE TABLE trace (id int, texte text);
DO $$
BEGIN
INSERT INTO trace VALUES (1, 'gardé');
BEGIN
INSERT INTO trace VALUES (2, 'défait');
RAISE EXCEPTION 'stop';
EXCEPTION WHEN OTHERS THEN
INSERT INTO trace VALUES (3, 'gardé aussi');
END;
END $$;
SELECT * FROM trace ORDER BY id; id | texte
----+-------------
1 | gardé
3 | gardé aussiThis savepoint is set only inside a transaction: that of a BEGIN … COMMIT block, or the implicit transaction of a call made outside a block by a PostgreSQL-language session. It is not set in the body of a trigger, nor for a routine called outside a transaction by a MIRAJ-language session: there, the handler undoes only the failing statement.
Another difference from PostgreSQL: the handler chosen is the most specific one that covers the error (a code, then a class, then OTHERS), not the first one written.
39.6 Triggers#
A trigger runs a function for each row inserted, modified or deleted in a table. It is created in two steps, as in PostgreSQL: a trigger function, then the trigger that attaches it to a table.
CREATE [OR REPLACE] TRIGGER name
{BEFORE | AFTER} {INSERT | UPDATE [OF column [, …]] | DELETE} [OR …]
ON table
FOR EACH ROW
[WHEN (condition)]
EXECUTE {FUNCTION | PROCEDURE} function ( [arguments] );
DROP TRIGGER [IF EXISTS] name ON table [CASCADE | RESTRICT];The trigger function#
It is a PL/pgSQL function with no parameters, declared RETURNS trigger. It has access to:
| Variable | Content |
|---|---|
NEW | the new row (INSERT, UPDATE); NULL for a DELETE |
OLD | the old row (UPDATE, DELETE); NULL for an INSERT |
TG_OP | 'INSERT', 'UPDATE' or 'DELETE' |
TG_NAME, TG_WHEN, TG_LEVEL | trigger name, 'BEFORE' or 'AFTER', 'ROW' |
TG_TABLE_NAME (or TG_RELNAME), TG_TABLE_SCHEMA | table and schema |
TG_NARGS, TG_ARGV[i] | arguments written in CREATE TRIGGER |
Its return value decides the fate of the row:
| Return | In a BEFORE trigger | In an AFTER trigger |
|---|---|---|
RETURN NEW | the row is written, with the changes made to NEW | ignored |
RETURN OLD | DELETE: the row is deleted; UPDATE: the old row is kept as is | ignored |
RETURN NULL | the row is skipped: neither inserted, nor modified, nor deleted, nor counted | ignored |
Reaching the end of the function without RETURN gives 2F005. Calling the function directly (SELECT f()) is rejected (0A000, trigger functions can only be called as triggers).
Completing a row (BEFORE)#
ALTER TABLE client ADD COLUMN modifie_le timestamp;
CREATE FUNCTION horodate() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.modifie_le := now();
RETURN NEW;
END $$;
CREATE TRIGGER client_horodate BEFORE INSERT OR UPDATE ON client
FOR EACH ROW EXECUTE FUNCTION horodate();
UPDATE client SET ville = 'Lyon' WHERE id = 1;
SELECT id, ville, modifie_le IS NOT NULL AS horodate FROM client ORDER BY id; id | ville | horodate
----+-------------+----------
1 | Lyon | t
2 | Constantine | f
3 | Lyon | fControlling a write#
CREATE FUNCTION controle_prix() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.prix <= 0 THEN
RAISE EXCEPTION 'prix invalide pour % : %', NEW.code, NEW.prix
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER article_prix BEFORE INSERT OR UPDATE OF prix ON article
FOR EACH ROW EXECUTE FUNCTION controle_prix();
UPDATE article SET prix = 0 WHERE id = 1;
-- ERROR: prix invalide pour STY-01 : 0.00 (23514)The error raised by the trigger makes the statement that fired it fail; nothing is written.
Keeping a journal (AFTER)#
CREATE TABLE journal (id serial PRIMARY KEY, quand timestamp NOT NULL DEFAULT now(),
table_nom text, operation text, cle integer, detail text);
CREATE FUNCTION journalise() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO journal (table_nom, operation, cle, detail)
VALUES (TG_TABLE_NAME, TG_OP, NEW.id, 'stock ' || NEW.stock);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO journal (table_nom, operation, cle, detail)
VALUES (TG_TABLE_NAME, TG_OP, NEW.id, 'stock ' || OLD.stock || ' -> ' || NEW.stock);
RETURN NEW;
ELSE
INSERT INTO journal (table_nom, operation, cle, detail)
VALUES (TG_TABLE_NAME, TG_OP, OLD.id, 'supprimé');
RETURN OLD;
END IF;
END $$;
CREATE TRIGGER article_journal AFTER INSERT OR UPDATE OR DELETE ON article
FOR EACH ROW EXECUTE FUNCTION journalise();
INSERT INTO article (code, libelle, prix, stock) VALUES ('GOM-05', 'Gomme', 0.80, 50);
UPDATE article SET stock = stock - 5 WHERE code = 'GOM-05';
DELETE FROM article WHERE code = 'GOM-05';
SELECT table_nom, operation, detail FROM journal ORDER BY id; table_nom | operation | detail
-----------+-----------+----------------
article | INSERT | stock 50
article | UPDATE | stock 50 -> 45
article | DELETE | suppriméThe trigger's writes are part of the statement's transaction: a ROLLBACK undoes them along with it.
Skipping a row, keeping a total#
CREATE FUNCTION ignore_petites() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.quantite <= 1 THEN RETURN NULL; END IF; -- row skipped, no error
RETURN NEW;
END $$;
CREATE TRIGGER ligne_filtre BEFORE INSERT ON ligne_commande
FOR EACH ROW EXECUTE FUNCTION ignore_petites();
INSERT INTO ligne_commande VALUES (3, 1, 1, 1.20), (3, 3, 2, 4.90); -- INSERT 0 1An AFTER trigger keeps another table up to date:
CREATE FUNCTION maj_stock() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
UPDATE article SET stock = stock - NEW.quantite WHERE id = NEW.article_id;
RETURN NULL;
END $$;
CREATE TRIGGER ligne_stock AFTER INSERT ON ligne_commande
FOR EACH ROW EXECUTE FUNCTION maj_stock();
SELECT stock FROM article WHERE id = 2; -- 195
INSERT INTO ligne_commande VALUES (2, 2, 10, 2.50);
SELECT stock FROM article WHERE id = 2; -- 185UPDATE OF, WHEN, arguments#
-- only when the stock changes and falls below 5
CREATE TRIGGER quand_stock AFTER UPDATE ON article FOR EACH ROW
WHEN (OLD.stock IS DISTINCT FROM NEW.stock AND NEW.stock < 5)
EXECUTE FUNCTION journalise();
-- arguments read by TG_ARGV[0], TG_ARGV[1]; TG_NARGS is 2
CREATE FUNCTION avec_args() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE '% % : % arguments, % et %', TG_NAME, TG_WHEN || '/' || TG_LEVEL || '/' || TG_OP,
TG_NARGS, TG_ARGV[0], TG_ARGV[1];
RAISE NOTICE 'table %.%', TG_TABLE_SCHEMA, TG_TABLE_NAME;
RETURN NEW;
END $$;
CREATE TRIGGER cmd_args BEFORE UPDATE ON commande FOR EACH ROW
EXECUTE FUNCTION avec_args('alerte', 7);
UPDATE commande SET etat = 'livrée' WHERE id = 1;NOTICE: cmd_args BEFORE/ROW/UPDATE : 2 arguments, alerte et 7
NOTICE: table ventes.commande
UPDATE 1The arguments are passed as text (SHOW TRIGGERS writes avec_args('alerte', '7')).
UPDATE OF column fires when the value of one of these columns changes, not when the column is merely named in the SET (PostgreSQL).
Rules#
- The same function can serve several triggers, on several tables; two tables can have a trigger of the same name. A name already taken on the table:
42710(trigger "client_horodate" for relation "client" already exists). - Replacing the function (
CREATE OR REPLACE FUNCTION) updates all the triggers that call it. - The triggers of a table, for a given timing and event, run in the alphabetical order of their names, as in PostgreSQL.
DROP FUNCTIONon a function that is still attached is rejected (2BP01,cannot drop function horodate() because other objects depend on it: trigger client_horodate on table client (use DROP ... CASCADE));DROP FUNCTION … CASCADEdrops the function and its triggers.- A trigger cannot write the table that fired it (
XX000,Can't update table 'commande' in stored function/trigger because it is already used by statement which invoked this stored function/trigger); to change the current row, modifyNEWin aBEFOREtrigger. EXECUTE FUNCTIONrequires aRETURNS triggerfunction (42P17,function prix_ttc() must return type trigger) that exists (42883,function absente() does not exist).TG_TABLE_NAMEand the otherTG_variables are fixed when the trigger is created.
39.7 Inspecting routines and triggers#
| Query or command | Returns |
|---|---|
\df, \df+, \sf name (psql) | list of routines, their definition |
pg_catalog.pg_proc | proname, prokind (f function, p procedure), prosrc (body as written), provolatile, proisstrict, prosecdef, prolang |
pg_get_functiondef(oid) | the complete CREATE OR REPLACE FUNCTION … statement |
pg_get_function_arguments(oid), pg_get_function_result(oid) | parameters and result type |
SHOW CREATE FUNCTION name, SHOW CREATE PROCEDURE name | the same text as pg_get_functiondef |
information_schema.routines | routine_name, routine_type |
pg_catalog.pg_trigger, \d table | one row per trigger (tgname, tgrelid, tgfoid) |
pg_get_triggerdef(oid), SHOW CREATE TRIGGER name | the CREATE TRIGGER … statement |
SELECT proname, prokind, pg_get_function_arguments(oid) AS args,
pg_get_function_result(oid) AS res
FROM pg_proc WHERE proname IN ('prix_ttc', 'passer_commande', 'f_out') ORDER BY proname; proname | prokind | args | res
-----------------+---------+-------------------------------------------------------------------------------------------+---------------
f_out | f | a integer, OUT carre integer | integer
passer_commande | p | p_client integer, p_article integer, p_qte integer, INOUT p_commande integer DEFAULT NULL |
prix_ttc | f | prix numeric(10,2), taux numeric(4,2) DEFAULT 0.20 | numeric(10,2)SELECT proname, provolatile, proisstrict, prosecdef FROM pg_proc
WHERE proname IN ('prix_ttc', 'f_strict') ORDER BY 1;
SELECT pg_get_functiondef('prix_ttc'::regproc);
SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgname = 'article_prix'; proname | provolatile | proisstrict | prosecdef
----------+-------------+-------------+-----------
f_strict | v | t | f
prix_ttc | i | f | f
CREATE OR REPLACE FUNCTION ventes.prix_ttc(prix numeric(10,2), taux numeric(4,2) DEFAULT 0.20)
RETURNS numeric(10,2)
LANGUAGE plpgsql
IMMUTABLE
AS $function$
DECLARE
resultat numeric(10,2);
BEGIN
resultat := prix * (1 + taux);
RETURN round(resultat, 2);
END;
$function$
CREATE TRIGGER article_prix BEFORE INSERT OR UPDATE OF prix ON ventes.article FOR EACH ROW EXECUTE FUNCTION controle_prix()SHOW CREATE PROCEDURE likewise writes CREATE OR REPLACE PROCEDURE … AS $procedure$…$procedure$, and SHOW CREATE TRIGGER the CREATE TRIGGER statement as it was written (table without schema), in its SQL Original Statement column.
SELECT routine_name, routine_type, data_type FROM information_schema.routines
WHERE routine_schema = 'ventes' AND routine_name IN ('prix_ttc', 'passer_commande', 'horodate')
ORDER BY 1;
SELECT trigger_name, event_manipulation, action_timing, action_statement, action_condition
FROM information_schema.triggers WHERE event_object_table = 'article'
ORDER BY trigger_name, event_manipulation; routine_name | routine_type | data_type
-----------------+--------------+-----------
horodate | FUNCTION | trigger
passer_commande | PROCEDURE |
prix_ttc | FUNCTION | numeric
trigger_name | event_manipulation | action_timing | action_statement | action_condition
-----------------+--------------------+---------------+----------------------------------+--------------------------------------------------------
article_journal | DELETE | AFTER | EXECUTE FUNCTION journalise() |
article_journal | INSERT | AFTER | EXECUTE FUNCTION journalise() |
article_journal | UPDATE | AFTER | EXECUTE FUNCTION journalise() |
article_prix | INSERT | BEFORE | EXECUTE FUNCTION controle_prix() |
article_prix | UPDATE | BEFORE | EXECUTE FUNCTION controle_prix() |
quand_stock | UPDATE | AFTER | EXECUTE FUNCTION journalise() | OLD.stock IS DISTINCT FROM NEW.stock AND NEW.stock < 5The body is returned character for character, as it was written. The header keeps the precision of the parameters (numeric(10,2), which PostgreSQL would reduce to numeric), because MIRAJ applies it.
information_schema.triggers and SHOW TRIGGERS show the trigger as it was written: its name (article_prix), one row per event as in PostgreSQL, and as its action EXECUTE FUNCTION controle_prix() (column action_statement or Statement; the WHEN condition is in action_condition). The body that MIRAJ actually runs, and the name it gives internally to the trigger of each event (article_prix$article$insert), do not appear there; an instance reopened in the MIRAJ language shows them.
The miraj-dump tool exports routines and triggers in PL/pgSQL, in a script that psql -f reloads (40. Catalog and tools).
39.8 Privileges#
| Action | Privilege, on the routine's schema (or on the routine itself) |
|---|---|
| create a function or a procedure | CREATE ROUTINE (schema only) |
| replace or drop it | ALTER ROUTINE (drop it: on the routine as well) |
| call it | EXECUTE (schema or routine) |
| create a trigger | TRIGGER on the table |
Without the privilege: 42501 (permission denied for schema ventes) at creation, 42501 (permission denied for function nb_clients) at call time.
- A routine runs by default with the rights of the caller (
SECURITY INVOKER), as in PostgreSQL: besidesEXECUTE, it needs the rights on the tables that the body reads or writes. WithSECURITY DEFINER, the body runs with the rights of its creator: this is the way to give controlled access to a table without granting any right on it. - A trigger always runs with the rights of its creator: an account that has only
INSERTonarticledoes feed thejournaltable through thearticle_journaltrigger, without any right onjournal.
-- as root
CREATE USER vendeur IDENTIFIED BY 'vendeur123';
CREATE FUNCTION nb_clients() RETURNS bigint LANGUAGE sql STABLE AS $$ SELECT count(*) FROM client $$;
CREATE FUNCTION nb_clients_def() RETURNS bigint LANGUAGE sql STABLE SECURITY DEFINER
AS $$ SELECT count(*) FROM client $$;
GRANT EXECUTE ON FUNCTION ventes.nb_clients_def() TO vendeur;
GRANT INSERT, SELECT ON ventes.article TO vendeur;
-- connected as vendeur
SELECT nb_clients();
SELECT nb_clients_def();
CREATE FUNCTION f_v() RETURNS int LANGUAGE sql AS 'SELECT 1';
INSERT INTO article (code, libelle, prix, stock) VALUES ('REG-30', 'Règle', 1.90, 10);
SELECT * FROM journal;ERROR: permission denied for function nb_clients (42501)
nb_clients_def()
------------------
3
ERROR: permission denied for schema ventes (42501)
INSERT 0 1
ERROR: permission denied for table journal (42501)The insert did write its INSERT | stock 10 row into journal, through the trigger.
Privileges are granted per schema (GRANT EXECUTE ON ventes.* TO app) or routine by routine (GRANT EXECUTE ON FUNCTION ventes.nb_clients() TO app, GRANT ALTER ROUTINE ON PROCEDURE …): see 38.3.
39.9 Unsupported features and differences#
Rejected at creation (0A000)#
The message names the construct, for example EXECUTE (dynamic SQL) is not supported in routines of the PostgreSQL language.
| Construct | Note |
|---|---|
several OUT parameters in a function that is not SETOF record, RETURNS record, table-typed parameter | a procedure, however, accepts several OUT parameters |
EXECUTE 'text' (dynamic SQL), FOR … IN EXECUTE | |
arrays (integer[]), FOREACH | |
COMMIT, ROLLBACK in a procedure | the transaction belongs to the caller |
definition statements (CREATE TABLE…) in the body | |
UPDATE … RETURNING, DELETE … RETURNING, INSERT … ON CONFLICT in the body | INSERT … VALUES … RETURNING … INTO is supported |
cursors with arguments, OPEN … FOR, refcursor, FETCH other than NEXT, MOVE | |
VARIADIC, polymorphic types (anyelement…) | |
LANGUAGE c, internal, plpython… | |
SET parameter = … clause of CREATE FUNCTION, BEGIN ATOMIC, WINDOW, TRANSFORM | |
FOR EACH STATEMENT triggers (or without FOR EACH ROW), INSTEAD OF, TRUNCATE, CREATE CONSTRAINT TRIGGER, REFERENCING | row-level triggers, BEFORE or AFTER, only |
ALTER FUNCTION, ALTER PROCEDURE, ALTER TRIGGER | CREATE OR REPLACE, or drop and recreate |
DROP FUNCTION a, b | one routine per statement |
COMMENT ON FUNCTION | comments on tables and columns only |
CREATE EVENT, ALTER EVENT | no scheduled event in the PostgreSQL language |
Other rejections#
| Form | Response |
|---|---|
SELECT without INTO in a PL/pgSQL body | 42601 at creation (query has no destination for result data) |
CREATE FUNCTION without LANGUAGE | 42P13 (no language specified) |
CREATE FUNCTION IF NOT EXISTS | 42601; write CREATE OR REPLACE |
SELECT * FROM f(…) AS t (a int, b text), … WITH ORDINALITY after a stored function | 0A000 |
ext.x (variable qualified by the label of its block) | 42703 at run time; give the variables distinct names |
ALTER TABLE t DISABLE TRIGGER name | 0A000; drop and recreate the trigger |
Behavior differences#
- Subtransaction of an
EXCEPTIONblock: not set in a trigger, nor outside a transaction for a MIRAJ-language session; the handler undoes only the failing statement there (39.5). - No overloading: one routine per name and per schema; the types written in
DROP FUNCTION name(types)are ignored. - No recursion: a function that calls itself, directly or not, fails at run time (
XX000,Recursive stored functions and triggers are not allowed). - Table in use: a function cannot write a table that is read by the statement that calls it (
SELECT f() FROM clientwherefmodifiesclient), nor can a trigger write its own table (XX000). - Name of a system function: a function created under the name of a built-in function (
upper,now…) is never called without its schema: as in PostgreSQL, wherepg_catalogcomes before the current schema,upper(x)calls the built-in function. Call yours by its qualified name:public.upper(x)(publicdesignates the current schema) orschema.upper(x).public.f(…)calls the stored functionfof the current schema; if there is none, the built-in function of that name (public.length('abc')). - Function in another schema: it is named
schema.f(…);search_pathretains only one schema (37.8). - Identical calls: within one statement, two calls with identical constant arguments are run only once.
- Types:
numericwithout precision isnumeric(38,18); the%TYPE/%ROWTYPEtypes are fixed at creation (39.4). - Variables and columns: a variable hides a column of the same name in the routine's queries (PostgreSQL reports the ambiguity); prefix parameters (
p_) and variables (v_). RAISE:DETAILandHINTare not passed to the client.UPDATE OF columnfires on a value change (39.6).OUTparameters of a procedure: their argument is required inCALL, even if it is ignored (39.2).
The condensed list is in chapter 29.
See also#
- 34. DML language for
RETURNINGandON CONFLICT. - 37. Transactions and session for transactions, conflicts and retries.
- 38. Accounts and privileges for
EXECUTE,CREATE ROUTINEandSECURITY DEFINER. - 40. Catalog and tools for
pg_proc,pg_triggerand export.