Mirajv1.0
FR

40. PostgreSQL : Routines et déclencheurs (PL/pgSQL)

Une instance en langage PostgreSQL accepte les fonctions et les procédures écrites en LANGUAGE plpgsql ou LANGUAGE sql, et les déclencheurs CREATE TRIGGER … EXECUTE FUNCTION f(). MIRAJ en prend en charge un sous-ensemble, décrit dans ce chapitre : les fonctions qui rendent une valeur, les fonctions qui rendent des lignes (RETURNS SETOF, RETURNS TABLE), les procédures, les blocs anonymes DO, les déclencheurs par ligne, et l'essentiel du langage PL/pgSQL (variables, conditions, boucles, requêtes, curseurs, RAISE, gestion des exceptions).

Ce qui sort de ce sous-ensemble est refusé à la création, avec le SQLSTATE 0A000 et un message qui nomme la construction : une routine acceptée s'exécute, rien n'est gardé de travers. La liste de ce qui est refusé est au 40.9.

Les exemples portent sur le schéma ventes (tables client, article, commande, ligne_commande) du chapitre 32. Prise en main, avec ses données de départ (articles STY-01 à 1.20, CAH-96 à 2.50, CLA-A4 à 4.90).


40.1 Fonctions#

Créer une fonction#

CREATE [OR REPLACE] FUNCTION [schéma.]nom ( [paramètre [, …]] )
  RETURNS {type | SETOF type | SETOF table | TABLE (colonne type [, …])}
  LANGUAGE {plpgsql | sql}
  [IMMUTABLE | STABLE | VOLATILE]
  [STRICT | RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT]
  [SECURITY {INVOKER | DEFINER}]
  AS $$ corps $$;

où paramètre est [IN | OUT | INOUT] [nom] type [{DEFAULT | =} valeur].

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

Une fonction s'emploie partout où une expression est admise : liste d'un SELECT, WHERE, valeurs d'un INSERT ou d'un UPDATE, requête d'une vue.

Éléments de l'en-tête#

ÉlémentPris en charge
CREATE OR REPLACEremplace la fonction de même nom ; sans lui, une fonction existante donne 42723
schéma.nomla fonction appartient à un schéma ; sans schéma, au schéma courant
paramètresIN (défaut), OUT, INOUT ; sans nom, ils se lisent $1, $2… ; types de 33. Types de données et table.colonne%TYPE
valeurs par défautDEFAULT valeur ou = valeur ; les derniers arguments d'un appel peuvent alors être omis
RETURNS typeun type de base, void, ou trigger (40.6)
RETURNS SETOF …, RETURNS TABLE (…)fonction qui rend des lignes, appelée dans un FROM (voir plus bas)
un paramètre OUT ou INOUTla fonction rend sa valeur finale (RETURNS peut être omis)
LANGUAGE plpgsql | sqlexigé (42P13 sinon), avant ou après le corps
corpsAS $$…$$, AS $etiquette$…$etiquette$, AS '…' ; pour LANGUAGE sql, aussi la forme RETURN expression
IMMUTABLE, STABLE, VOLATILEgardé et affiché (pg_proc.provolatile)
STRICTsi un argument est NULL, la fonction rend NULL sans exécuter son corps
SECURITY INVOKER | DEFINERdroits de l'appelant (défaut) ou du créateur, voir 40.8
LEAKPROOF, PARALLEL …, COST n, ROWS nacceptés sans effet

Il n'y a pas de surcharge : un schéma ne porte qu'une fonction par nom, quels que soient ses paramètres. Les noms sont insensibles à la casse (PRIX_TTC(1) appelle prix_ttc).

Fonctions LANGUAGE sql#

Le corps est une suite d'instructions SQL ; la fonction rend le résultat de son dernier SELECT (première ligne, NULL s'il ne rend aucune ligne). Les instructions qui précèdent sont des écritures.

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

Fonctions qui lisent les 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

Fonction en valeur par défaut#

Une fonction stockée peut donner la valeur par défaut d'une colonne, seule ou dans une 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);   -- remise lue dans la table remise, pour chaque ligne
SELECT * FROM devis;
 id | client | remise | plafond
