39. 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 39.9.
Les exemples portent sur le schéma ventes (tables client, article, commande, ligne_commande) du chapitre 31. Prise en main, avec ses données de départ (articles STY-01 à 1.20, CAH-96 à 2.50, CLA-A4 à 4.90).
39.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.88Une 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ément | Pris en charge |
|---|---|
CREATE OR REPLACE | remplace la fonction de même nom ; sans lui, une fonction existante donne 42723 |
schéma.nom | la fonction appartient à un schéma ; sans schéma, au schéma courant |
| paramètres | IN (défaut), OUT, INOUT ; sans nom, ils se lisent $1, $2… ; types de 32. Types de données et table.colonne%TYPE |
| valeurs par défaut | DEFAULT valeur ou = valeur ; les derniers arguments d'un appel peuvent alors être omis |
RETURNS type | un type de base, void, ou trigger (39.6) |
RETURNS SETOF …, RETURNS TABLE (…) | fonction qui rend des lignes, appelée dans un FROM (voir plus bas) |
un paramètre OUT ou INOUT | la fonction rend sa valeur finale (RETURNS peut être omis) |
LANGUAGE plpgsql | sql | exigé (42P13 sinon), avant ou après le corps |
| corps | AS $$…$$, AS $etiquette$…$etiquette$, AS '…' ; pour LANGUAGE sql, aussi la forme RETURN expression |
IMMUTABLE, STABLE, VOLATILE | gardé et affiché (pg_proc.provolatile) |
STRICT | si un argument est NULL, la fonction rend NULL sans exécuter son corps |
SECURITY INVOKER | DEFINER | droits de l'appelant (défaut) ou du créateur, voir 39.8 |
LEAKPROOF, PARALLEL …, COST n, ROWS n | accepté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 | 21Fonctions 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.00Fonction 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) : sinon42883. Elle n'est pas exécutée à ce moment. - Elle est exécutée pour chaque ligne écrite qui ne fournit pas la colonne :
INSERTd'une ou de plusieurs lignes, colonne omise ouDEFAULTécrit,INSERT … SELECT,COPY … FROM STDIN, etUPDATE … 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éclencheursBEFORE INSERT, qui la voient dansNEW, 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'unINSERTde plusieurs lignes). - La session qui écrit sans fournir la colonne doit avoir le droit
EXECUTEsur 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_defaultetSHOW CREATE TABLErendent 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.numericsans précision vautnumeric(38,18):SELECT num_libre(1.5)d'une fonctionRETURNS numericqui double son argument affiche3.000000000000000000. - Une fonction
RETURNS voidrend 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); -- 25Fonctions 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 :
| Forme | Lignes rendues |
|---|---|
LANGUAGE sql | les 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.50CREATE FUNCTION commandes_client(p_client integer)
RETURNS TABLE (commande integer, passee date, montant numeric(12,2))
LANGUAGE plpgsql STABLE AS $$
BEGIN
RETURN QUERY
SELECT o.id, o.passee_le, sum(l.quantite * l.prix_unit)
FROM commande o JOIN ligne_commande l ON l.commande_id = o.id
WHERE o.client_id = p_client
GROUP BY o.id, o.passee_le
ORDER BY o.id;
END $$;
SELECT * FROM commandes_client(1);
SELECT * FROM commandes_client(1) WHERE montant > 10; commande | passee | montant
----------+------------+---------
1 | 2026-10-01 | 22.00
2 | 2026-10-05 | 9.80
commande | passee | montant
----------+------------+---------
1 | 2026-10-01 | 22.00CREATE FUNCTION paliers(n integer) RETURNS SETOF integer LANGUAGE plpgsql AS $$
BEGIN
FOR i IN 1 .. n LOOP
IF i % 2 = 0 THEN
RETURN NEXT i * 10;
END IF;
END LOOP;
RETURN;
END $$;
CREATE FUNCTION carres(n integer, OUT k integer, OUT carre integer) RETURNS SETOF record
LANGUAGE plpgsql AS $$
BEGIN
FOR i IN 1 .. n LOOP
k := i;
carre := i * i;
RETURN NEXT;
END LOOP;
END $$;
SELECT * FROM paliers(7);
SELECT x.p * 2 FROM paliers(5) AS x(p);
SELECT * FROM carres(3); paliers
---------
20
40
60
x.p * 2
---------
40
80
k | carre
---+-------
1 | 1
2 | 4
3 | 9- Les colonnes du résultat portent le nom des colonnes de la table (
SETOF table), deTABLE (…)ou des paramètresOUT; pourSETOF 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 expressiondans une telle fonction est refusé à la création (42804,RETURN cannot have a parameter in function returning set).- Depuis psycopg2,
cursor.callproc("paliers", (7,))envoieSELECT * 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 (39.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.
39.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 sqln'exécute que des écritures. - Une procédure peut en appeler une autre (
CALLdans 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; -- 0Hors 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éfaitDans 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 (37.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 35.3) et une fonction ensembliste ses lignes (39.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.000000000000000000Un 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(*)
----------
3LANGUAGE 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.
39.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éclencheurToute 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 | grandCASE 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#
| Forme | Rôle |
|---|---|
LOOP … END LOOP | boucle sans fin, quittée par EXIT ou RETURN |
WHILE condition LOOP … END LOOP | tant que la condition est vraie |
FOR i IN [REVERSE] a .. b [BY pas] LOOP … END LOOP | compteur entier, propre à la boucle ; bornes évaluées une fois |
FOR r IN requête LOOP … END LOOP | une itération par ligne ; r est un RECORD ou une liste de variables |
FOR r IN curseur LOOP … END LOOP | idem, 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 / 6CREATE FUNCTION liste_codes(p_prix_min numeric) RETURNS text LANGUAGE plpgsql AS $$
DECLARE
r RECORD;
res text := '';
BEGIN
FOR r IN SELECT code, prix FROM article WHERE prix >= p_prix_min ORDER BY code LOOP
res := res || r.code || '=' || r.prix || ';';
END LOOP;
IF NOT FOUND THEN res := 'aucun'; END IF;
RETURN res;
END $$;
SELECT liste_codes(2), liste_codes(100); -- CAH-96=2.50;CLA-A4=4.90; | aucun39.4 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, 39.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-A4Un 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).
39.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
DOChaque % 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| Condition | Attrape |
|---|---|
| nom de condition | unique_violation (23505), foreign_key_violation (23503), not_null_violation (23502), check_violation (23514), division_by_zero (22012), no_data_found (P0002), too_many_rows (P0003), raise_exception (P0001)… |
| nom de classe | integrity_constraint_violation (classe 23)… |
SQLSTATE 'xxxxx' | ce code précis, par exemple un code métier P0100 |
OTHERS | toute erreur |
cond1 OR cond2 | l'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é aussiCe 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.
39.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 :
| Variable | Contenu |
|---|---|
NEW | la ligne nouvelle (INSERT, UPDATE) ; NULL pour un DELETE |
OLD | la ligne ancienne (UPDATE, DELETE) ; NULL pour un INSERT |
TG_OP | 'INSERT', 'UPDATE' ou 'DELETE' |
TG_NAME, TG_WHEN, TG_LEVEL | nom du déclencheur, 'BEFORE' ou 'AFTER', 'ROW' |
TG_TABLE_NAME (ou TG_RELNAME), TG_TABLE_SCHEMA | table et schéma |
TG_NARGS, TG_ARGV[i] | arguments écrits dans CREATE TRIGGER |
Sa valeur de retour décide du sort de la ligne :
| Retour | Dans un déclencheur BEFORE | Dans un déclencheur AFTER |
|---|---|---|
RETURN NEW | la ligne s'écrit, avec les modifications faites à NEW | ignoré |
RETURN OLD | DELETE : la ligne est supprimée ; UPDATE : l'ancienne ligne est gardée telle quelle | ignoré |
RETURN NULL | la ligne est écartée : ni insérée, ni modifiée, ni supprimée, ni comptée | ignoré |
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 | fContrô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 1Un 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; -- 185UPDATE 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 1Les 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 FUNCTIONd'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 … CASCADEsupprime 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, modifiezNEWdans un déclencheurBEFORE. EXECUTE FUNCTIONexige une fonctionRETURNS trigger(42P17,function prix_ttc() must return type trigger) qui existe (42883,function absente() does not exist).TG_TABLE_NAMEet les autres variablesTG_sont fixées à la création du déclencheur.
39.7 Consulter les routines et les déclencheurs#
| Requête ou commande | Rend |
|---|---|
\df, \df+, \sf nom (psql) | liste des routines, leur définition |
pg_catalog.pg_proc | proname, 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 nom | le même texte que pg_get_functiondef |
information_schema.routines | routine_name, routine_type |
pg_catalog.pg_trigger, \d table | une ligne par déclencheur (tgname, tgrelid, tgfoid) |
pg_get_triggerdef(oid), SHOW CREATE TRIGGER nom | l'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 < 5Le 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 (40. Catalogue et outils).
39.8 Droits#
| Action | Privilège, sur le schéma de la routine (ou sur la routine elle-même) |
|---|---|
| créer une fonction ou une procédure | CREATE ROUTINE (schéma seulement) |
| la remplacer ou la supprimer | ALTER ROUTINE (la supprimer : sur la routine aussi) |
| l'appeler | EXECUTE (schéma ou routine) |
| créer un déclencheur | TRIGGER 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. AvecSECURITY 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
INSERTsurarticlealimente bien la tablejournalpar le déclencheurarticle_journal, sans aucun droit surjournal.
-- 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 38.3.
39.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.
| Construction | Remarque |
|---|---|
plusieurs paramètres OUT dans une fonction qui n'est pas SETOF record, RETURNS record, paramètre de type table | une procédure, elle, admet plusieurs paramètres OUT |
EXECUTE 'texte' (SQL dynamique), FOR … IN EXECUTE | |
tableaux (integer[]), FOREACH | |
COMMIT, ROLLBACK dans une procédure | la transaction appartient à l'appelant |
instructions de définition (CREATE TABLE…) dans le corps | |
UPDATE … RETURNING, DELETE … RETURNING, INSERT … ON CONFLICT dans le corps | INSERT … 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, REFERENCING | déclencheurs par ligne, BEFORE ou AFTER, seulement |
ALTER FUNCTION, ALTER PROCEDURE, ALTER TRIGGER | CREATE OR REPLACE, ou supprimer et recréer |
DROP FUNCTION a, b | une routine par instruction |
COMMENT ON FUNCTION | commentaires de tables et de colonnes seulement |
CREATE EVENT, ALTER EVENT | pas d'événement planifié en langage PostgreSQL |
Autres refus#
| Forme | Réponse |
|---|---|
SELECT sans INTO dans un corps PL/pgSQL | 42601 à la création (query has no destination for result data) |
CREATE FUNCTION sans LANGUAGE | 42P13 (no language specified) |
CREATE FUNCTION IF NOT EXISTS | 42601 ; écrire CREATE OR REPLACE |
SELECT * FROM f(…) AS t (a int, b text), … WITH ORDINALITY après une fonction stockée | 0A000 |
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 nom | 0A000 ; 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 (39.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 clientoùfmodifieclient), 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_catalogpasse avant le schéma courant,upper(x)appelle la fonction intégrée. Appelez la vôtre par son nom qualifié :public.upper(x)(publicdésigne le schéma courant) ouschéma.upper(x).public.f(…)appelle la fonction stockéefdu 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_pathne retient qu'un schéma (37.8). - Appels identiques : dans une même instruction, deux appels à arguments constants identiques ne sont exécutés qu'une fois.
- Types :
numericsans précision vautnumeric(38,18); les types%TYPE/%ROWTYPEsont fixés à la création (39.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:DETAILetHINTne sont pas transmis au client.UPDATE OF colonnese déclenche au changement de valeur (39.6).- Paramètres
OUTd'une procédure : leur argument est exigé dansCALL, même s'il est ignoré (39.2).
La liste condensée est au chapitre 29.
Voir aussi#
- 34. Langage DML pour
RETURNINGetON CONFLICT. - 37. Transactions et session pour les transactions, les conflits et les réessais.
- 38. Comptes et privilèges pour
EXECUTE,CREATE ROUTINEetSECURITY DEFINER. - 40. Catalogue et outils pour
pg_proc,pg_triggeret l'export.