Mirajv1.0
EN

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.88

A 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#

ElementSupported
CREATE OR REPLACEreplaces the function of the same name; without it, an existing function gives 42723
schema.namethe function belongs to a schema; without a schema, to the current schema
parametersIN (default), OUT, INOUT; without a name, they are read as $1, $2…; types from 32. Data types and table.column%TYPE
default valuesDEFAULT value or = value; the last arguments of a call can then be omitted
RETURNS typea 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 parameterthe function returns its final value (RETURNS can be omitted)
LANGUAGE plpgsql | sqlrequired (42P13 otherwise), before or after the body
bodyAS $$…$$, AS $tag$…$tag$, AS '…'; for LANGUAGE sql, also the form RETURN expression
IMMUTABLE, STABLE, VOLATILEkept and displayed (pg_proc.provolatile)
STRICTif an argument is NULL, the function returns NULL without running its body
SECURITY INVOKER | DEFINERrights of the caller (default) or of the creator, see 39.8
LEAKPROOF, PARALLEL …, COST n, ROWS naccepted 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 |        21

Functions 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.00

Function 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): otherwise 42883. It is not run at that time.
  • It is run for each written row that does not supply the column: INSERT of one or more rows, column omitted or DEFAULT written, INSERT … SELECT, COPY … FROM STDIN, and UPDATE … 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 the BEFORE INSERT triggers, which see it in NEW, 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-row INSERT).
  • The session that writes without supplying the column must have the EXECUTE privilege 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_default and SHOW CREATE TABLE return 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. numeric without precision is numeric(38,18): SELECT num_libre(1.5) of a function RETURNS numeric that doubles its argument displays 3.000000000000000000.
  • A function RETURNS void returns 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);      -- 25

Functions 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:

FormRows returned
LANGUAGE sqlthe 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.50
CREATE 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.00
CREATE 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), of TABLE (…) or of the OUT parameters; for SETOF 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 expression in such a function is rejected at creation (42804, RETURN cannot have a parameter in function returning set).
  • From psycopg2, cursor.callproc("paliers", (7,)) sends SELECT * 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 OUT parameters, 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 sql procedure runs only writes.
  • A procedure can call another one (CALL in 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;     -- 0

Outside 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 undone

In 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.000000000000000000

A 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(*)
----------
        3

LANGUAGE 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 row

Any 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 | grand

CASE 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#

FormRole
LOOP … END LOOPendless loop, left with EXIT or RETURN
WHILE condition LOOP … END LOOPwhile the condition is true
FOR i IN [REVERSE] a .. b [BY step] LOOP … END LOOPinteger counter, local to the loop; bounds evaluated once
FOR r IN query LOOP … END LOOPone iteration per row; r is a RECORD or a list of variables
FOR r IN cursor LOOP … END LOOPsame, 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 / 6
CREATE 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; | aucun

39.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-A4

A 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
DO

Each % 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
ConditionCatches
condition nameunique_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 nameintegrity_constraint_violation (class 23)…
SQLSTATE 'xxxxx'that exact code, for example a business code P0100
OTHERSany error
cond1 OR cond2either 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é aussi

This 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:

VariableContent
NEWthe new row (INSERT, UPDATE); NULL for a DELETE
OLDthe old row (UPDATE, DELETE); NULL for an INSERT
TG_OP'INSERT', 'UPDATE' or 'DELETE'
TG_NAME, TG_WHEN, TG_LEVELtrigger name, 'BEFORE' or 'AFTER', 'ROW'
TG_TABLE_NAME (or TG_RELNAME), TG_TABLE_SCHEMAtable and schema
TG_NARGS, TG_ARGV[i]arguments written in CREATE TRIGGER

Its return value decides the fate of the row:

ReturnIn a BEFORE triggerIn an AFTER trigger
RETURN NEWthe row is written, with the changes made to NEWignored
RETURN OLDDELETE: the row is deleted; UPDATE: the old row is kept as isignored
RETURN NULLthe row is skipped: neither inserted, nor modified, nor deleted, nor countedignored

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        | f

Controlling 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 1

An 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;                -- 185

UPDATE 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 1

The 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 FUNCTION on 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 … CASCADE drops 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, modify NEW in a BEFORE trigger.
  • EXECUTE FUNCTION requires a RETURNS trigger function (42P17, function prix_ttc() must return type trigger) that exists (42883, function absente() does not exist).
  • TG_TABLE_NAME and the other TG_ variables are fixed when the trigger is created.

39.7 Inspecting routines and triggers#

Query or commandReturns
\df, \df+, \sf name (psql)list of routines, their definition
pg_catalog.pg_procproname, 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 namethe same text as pg_get_functiondef
information_schema.routinesroutine_name, routine_type
pg_catalog.pg_trigger, \d tableone row per trigger (tgname, tgrelid, tgfoid)
pg_get_triggerdef(oid), SHOW CREATE TRIGGER namethe 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 < 5

The 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#

ActionPrivilege, on the routine's schema (or on the routine itself)
create a function or a procedureCREATE ROUTINE (schema only)
replace or drop itALTER ROUTINE (drop it: on the routine as well)
call itEXECUTE (schema or routine)
create a triggerTRIGGER 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: besides EXECUTE, it needs the rights on the tables that the body reads or writes. With SECURITY 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 INSERT on article does feed the journal table through the article_journal trigger, without any right on journal.
-- 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.

ConstructNote
several OUT parameters in a function that is not SETOF record, RETURNS record, table-typed parametera procedure, however, accepts several OUT parameters
EXECUTE 'text' (dynamic SQL), FOR … IN EXECUTE
arrays (integer[]), FOREACH
COMMIT, ROLLBACK in a procedurethe transaction belongs to the caller
definition statements (CREATE TABLE…) in the body
UPDATE … RETURNING, DELETE … RETURNING, INSERT … ON CONFLICT in the bodyINSERT … 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, REFERENCINGrow-level triggers, BEFORE or AFTER, only
ALTER FUNCTION, ALTER PROCEDURE, ALTER TRIGGERCREATE OR REPLACE, or drop and recreate
DROP FUNCTION a, bone routine per statement
COMMENT ON FUNCTIONcomments on tables and columns only
CREATE EVENT, ALTER EVENTno scheduled event in the PostgreSQL language

Other rejections#

FormResponse
SELECT without INTO in a PL/pgSQL body42601 at creation (query has no destination for result data)
CREATE FUNCTION without LANGUAGE42P13 (no language specified)
CREATE FUNCTION IF NOT EXISTS42601; write CREATE OR REPLACE
SELECT * FROM f(…) AS t (a int, b text), … WITH ORDINALITY after a stored function0A000
ext.x (variable qualified by the label of its block)42703 at run time; give the variables distinct names
ALTER TABLE t DISABLE TRIGGER name0A000; drop and recreate the trigger

Behavior differences#

  • Subtransaction of an EXCEPTION block: 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 client where f modifies client), 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, where pg_catalog comes before the current schema, upper(x) calls the built-in function. Call yours by its qualified name: public.upper(x) (public designates the current schema) or schema.upper(x). public.f(…) calls the stored function f of 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_path retains only one schema (37.8).
  • Identical calls: within one statement, two calls with identical constant arguments are run only once.
  • Types: numeric without precision is numeric(38,18); the %TYPE / %ROWTYPE types 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: DETAIL and HINT are not passed to the client.
  • UPDATE OF column fires on a value change (39.6).
  • OUT parameters of a procedure: their argument is required in CALL, even if it is ignored (39.2).

The condensed list is in chapter 29.

See also#