----+--------+--------+---------
  1 |      1 |   0.10 |  100.00
  2 |      2 |   0.10 |  100.00
  • La fonction doit exister à la création de la table (CREATE TABLE, ALTER TABLE … ADD COLUMN, ALTER COLUMN … SET DEFAULT) : sinon 42883. Elle n'est pas exécutée à ce moment.
  • Elle est exécutée pour chaque ligne écrite qui ne fournit pas la colonne : INSERT d'une ou de plusieurs lignes, colonne omise ou DEFAULT écrit, INSERT … SELECT, COPY … FROM STDIN, et UPDATE … SET colonne = DEFAULT, ON CONFLICT DO UPDATE SET colonne = DEFAULT. Une fonction volatile (random(), compteur lu dans une table) rend donc une valeur par ligne, comme dans PostgreSQL. La valeur est calculée avant les déclencheurs BEFORE INSERT, qui la voient dans NEW, et avant les colonnes générées, qui peuvent la citer.
  • ALTER TABLE … ADD COLUMN … DEFAULT f() sur une table qui a déjà des lignes exécute la fonction pour chacune ; si elle lit la table modifiée, elle la voit telle qu'avant l'instruction.
  • Les tables que lit le corps de la fonction (et des fonctions qu'il appelle) sont verrouillées en lecture avec l'instruction qui écrit, comme celles d'une fonction appelée dans la requête : une écriture concurrente de ces tables attend la fin de l'instruction. La fonction peut lire la table écrite elle-même : elle y voit les lignes déjà écrites par l'instruction (DEFAULT (SELECT max(id) + 1 …) écrit dans une fonction numérote les lignes d'un INSERT de plusieurs lignes).
  • La session qui écrit sans fournir la colonne doit avoir le droit EXECUTE sur la fonction (42501, permission denied for function f), comme dans PostgreSQL ; une ligne qui fournit la colonne ne l'appelle pas.
  • La fonction est exécutée en lecture seule, comme tout appel ligne à ligne : une fonction qui écrit (UPDATE compteur …) fait échouer l'écriture de la ligne (0A000). Une numérotation se fait par une séquence (DEFAULT nextval('…'), 3.8) ou une colonne d'identité.
  • pg_attrdef, pg_get_expr, information_schema.columns.column_default et SHOW CREATE TABLE rendent l'appel (remise_courante()) ; une fonction ainsi appelée ne se supprime pas seule (2BP01, voir ci-dessous).
  • Le langage MIRAJ refuse une fonction stockée dans une valeur par défaut, comme la référence (3770, Default value expression of column 'b' contains a disallowed function) ; une table créée en langage PostgreSQL garde sa valeur par défaut, calculée aussi pour les écritures d'une session en langage MIRAJ.

Résultat, void, paramètre OUT#

  • La valeur rendue est convertie au type déclaré, précision comprise : une fonction RETURNS numeric(10,2) rend deux décimales. numeric sans précision vaut numeric(38,18) : SELECT num_libre(1.5) d'une fonction RETURNS numeric qui double son argument affiche 3.000000000000000000.
  • Une fonction RETURNS void rend NULL.
  • Atteindre la fin d'une fonction PL/pgSQL sans 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

Fonctions qui rendent des lignes (SETOF, TABLE)#

Une fonction déclarée RETURNS SETOF type, RETURNS SETOF table (une ligne de la table) ou RETURNS TABLE (colonne type, …) rend un ensemble de lignes ; elle s'appelle dans le FROM d'une requête, comme une table :

FormeLignes rendues
LANGUAGE sqlles lignes de la dernière requête du corps
RETURN QUERY requête; (PL/pgSQL)ajoute les lignes de la requête ; s'écrit plusieurs fois
RETURN NEXT expression;ajoute une ligne (fonction d'une seule colonne)
RETURN NEXT;ajoute une ligne faite des valeurs courantes des paramètres OUT ou des colonnes de TABLE (…)
RETURN;termine la fonction ; les lignes déjà ajoutées sont rendues
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
  • Les colonnes du résultat portent le nom des colonnes de la table (SETOF table), de TABLE (…) ou des paramètres OUT ; pour SETOF type, la colonne unique porte le nom de la fonction, ou celui que donne l'alias (AS p, AS x(p)). Chaque valeur est convertie au type déclaré.
  • Le résultat se filtre, se joint, se compte comme une table (WHERE, JOIN, count(*)). Une fonction qui ne rend aucune ligne donne un résultat vide.
  • Les arguments sont des constantes ou des paramètres liés.
  • Un appel dans une expression (SELECT paliers(3)) est refusé : 0A000, set-valued function called in context that cannot accept a set (call it in FROM).
  • RETURN expression dans une telle fonction est refusé à la création (42804, RETURN cannot have a parameter in function returning set).
  • Depuis psycopg2, cursor.callproc("paliers", (7,)) envoie SELECT * FROM paliers(7) et rend les trois lignes.

Supprimer#

DROP FUNCTION [IF EXISTS] [schéma.]nom [(types)] [CASCADE | RESTRICT];

La liste de types est acceptée sans effet (il n'y a qu'une fonction par nom). Une fonction absente donne 42883, ou un avis avec IF EXISTS. Une fonction appelée par des déclencheurs n'est supprimée qu'avec CASCADE, qui supprime aussi ces déclencheurs (40.6). De même pour une fonction appelée par la valeur par défaut d'une colonne, dans tout schéma : 2BP01 (cannot drop function f() because other objects depend on it (default value for column b of table t depends on function f())) ; avec CASCADE, ces valeurs par défaut sont retirées, les colonnes restent. Une seule routine par instruction.


40.2 Procédures et CALL#

CREATE [OR REPLACE] PROCEDURE [schéma.]nom ( [paramètre [, …]] )
  LANGUAGE {plpgsql | sql}
  [SECURITY {INVOKER | DEFINER}]
  AS $$ corps $$;

CALL [schéma.]nom ( [arguments] );
DROP PROCEDURE [IF EXISTS] [schéma.]nom [(types)];

Une procédure ne rend pas de valeur ; elle s'appelle par CALL (l'employer dans une expression donne 42883, comme appeler une fonction par CALL). Ses paramètres OUT et INOUT sont rendus au client sous la forme d'une ligne, comme dans 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)
  • Les derniers arguments peuvent être omis s'ils ont une valeur par défaut ; sinon tous sont exigés, y compris ceux des paramètres OUT, dont la valeur donnée est ignorée :
    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)
  • Une procédure LANGUAGE sql n'exécute que des écritures.
  • Une procédure peut en appeler une autre (CALL dans le corps) et appeler des fonctions.

Transactions et atomicité d'un appel#

COMMIT et ROLLBACK ne s'écrivent pas dans une procédure (refusés à la création, 0A000). La procédure s'exécute dans la transaction de l'appelant :

BEGIN;
CALL passer_commande(3, 1, 1);     -- NOTICE:  commande 7 créée pour le client 3
ROLLBACK;                           -- la commande, sa ligne et le stock sont défaits
SELECT count(*) FROM commande WHERE client_id = 3;     -- 0

Hors d'un bloc de transaction, un appel est atomique, comme dans PostgreSQL : le CALL (ou l'instruction qui appelle une fonction qui écrit, SELECT f(…)) s'exécute dans une transaction implicite, validée à la fin et annulée en entier si la routine échoue — y compris ce qu'elle a écrit avant l'erreur et ce qu'ont écrit les routines qu'elle appelle.

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 : l'UPDATE est défait

Dans un bloc BEGIN … COMMIT, c'est la transaction du client qui tranche : ROLLBACK défait tout l'appel.

Une routine qui écrit peut aussi recevoir un conflit de concurrence (40001) : l'application la rappelle, comme toute transaction (38.5).

Appeler depuis un pilote#

cur.execute("CALL passer_commande(%s, %s, %s)", (1, 1, 1))
print(cur.fetchone())            # (6,)  : la valeur du paramètre INOUT
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'),)

Une fonction s'appelle dans une expression, ou dans un FROM où une fonction scalaire rend une ligne d'une colonne (SELECT * FROM prix_ttc(10), la forme qu'envoie cursor.callproc de psycopg2 ; voir 36.3) et une fonction ensembliste ses lignes (40.1) :

cur.callproc("prix_ttc", (10,))
print(cur.fetchone())            # (Decimal('12.00'),)
cur.callproc("paliers", (7,))
print(cur.fetchall())            # [(20,), (40,), (60,)]

callproc d'une procédure rend 42883 (psycopg2 l'appelle par SELECT) : écrire CALL.

Les arguments peuvent être nommés (f(nom => valeur), ou l'ancienne écriture f(nom := valeur)), dans n'importe quel ordre, après d'éventuels arguments positionnels ; un paramètre omis prend sa valeur par défaut :

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

Un nom qui n'est pas celui d'un paramètre (no parameter is named "tx"), un paramètre donné deux fois ou un paramètre sans valeur par défaut omis rendent 42883 ; un argument positionnel écrit après un argument nommé, 42601 (positional argument cannot follow named argument). La notation nommée vaut pour les routines stockées, CALL procédure(nom => …) compris (mêmes règles, mêmes erreurs). Parmi les fonctions du serveur, seules celles dont PostgreSQL nomme les paramètres l'admettent : make_date(year, month, day), make_time(hour, min, sec), make_timestamp(year, month, mday, hour, min, sec) ; les autres n'ont pas de paramètre nommé (upper(s => 'a') : 42883, comme PostgreSQL).

Le nom de la fonction peut être écrit entre guillemets, avec ou sans schéma (SELECT "prix_ttc"(10), ventes."prix_ttc"(10)).

Bloc anonyme DO#

DO exécute aussitôt un bloc PL/pgSQL sans créer de routine : il se comporte comme le corps d'une procédure sans paramètre (mêmes instructions, mêmes refus), exécutée avec les droits de la session dans la base courante, puis oubliée. Le client reçoit l'étiquette DO, aucune ligne, et les avis de RAISE ; une erreur sort avec son SQLSTATE, et, hors bloc de transaction du client, le bloc entier est défait, comme un 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 s'écrit avant ou après le corps ; LANGUAGE sql est refusé (0A000, language "sql" does not support inline code execution, comme PostgreSQL), de même que COMMIT / ROLLBACK dans le bloc.


40.3 Le langage PL/pgSQL#

Structure d'un bloc#

[<<étiquette>>]
[DECLARE
   déclarations]
BEGIN
   instructions
[EXCEPTION
   WHEN condition THEN instructions …]
END [étiquette];

Les blocs s'imbriquent ; une variable d'un bloc intérieur masque celle de même nom du bloc qui l'entoure. Les commentaires s'écrivent -- … et /* … */.

Déclarations#

DECLARE
  v_total   numeric(12,2);                       -- NULL au départ
  v_compte  integer := 0;                        -- ou DEFAULT 0, ou = 0
  c_sep     CONSTANT text := ' : ';
  v_nn      integer NOT NULL := 0;
  v_prix    article.prix%TYPE;                   -- type d'une colonne
  v_copie   v_prix%TYPE;                         -- type d'une autre variable
  a         article%ROWTYPE;                     -- une ligne de la table : a.code, a.prix…
  r         RECORD;                              -- ligne d'une requête, dans une boucle FOR
  p         ALIAS FOR $1;                        -- nom donné à un paramètre anonyme
  c_art     CURSOR FOR SELECT code FROM article ORDER BY id;

CONSTANT et NOT NULL exigent une valeur initiale ; ils ne sont pas contrôlés ensuite.

Affectation et expressions#

v_total := v_total + 1;        -- ou v_total = v_total + 1
a.stock := 0;                  -- champ d'un enregistrement
NEW.modifie_le := now();       -- colonne de la ligne d'un déclencheur

Toute expression SQL est admise, sous-requêtes comprises : 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 choisit une branche selon une valeur, ou selon des 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;

Dans une fonction jour(n) qui rend r, SELECT jour(1), jour(3), jour(200) rend week-end | semaine | invalide!.

Un CASE dont aucune branche ne convient et qui n'a pas d'ELSE donne 20000 (Case not found for CASE statement). NULL; est l'instruction qui ne fait rien.

Boucles#

FormeRôle
LOOP … END LOOPboucle sans fin, quittée par EXIT ou RETURN
WHILE condition LOOP … END LOOPtant que la condition est vraie
FOR i IN [REVERSE] a .. b [BY pas] LOOP … END LOOPcompteur entier, propre à la boucle ; bornes évaluées une fois
FOR r IN requête LOOP … END LOOPune itération par ligne ; r est un RECORD ou une liste de variables
FOR r IN curseur LOOP … END LOOPidem, sur un curseur déclaré
EXIT [étiquette] [WHEN condition]quitte la boucle (ou la boucle étiquetée)
CONTINUE [étiquette] [WHEN condition]passe à l'itération suivante
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

40.4 Requêtes dans une 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;

La requête range sa première ligne dans les cibles ; sans ligne, les cibles valent NULL et FOUND est faux. Avec STRICT, la requête doit rendre exactement une ligne : P0002 (no_data_found, query returned no rows) sans ligne, P0003 (too_many_rows) s'il y en a plusieurs.

Dans un corps PL/pgSQL, un SELECT sans INTO est refusé à la création (42601, query has no destination for result data) : écrivez PERFORM pour ignorer son résultat.

Écritures, FOUND et ROW_COUNT#

INSERT, UPDATE, DELETE et TRUNCATE s'écrivent tels quels dans le corps. Après eux — et après SELECT … INTO, PERFORM, FETCH et une boucle FOR — la variable FOUND dit si une ligne au moins a été traitée, et GET DIAGNOSTICS en donne le nombre :

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 colonne INTO variable rend la valeur d'une colonne de la ligne insérée, en particulier l'identifiant qu'une colonne serial vient d'attribuer (voir passer_commande, 40.2).

Curseurs#

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

Un curseur se déclare sans argument, se lit en avant (FETCH [NEXT | FORWARD] [FROM] c INTO …) et se ferme par CLOSE. La boucle FOR r IN c1 LOOP l'ouvre, le parcourt et le ferme d'elle-même.

Types pris à la création#

Les types %TYPE et %ROWTYPE, et les champs d'un RECORD, sont résolus à la création de la routine : les tables citées doivent exister, et une table modifiée ensuite (colonne ajoutée, type changé) n'est pas relue — recréez la routine par CREATE OR REPLACE. Une table citée seulement dans une expression n'est cherchée qu'à l'exécution (42P01 si elle manque alors).


40.5 Messages et erreurs#

RAISE : avis#

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

Chaque % du format est remplacé par l'argument suivant ; %% écrit un % ; un argument NULL s'écrit <NULL>. L'avis arrive au client avec son niveau (NOTICE, WARNING, INFO), y compris depuis une fonction appelée dans une requête ou depuis un déclencheur ; psql l'affiche, psycopg2 le range dans connection.notices. Les avis émis avant une erreur sont eux aussi envoyés, avant l'erreur, comme dans PostgreSQL : DO $$ BEGIN RAISE NOTICE 'a'; RAISE EXCEPTION 'c'; END $$ affiche NOTICE: a puis l'erreur P0001. RAISE DEBUG et RAISE LOG n'envoient rien.

RAISE : erreurs#

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 »

Une erreur interrompt la routine et l'instruction qui l'a appelée. Son SQLSTATE est P0001 (raise_exception) par défaut, ou celui donné par ERRCODE : un code de cinq caractères, ou un nom de condition de PostgreSQL. Choisir ses propres codes dans la classe P0 (P0100…) permet à l'application de reconnaître ses erreurs métier.

Les options DETAIL, HINT, COLUMN, CONSTRAINT, TABLE… de USING sont acceptées sans effet : seuls le message et le SQLSTATE arrivent au client.

ASSERT condition [, message] lève P0004 quand la condition est fausse ou NULL, avec le message donné ou assertion failed.

Attraper une erreur : 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
ConditionAttrape
nom de conditionunique_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)…
nom de classeintegrity_constraint_violation (classe 23)…
SQLSTATE 'xxxxx'ce code précis, par exemple un code métier P0100
OTHERStoute erreur
cond1 OR cond2l'une ou l'autre

Dans le gestionnaire, SQLERRM est le message de l'erreur et SQLSTATE son code ; GET STACKED DIAGNOSTICS v = MESSAGE_TEXT | RETURNED_SQLSTATE les range dans des 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)
-------------

RAISE; seul, dans un gestionnaire, relance l'erreur attrapée avec son message et son SQLSTATE : avec EXCEPTION WHEN OTHERS THEN RAISE;, SELECT f(0) sort en ERROR: division by zero (22012).

Comme dans PostgreSQL, un bloc à clause EXCEPTION est une sous-transaction : un point de sauvegarde implicite est posé à l'entrée du bloc, et le gestionnaire y revient avant de s'exécuter. Ce que le bloc a écrit avant l'erreur est défait ; ce qu'ont écrit les instructions d'avant le bloc, et le gestionnaire lui-même, est gardé. Les variables gardent leurs valeurs.

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

Ce point de sauvegarde n'est posé que dans une transaction : celle d'un bloc BEGIN … COMMIT, ou la transaction implicite d'un appel fait hors d'un bloc par une session en langage PostgreSQL. Il ne l'est pas dans le corps d'un déclencheur, ni pour une routine appelée hors transaction par une session en langage MIRAJ : là, le gestionnaire ne défait que l'instruction en faute.

Autre différence avec PostgreSQL : le gestionnaire choisi est le plus précis qui couvre l'erreur (un code, puis une classe, puis OTHERS), et non le premier écrit.


40.6 Déclencheurs#

Un déclencheur exécute une fonction à chaque ligne insérée, modifiée ou supprimée d'une table. Il se crée en deux temps, comme dans PostgreSQL : une fonction de déclencheur, puis le déclencheur qui l'attache à une table.

CREATE [OR REPLACE] TRIGGER nom
  {BEFORE | AFTER} {INSERT | UPDATE [OF colonne [, …]] | DELETE} [OR …]
  ON table
  FOR EACH ROW
  [WHEN (condition)]
  EXECUTE {FUNCTION | PROCEDURE} fonction ( [arguments] );

DROP TRIGGER [IF EXISTS] nom ON table [CASCADE | RESTRICT];

La fonction de déclencheur#

C'est une fonction PL/pgSQL sans paramètre, déclarée RETURNS trigger. Elle dispose de :

VariableContenu
NEWla ligne nouvelle (INSERT, UPDATE) ; NULL pour un DELETE
OLDla ligne ancienne (UPDATE, DELETE) ; NULL pour un INSERT
TG_OP'INSERT', 'UPDATE' ou 'DELETE'
TG_NAME, TG_WHEN, TG_LEVELnom du déclencheur, 'BEFORE' ou 'AFTER', 'ROW'
TG_TABLE_NAME (ou TG_RELNAME), TG_TABLE_SCHEMAtable et schéma
TG_NARGS, TG_ARGV[i]arguments écrits dans CREATE TRIGGER

Sa valeur de retour décide du sort de la ligne :

RetourDans un déclencheur BEFOREDans un déclencheur AFTER
RETURN NEWla ligne s'écrit, avec les modifications faites à NEWignoré
RETURN OLDDELETE : la ligne est supprimée ; UPDATE : l'ancienne ligne est gardée telle quelleignoré
RETURN NULLla ligne est écartée : ni insérée, ni modifiée, ni supprimée, ni comptéeignoré

Atteindre la fin de la fonction sans RETURN donne 2F005. Appeler la fonction directement (SELECT f()) est refusé (0A000, trigger functions can only be called as triggers).

Compléter une ligne (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

Contrôler une écriture#

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)

L'erreur levée par le déclencheur fait échouer l'instruction qui l'a déclenché ; rien n'est écrit.

Tenir un 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é

Les écritures du déclencheur font partie de la transaction de l'instruction : un ROLLBACK les défait avec elle.

Écarter une ligne, tenir un total#

CREATE FUNCTION ignore_petites() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF NEW.quantite <= 1 THEN RETURN NULL; END IF;     -- ligne écartée, sans erreur
  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

Un déclencheur AFTER tient à jour une autre table :

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#

-- seulement quand le stock change et passe sous 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 lus par TG_ARGV[0], TG_ARGV[1] ; TG_NARGS vaut 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

Les arguments sont transmis en texte (SHOW TRIGGERS écrit avec_args('alerte', '7')).

UPDATE OF colonne déclenche quand la valeur d'une de ces colonnes change, et non quand la colonne est simplement citée dans le SET (PostgreSQL).

Règles#

  • Une même fonction sert à plusieurs déclencheurs, sur plusieurs tables ; deux tables peuvent avoir un déclencheur de même nom. Un nom déjà pris sur la table : 42710 (trigger "client_horodate" for relation "client" already exists).
  • Remplacer la fonction (CREATE OR REPLACE FUNCTION) met à jour tous les déclencheurs qui l'appellent.
  • Les déclencheurs d'une table, d'un moment et d'un événement s'exécutent dans l'ordre alphabétique de leurs noms, comme dans PostgreSQL.
  • DROP FUNCTION d'une fonction encore attachée est refusé (2BP01, cannot drop function horodate() because other objects depend on it: trigger client_horodate on table client (use DROP ... CASCADE)) ; DROP FUNCTION … CASCADE supprime la fonction et ses déclencheurs.
  • Un déclencheur ne peut pas écrire la table qui l'a déclenché (XX000, Can't update table 'commande' in stored function/trigger because it is already used by statement which invoked this stored function/trigger) ; pour changer la ligne en cours, modifiez NEW dans un déclencheur BEFORE.
  • EXECUTE FUNCTION exige une fonction RETURNS trigger (42P17, function prix_ttc() must return type trigger) qui existe (42883, function absente() does not exist).
  • TG_TABLE_NAME et les autres variables TG_ sont fixées à la création du déclencheur.

40.7 Consulter les routines et les déclencheurs#

Requête ou commandeRend
\df, \df+, \sf nom (psql)liste des routines, leur définition
pg_catalog.pg_procproname, prokind (f fonction, p procédure), prosrc (corps tel qu'écrit), provolatile, proisstrict, prosecdef, prolang
pg_get_functiondef(oid)l'instruction CREATE OR REPLACE FUNCTION … complète
pg_get_function_arguments(oid), pg_get_function_result(oid)paramètres et type du résultat
SHOW CREATE FUNCTION nom, SHOW CREATE PROCEDURE nomle même texte que pg_get_functiondef
information_schema.routinesroutine_name, routine_type
pg_catalog.pg_trigger, \d tableune ligne par déclencheur (tgname, tgrelid, tgfoid)
pg_get_triggerdef(oid), SHOW CREATE TRIGGER noml'instruction CREATE TRIGGER …
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 écrit de même CREATE OR REPLACE PROCEDURE … AS $procedure$…$procedure$, et SHOW CREATE TRIGGER l'instruction CREATE TRIGGER telle qu'elle a été écrite (table sans schéma), dans sa colonne SQL Original Statement.

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

Le corps est rendu au caractère près, tel qu'il a été écrit. L'en-tête garde la précision des paramètres (numeric(10,2), que PostgreSQL réduirait à numeric), parce que MIRAJ l'applique.

information_schema.triggers et SHOW TRIGGERS montrent le déclencheur tel qu'il a été écrit : son nom (article_prix), une ligne par événement comme dans PostgreSQL, et pour action EXECUTE FUNCTION controle_prix() (colonne action_statement ou Statement ; la condition WHEN est dans action_condition). Le corps que MIRAJ exécute réellement, et le nom qu'il donne en interne au déclencheur de chaque événement (article_prix$article$insert), n'y paraissent pas ; une instance rouverte en langage MIRAJ les montre.

L'outil miraj-dump exporte les routines et les déclencheurs en PL/pgSQL, dans un script que psql -f recharge (41. Catalogue et outils).


40.8 Droits#

ActionPrivilège, sur le schéma de la routine (ou sur la routine elle-même)
créer une fonction ou une procédureCREATE ROUTINE (schéma seulement)
la remplacer ou la supprimerALTER ROUTINE (la supprimer : sur la routine aussi)
l'appelerEXECUTE (schéma ou routine)
créer un déclencheurTRIGGER sur la table

Sans le privilège : 42501 (permission denied for schema ventes) à la création, 42501 (permission denied for function nb_clients) à l'appel.

  • Une routine s'exécute par défaut avec les droits de l'appelant (SECURITY INVOKER), comme dans PostgreSQL : il lui faut, en plus d'EXECUTE, les droits sur les tables que le corps lit ou écrit. Avec SECURITY DEFINER, le corps s'exécute avec les droits de son créateur : c'est le moyen de donner un accès contrôlé à une table sans accorder de droit sur elle.
  • Un déclencheur s'exécute toujours avec les droits de son créateur : un compte qui n'a que INSERT sur article alimente bien la table journal par le déclencheur article_journal, sans aucun droit sur journal.
-- en 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;

-- connecté en 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)

L'insertion a pourtant écrit sa ligne INSERT | stock 10 dans journal, par le déclencheur.

Les privilèges s'accordent par schéma (GRANT EXECUTE ON ventes.* TO app) ou routine par routine (GRANT EXECUTE ON FUNCTION ventes.nb_clients() TO app, GRANT ALTER ROUTINE ON PROCEDURE …) : voir 39.3.


40.9 Non pris en charge et écarts#

Refusé à la création (0A000)#

Le message nomme la construction, par exemple EXECUTE (dynamic SQL) is not supported in routines of the PostgreSQL language.

ConstructionRemarque
plusieurs paramètres OUT dans une fonction qui n'est pas SETOF record, RETURNS record, paramètre de type tableune procédure, elle, admet plusieurs paramètres OUT
EXECUTE 'texte' (SQL dynamique), FOR … IN EXECUTE
tableaux (integer[]), FOREACH
COMMIT, ROLLBACK dans une procédurela transaction appartient à l'appelant
instructions de définition (CREATE TABLE…) dans le corps
UPDATE … RETURNING, DELETE … RETURNING, INSERT … ON CONFLICT dans le corpsINSERT … VALUES … RETURNING … INTO est pris en charge
curseurs à arguments, OPEN … FOR, refcursor, FETCH autre que NEXT, MOVE
VARIADIC, types polymorphes (anyelement…)
LANGUAGE c, internal, plpython…
clause SET paramètre = … de CREATE FUNCTION, BEGIN ATOMIC, WINDOW, TRANSFORM
déclencheurs FOR EACH STATEMENT (ou sans FOR EACH ROW), INSTEAD OF, TRUNCATE, CREATE CONSTRAINT TRIGGER, REFERENCINGdéclencheurs par ligne, BEFORE ou AFTER, seulement
ALTER FUNCTION, ALTER PROCEDURE, ALTER TRIGGERCREATE OR REPLACE, ou supprimer et recréer
DROP FUNCTION a, bune routine par instruction
COMMENT ON FUNCTIONcommentaires de tables et de colonnes seulement
CREATE EVENT, ALTER EVENTpas d'événement planifié en langage PostgreSQL

Autres refus#

FormeRéponse
SELECT sans INTO dans un corps PL/pgSQL42601 à la création (query has no destination for result data)
CREATE FUNCTION sans LANGUAGE42P13 (no language specified)
CREATE FUNCTION IF NOT EXISTS42601 ; écrire CREATE OR REPLACE
SELECT * FROM f(…) AS t (a int, b text), … WITH ORDINALITY après une fonction stockée0A000
ext.x (variable qualifiée par l'étiquette de son bloc)42703 à l'exécution ; donner des noms distincts aux variables
ALTER TABLE t DISABLE TRIGGER nom0A000 ; supprimer et recréer le déclencheur

Écarts de comportement#

  • Sous-transaction d'un bloc EXCEPTION : non posée dans un déclencheur, ni hors transaction pour une session en langage MIRAJ ; le gestionnaire n'y défait que l'instruction en faute (40.5).
  • Pas de surcharge : une seule routine par nom et par schéma ; les types écrits dans DROP FUNCTION nom(types) sont ignorés.
  • Pas de récursion : une fonction qui s'appelle elle-même, directement ou non, échoue à l'exécution (XX000, Recursive stored functions and triggers are not allowed).
  • Table en cours d'emploi : une fonction ne peut pas écrire une table que lit l'instruction qui l'appelle (SELECT f() FROM client où f modifie client), ni un déclencheur sa propre table (XX000).
  • Nom d'une fonction du système : une fonction créée sous le nom d'une fonction intégrée (upper, now…) n'est jamais appelée sans son schéma : comme dans PostgreSQL, où pg_catalog passe avant le schéma courant, upper(x) appelle la fonction intégrée. Appelez la vôtre par son nom qualifié : public.upper(x) (public désigne le schéma courant) ou schéma.upper(x). public.f(…) appelle la fonction stockée f du schéma courant ; s'il n'y en a pas, la fonction intégrée de ce nom (public.length('abc')).
  • Fonction d'un autre schéma : elle se nomme schéma.f(…) ; search_path ne retient qu'un schéma (38.8).
  • Appels identiques : dans une même instruction, deux appels à arguments constants identiques ne sont exécutés qu'une fois.
  • Types : numeric sans précision vaut numeric(38,18) ; les types %TYPE / %ROWTYPE sont fixés à la création (40.4).
  • Variables et colonnes : une variable masque une colonne de même nom dans les requêtes de la routine (PostgreSQL signale l'ambiguïté) ; préfixez les paramètres (p_) et les variables (v_).
  • RAISE : DETAIL et HINT ne sont pas transmis au client.
  • UPDATE OF colonne se déclenche au changement de valeur (40.6).
  • Paramètres OUT d'une procédure : leur argument est exigé dans CALL, même s'il est ignoré (40.2).

La liste condensée est au chapitre 30.

Voir aussi#