dimanche 14 décembre 2014

Session graphique inaccessible

Suite une réinstallation de Ubuntu (en vue de changer proprement de version), nous constatons que toute tentative de connexion via une session graphique est vouée à l'échec: après saisie du mot de passe, retour immédiat à l'écran d'accueil du display manager et ce sans aucun message d'erreur.
Dans notre cas, il s'agit de lightdm. Mais un autre display manager peut éventuellement afficher un message d'erreur.
Pour comprendre ce qu'il en est, nous basculons vers le terminal virtuel tty2: CTRL-ALT-F2.
Cette fois nous pouvons nous connecter.
Utilisons les commandes ls et id:

toto@aldebaran:~$ ls -ld ../toto
drwxr-xr-x 26 1001 1001 4096 déc  5 09:53 ../toto
toto@aldebaran:~$ id
uid=1000(toto) gid=1000(toto) groupes=1000(toto)
toto@aldebaran:~$ 

Dans l'ouput de la commande ls, le nom de l'utilisateur et de son groupe principal qui figurent habituellement en colonnes 3 et 4 sont remplacés par 1001. Donc il n'y a aucun utilisateur (et aucun groupe) dont l'id est 1001.
La commande id nous indique que l'utilisateur courant (toto) possède en fait l'uid et le gid 1000.
Explication: dans l'installation précédente toto n'était pas le premier utilisateur et il était effectivement caractérisé par un uid et un gid valant 1001. La réinstallation a été effectuée avec une partition /home séparée sans qu'elle soit formatée. Lors de la création de l'utilisateur toto, l'installateur a constaté que /home/toto existait déjà mais il n'a pas modifié les droits y afférents.
Solution du problème: réattribuer /home/toto à toto:

toto@aldebaran:~$ sudo -s
[sudo] password for toto: 
root@aldebaran:~# chown -R toto.toto ../toto
root@aldebaran:~# 

dimanche 31 août 2014

Pepper flash (suite)

Dans l'article précédent, dont celui-ci constitue la suite, après quelques généralités sur le plugin 'pepper flash' et son emploi avec chromium, nous avons traité uniquement du cas de ubuntu 14.04. Nous allons cette fois nous intéresser à d'autres distributions à commencer par Fedora (20).
Déjà l'installation de chromium n’est pas évidente car celui-ci ne fait pas partie de l'offre logicielle par défaut.
Pour en disposer, il faut avant de procéder à cette installation, configurer le dépôt churchyard-chromium-russianfedora:
 
[root@rigel ~]# cd /etc/yum.repos.d && wget -O churchyard-chromium-russianfedora.repo http://copr.fedoraproject.org/coprs/churchyard/chromium-russianfedora/repo/fedora-20-x86_64/
[root@rigel yum.repos.d]# yum install chromium

Cependant ce dépôt fournit très peu de paquets et aucun qui ait un lien avec le plugin pepper flash:


Pour pouvoir disposer du paquet qui installe le plugin, à savoir chromium-pepper-flash, il faut en plus configurer le dépôt russianfedora-nonfree. Cependant l’ajout de ce dépôt nécessite le dépôt russianfedora-free que nous pouvons cependant désactiver par la suite (par exemple avec la commande sed):

[root@rigel ~]# yum install http://mirror.yandex.ru/fedora/russianfedora/russianfedora/free/fedora/releases/20/Everything/x86_64/os/russianfedora-free-release-20-1.R.noarch.rpm
[root@rigel ~]# yum install http://mirror.yandex.ru/fedora/russianfedora/russianfedora/nonfree/fedora/releases/20/Everything/x86_64/os/russianfedora-nonfree-release-20-1.R.noarch.rpm
[root@rigel ~]# sed -i 's/enabled=1/enabled=0/' /etc/yum.repos.d/russianfedora-free.repo
[root@rigel ~]# sed -i 's/enabled=1/enabled=0/' /etc/yum.repos.d/russianfedora-free-updates.repo
[root@rigel ~]# yum install chromium-pepper-flash

Dans chromium, au niveau de l'URL chrome://plugins, en affichant + de détails, il est possible de désactiver l'ancien plugin, si celui-ci a été installé:

Dans le cas de la distribution Manjaro (ou Archlinux), pour disposer du plugin, il suffit d'installer le paquet chromium-pepper-flash à partir du dépôt AUR:

[toto@rigel ~]$ yaourt -S chromium-pepper-flash 

Le paquet est construit sur place après téléchargement du rpm google-chrome et extraction du plugin:



Dans d'autres distributions comme par exemple Chakra, le plugin est fourni avec le paquet qui contient le navigateur (chromium):


C'est la solution la plus simple mais qui ne convient pas à tout le monde.
Dans tous les cas évoqués ci-dessous, la mise à jour du plugin, une fois celui-ci installé, s'effectue automatiquement avec les autres mise à jour (contrairement à ce qui se passe pour Ubuntu 14.04)

dimanche 24 août 2014

Pepper flash

Le plugin flash livré par adobe pour certains navigateurs (dont firefox) est resté bloqué sur la version 11. Certes des mises à jour de sécurité sont encore fournies mais sans plus. De ce fait certaines animations ne fonctionnent plus correctement.
Cependant une version plus récente de ce plugin est intégrée dans google chrome. Cette version utilise l'API pepper (PPAPI) également disponible dans le navigateur chromium (mais pas dans firefox).
Il doit donc être possible d'utiliser cette version du plugin avec chromium.
Sous ubuntu 14.04 il suffit d'installer le paquet  pepperflashplugin-nonfree.
Mais attention: la version du plugin qui a été installée est figée: il n'y a pas de mise à jour, même si le système a lui été mis à jour et même si une nouvelle version du plugin existe.
Nous verrons en fin de billet comment procéder pour mettre à jour le plugin.
Essayons tout d'abord de comprendre pourquoi il faut procéder de la façon qui sera indiquée.
Téléchargeons le paquet afin de pouvoir l'analyser.

toto@rigel:~$ mkdir -p linux/pepper
toto@rigel:~$ cd linux/pepper
toto@:~/linux/pepper$ apt-get download pepperflashplugin-nonfree

Le fichier téléchargé est (dans notre cas) le paquet  pepperflashplugin-nonfree_1.3ubuntu1_amd64.deb
Il s'agit d'une archive 'ar' que nous allons décompresser:

toto@rigel:~/linux/pepper$ ar -xv pepperflashplugin-nonfree_1.3ubuntu1_amd64.deb 
x - debian-binary
x - control.tar.gz
x - data.tar.xz

Ce qui nous intéresse est le fichier control.tar.gz et plus précisément le script de post-installation (postinst) qu'il contient.
Afin de travailler proprement créons le dossier work/DEBIAN dans lequel nous allons décompresser control.tar.gz. On peut aussi décompresser data.tar.xz dans (dans work):

toto@rigel:~/linux/pepper$ mkdir -p work/DEBIAN
toto@rigel:~/linux/pepper$ tar -C work/DEBIAN -xzvf control.tar.gz 
./
./control
./prerm
./postrm
./postinst
./md5sums
toto@rigel:~/linux/pepper$ tar -C work -x --lzma -f data.tar.xz 

Il est possible d'arriver plus facilement au même résultat en utilisant l'outil dpkg-deb.
Une seule commande suffit après le téléchargement du fichier:

toto@rigel:~/linux/pepper$ dpkg-deb -R pepperflashplugin-nonfree_1.3ubuntu1_amd64.deb work

Le répertoire work est créé automatiquement.
Nous pouvons aussi, avec le même outil, extraire uniquement le contenu de control.tar.gz, mais alors nous devrons créer explicitement le fichier work (pour travailler dans la même arborescence):

toto@rigel:~/linux/pepper$ dpkg -e pepperflashplugin-nonfree_1.3ubuntu1_amd64.deb work/DEBIAN
dpkg-deb (sous-processus) : impossible de créer le répertoire: Aucun fichier ou dossier de ce type
dpkg-deb : erreur : le sous-processus tar a retourné une erreur de sortie d'état 2
toto@rigel:~/linux/pepper$ mkdir work
toto@rigel:~/linux/pepper$ dpkg -e pepperflashplugin-nonfree_1.3ubuntu1_amd64.deb work/DEBIAN

La commande suivante:

toto@rigel:~/linux/pepper$ head -7 work/DEBIAN/postinst 
#!/bin/sh

set -e

case "$1" in
    configure)
update-pepperflashplugin-nonfree --install --fast || true

montre que l'installation du plugin s'effectue au niveau du script de post-installation du paquet (qui s'exécute lors de l'installation du paquet).

L'installation du plugin consiste en fait à télécharger le paquet contenant google-chrome, à ne pas installer ce paquet mais à le décompresser et à mettre le plugin à disposition pour chromium.
Voici l'extrait du script qui correspond à ces deux dernières actions:

toto@rigel:~/linux/pepper$ awk '/dpkg-deb/,/mv/' work/usr/sbin/update-pepperflashplugin-nonfree 
dpkg-deb -x $debfile unpackchrome

sofile=unpackchrome/opt/google/chrome/PepperFlash/libpepflashplayer.so
[ -e $sofile ] || sofile=unpackchrome/opt/google/chrome-unstable/PepperFlash/libpepflashplayer.so
[ -e $sofile ] || sofile=unpackchrome/opt/google/chrome/PepperFlash/libpepflashplayer.so

mv -f $sofile /usr/lib/pepperflashplugin-nonfree

(Quelques explications sur le fonctionnement de awk se trouvent ici.)

L'utilisation du script update-pepperflashplugin-nonfree avec l'option status nous permet de vérifier si une nouvelle version du plugin est disponible, comme le montre cette copie d'écran effectuée juste après une mise à jour du système.


Pour installer la nouvelle version, il reste à exécuter la commande:

root@rigel:~# /update-pepperflashplugin-nonfree --install

En guise de vérification des assertions précédentes (la mise à jour s'effectue lors de l'exécution du script postinst), on peut aussi procéder comme suit:

root@rigel:~# dpkg-reconfigure pepperflashplugin-nonfree


mercredi 23 avril 2014

Fonction renvoyant des données de plusieurs tables

Les fonctions PL/pgSQL étudiées dans les articles précédents envoyaient en sortie des rangées contenant les données d'une seule table. Nous allons maintenant travailler sur un exemple où ce n'est plus le cas.
Soient un ensemble de tables liées par des relations d'intégrité référentielle telles que représentées sur ce schéma:


Les tables frcp, frdepartem et frregions sont présentées ici. Il est question de la table service et de la table employés dans l'article relatif à l' intégrité référentielle. Quant à la table gestion, elle a été introduite dans cet article.
Le challenge consiste à construire une fonction donnant une liste contenant le matricule, le nom, le nom du département de résidence des employés d'un service donné, le nom du service étant transmis en tant que paramètre de la fonction.
Tout d'abord nous plaçons dans une variable code le code du service (service_id) tiré à partir de son nom fourni en input ($1):
SELECT service_id
INTO code
FROM services
WHERE dénomination = $1
Une fois le code récupéré, nous accédons via la table gestion aux matricules et aux noms grâce à un join avec la table employés. Partant de la table employés nous pouvons atteindre le nom du département en effectuant des joins avec les tables frcp et frdepartem. 
Voici la requête qui sera utilisée:
SELECT a.matricule, a.nom, c.departem_nom
FROM employés a, frcp b, frdepartem c, gestion d
WHERE a.code_postal = b.code_postal
AND a.cp_seq = b.cp_seq
AND b.code_departem = c.departem_id
AND a.matricule = d.matricule
AND d.service = code
ORDER BY 3, 1 
Cela étant, différentes possibilités s'offre à nous en ce qui concerne la fonction proprement dite.
Nous pouvons par exemple utiliser un curseur:


CREATE FUNCTION stat1services(text)
RETURNS SETOF refcursor
AS $$
DECLARE
  code services.service_id%TYPE;
  service alias FOR $1;
  cursemp CURSOR (code text) IS
  SELECT a.matricule, a.nom, c.departem_nom
  FROM employés a, frcp b, frdepartem c, gestion d
  WHERE a.code_postal = b.code_postal
  AND a.cp_seq = b.cp_seq
  AND b.code_departem = c.departem_id
  AND a.matricule = d.matricule
  AND d.service = code
  ORDER BY 3, 1;
BEGIN
  SELECT service_id
  INTO code
  FROM services
  WHERE dénomination = service;
  FOR empr IN cursemp (code) LOOP
   RETURN NEXT empr;
  END LOOP;
  RETURN; 
END;
$$ LANGUAGE 'plpgsql';

La variable empr est  créée d'office et ne doit pas être déclarée. chaque rangée lue par le curseur lui est assignée. Le curseur est ouvert automatiquement avec l'instruction FOR.

Résultat:


EDIT: SETOF refcursor ne fonctionne plus. Il faut donc indiquer après SETOF un type correspondant aux rangées émises. Ce type doit être créé comme indiqué plus loin.
SELECT * FROM ... donnera alors un output plus agréable.

Cet output peut-être amélioré car la variable empr possède une sous-structure qui est définie au moment où une valeur lui est assignée:

CREATE FUNCTION stat11services(text)
RETURNS SETOF refcursor
AS $$
DECLARE
  code services.service_id%TYPE;
  service alias FOR $1;
  cursemp CURSOR (code text) IS
  SELECT a.matricule, a.nom, c.departem_nom AS département
  FROM employés a, frcp b, frdepartem c, gestion d
  WHERE a.code_postal = b.code_postal
  AND a.cp_seq = b.cp_seq
  AND b.code_departem = c.departem_id
  AND a.matricule = d.matricule
  AND d.service = code
  ORDER BY 3, 1;
BEGIN
  SELECT service_id
  INTO code
  FROM services
  WHERE dénomination = service;
  RETURN NEXT 'matr | nom                            | département';
  RETURN NEXT '-----+--------------------------------+------------------------------------';
  FOR empr IN cursemp (code) LOOP
   RETURN NEXT empr.matricule || ' | ' || rpad(empr.nom, 30) || ' | ' || empr.département;
  END LOOP;
  RETURN; 
END;
$$ LANGUAGE 'plpgsql';

Regardons ce que ça donne:


EDIT: SETOF refcursor ne fonctionne plus: il faut donc procéder comme ci-après

L'idéal serait de pouvoir indiquer dans la clause RETURNS un type correspondant aux rangées émises. C'est facile quand les rangées émises proviennent d'une seule table, mais ce n'est pas le cas.
Il faut donc créer le type dont on a besoin:

CREATE TYPE emp AS
(matricule integer,
nom char(50),
département char(30))

Nous pouvons maintenant dans la définition de la fonction stat1services remplacer SETOF refcursor par SETOF emp.
Appelons stat12services la nouvelle fonction ainsi obtenue. SELECT * FROM permet alors d'avoir facilement un format de sortie nettement plus convivial:



On peut même se passer de déclarer un curseur:


CREATE FUNCTION stat2services(text)
RETURNS SETOF emp
AS $$
DECLARE
  code services.service_id%TYPE;
  service alias FOR $1;
  empr RECORD;  
BEGIN
  SELECT service_id
  INTO code
  FROM services
  WHERE dénomination = service;
  FOR empr IN
    SELECT a.matricule, a.nom, c.departem_nom
    FROM employés a, frcp b, frdepartem c, gestion d
    WHERE a.code_postal = b.code_postal
    AND a.cp_seq = b.cp_seq
    AND b.code_departem = c.departem_id
    AND a.matricule = d.matricule
    AND d.service = code
    ORDER BY 3, 1
  LOOP
  RETURN NEXT empr;
  END LOOP;
  RETURN; 
END;
$$ LANGUAGE 'plpgsql';

Si nous ne souhaitons pas créer un nouveau type, il est possible d'utiliser la clause RETURNS TABLE associée à RETURN QUERY:

CREATE FUNCTION stat3services(text)
RETURNS TABLE (matricule integer, nom character(50), département character(30))
AS $$
DECLARE
  code services.service_id%TYPE;
  service alias FOR $1;
BEGIN
  SELECT service_id
  INTO code
  FROM services
  WHERE dénomination = service;
  RETURN QUERY SELECT a.matricule, a.nom, c.departem_nom
  FROM employés a, frcp b, frdepartem c, gestion d
  WHERE a.code_postal = b.code_postal
  AND a.cp_seq = b.cp_seq
  AND b.code_departem = c.departem_id
  AND a.matricule = d.matricule
  AND d.service = code
  ORDER BY 3, 1;
END;
$$ LANGUAGE 'plpgsql';


jeudi 17 avril 2014

Fonction PL/pgSQL retournant une table

Nous allons montrer sur un exemple comment utiliser la clause
RETURNS TABLE
associée à
RETURN QUERY
permet d'améliorer l'output d'une fonction PL/pgSQL renvoyant un ensemble de rangées.

Pour ce faire, créons la fonction fdépenses:


CREATE FUNCTION fdépenses(text)
RETURNS TABLE (ref char(9), date_paie date, montants numeric(13,12), détail character varying(50)) AS $$
DECLARE
  code dépenses.code_u%TYPE;
  usage alias for $1;
BEGIN
  SELECT code_u
  INTO code
  FROM utilisations
  WHERE signification = usage;
  RETURN QUERY SELECT référence, date_exec, montant, détails
  FROM dépenses WHERE code_u =  code  ORDER BY référence;        
END;
$$ LANGUAGE 'plpgsql';

Attention les noms des colonnes figurant dans la clause RETURNS TABLE doivent être différents des noms qui apparaissent dans le query. Si par exemple en ligne 2 on écrit 'montant' (sans s), aucun problème n'apparaît à la création de la fonction. Par contre à l'utilisation:


Supposons que tout est en ordre, donc que la fonction a été créée avec 'montants' écrit en ligne 2.
La fonction est maintenant utilisable:


Caramba! Ça ne fonctionne pas: malgré le RETURNS TABLE l'output est toujours aussi calamiteux.

En fait il faut encore faire  SELECT * FROM:



Et voilà!

Pour avoir la liberté de donner aux colonnes de la table affichée en output les mêmes noms que ceux des colonnes de la table d'origine (dépenses), il suffit de qualifier le nom des colonnes dans le query, comme ceci:

RETURN QUERY SELECT a.référence, a.date_exec, a.montant, a.détails
FROM dépenses a WHERE code_u =  code  ORDER BY référence;    

ou alors nous pouvons utiliser:

RUN QUERY EXECUTE chaîne de commande

Essayons avec:

RETURN QUERY EXECUTE 'SELECT référence, date_exec, montant, détails
FROM dépenses WHERE code_u = code ORDER BY référence';



Lorsqu'une commande est exécutée à l'intérieur d'une fonction PL/pgSQL, chaque variable apparaissant dans la commande  est traitée comme un paramètre auquel est assigné la valeur de la variable. C'était le cas auparavant pour la variable code. Ici ce n'est plus le cas: la commande est exécutée à l'extérieur de la fonction et code est considéré comme le nom d'une colonne.

Ceci:

RETURN QUERY EXECUTE 'SELECT référence, date_exec, montant, détails
FROM dépenses WHERE code_u = ' || code || ' ORDER BY référence'; 

conduit à une autre erreur:



On peut contourner le problème comme ceci:

RETURN QUERY EXECUTE 'SELECT référence, date_exec, montant, détails 
FROM dépenses WHERE code_u::numeric = ' || code || ' ORDER BY référence';  

ou le résoudre comme cela:

RETURN QUERY EXECUTE 'SELECT référence, date_exec, montant, détails 
FROM dépenses WHERE code_u = ' || quote_literal(code) || ' ORDER BY référence';  

mais le plus simple est quand même d'éviter les concaténations comme ci-après:


CREATE FUNCTION f2dépenses(text)
RETURNS TABLE (ref char(9), date_exec date, montant numeric(13,12), détails character varying(50))
AS $$
DECLARE
  code dépenses.code_u%TYPE;
  usage alias for $1;
BEGIN
  SELECT code_u
  INTO code
  FROM utilisations
  WHERE signification = usage;
  RETURN QUERY EXECUTE 'SELECT référence, date_exec, montant, détails
  FROM dépenses WHERE code_u = $1 ORDER BY référence' USING code;
END;
$$ LANGUAGE 'plpgsql';


vendredi 11 avril 2014

Formater des rangées dans une fonction PL/pgSQL

Dans le billet précédent nous nous sommes plaints de l'output calamiteux offert par les fonctions PL/pgSQL lorsqu'elles retournaient un ensemble de rangées:


EDIT: pour une fonction telle que celle-ci, SELECT * FROM fedépenses('Santé') donnera un bel output à condition d'avoir dans la clause RETURNS le type 'dépenses' et non pas 'refcursor' qui ne fonctionne plus. C'est possible car le retour de la fonction est constitué de rangées complètes (avec toutes les colonnes) de la seule table dépenses.

Les fonctions de formatage utilisées par un client psql (où un autre client)  ne sont pas accessibles depuis l'intérieur d'une procédure stockée sur le serveur (c'est le cas de la procédure PL/pgSQL utilisée ici). Donc pour améliorer l'output nous devons recourir aux fonctions PostgreSQL de formatage des chaînes.
Pour ce faire, remplaçons la fonction fedépenses telle que définie auparavant par celle-ci:

CREATE FUNCTION fe4dépenses (text)
RETURNS SETOF refcursor AS $$
DECLARE
 code dépenses.code_u%TYPE;
 usage alias for $1;
 l integer;
 fedépenses refcursor;
 d RECORD;
BEGIN
 SELECT code_u
 INTO code
 FROM utilisations
 WHERE signification = usage;
 SELECT max(length(détails))
 INTO l
 FROM dépenses
 WHERE code_u = code;
 IF l < 7 OR l is NULL THEN 
    l :=7;
 END IF;
 OPEN fedépenses FOR SELECT * FROM dépenses WHERE code_u = code ORDER BY référence;
 LOOP 
   FETCH NEXT FROM fedépenses INTO d;
   EXIT WHEN NOT FOUND;
   IF d.détails IS NULL THEN
      d.détails := '';
   END IF;
   RETURN NEXT d.référence||' | '||d.date_exec||' | '||to_char(d.montant, '9G999D99')||' | '||rpad(d. détails, l)||' | '||d.code_u;
 END LOOP;
 CLOSE fedépenses;
 RETURN;
END;
$$ LANGUAGE 'plpgsql';

L'utilisation de la clause
RETURNS SETOF dépenses
conduit à l'erreur
ERROR: RETURN NEXT must specify a record or row variable in function returning row:



D'où l'utilisation de
RETURNS SETOF refcursor

La variable d de type RECORD dans la quelle on met toute la rangée possède une sous-structure qui est déterminée au moment du fetch (lorsqu'une valeur lui est assignée).
Nous aurions pu aussi déclarer cette variable comme ceci:
d dépenses%ROWTYPE
Dans ce cas la sous-structure est définie au niveau du DECLARE
Mais dans les deux cas, on peut retourner les différents champs individuels et transformer ceux-ci à l'aide des fonctions de formatage PostgreSQL.
Ainsi 'montant' est transformé en une chaîne de caractères de longueur définie. Le motif utilisé dépend évidemment des montants concernés.
De même 'détails' est porté à une longueur fixe déterminée par
SELECT max(length(détails))
INTO l
FROM dépenses
WHERE code_u = code;
IF l < 7 OR l is NULL THEN 
  l :=7;
END IF;
Les différents champs individuels sont ensuite concaténés entre eux(opérateur ||) et aussi avec le séparateur  '  |  ' de manière à ce qu'une chaîne unique soit renvoyée.
Attention: si un seul des champs individuels présent dans la chaîne renvoyée est NULL, alors toute la chaîne est renvoyée avec la valeur NULL.
D'où la présence du test:
IF d.détails IS NULL THEN
   d.détails := '';
END IF;
(à supposer que détails soit le seul champ susceptible d'être NULL).
Voici le résultat:



On peut pousser l'amélioration encore plus loin en introduisant avant LOOP le code:

RETURN NEXT 'référence |    date    |   montant |'||rpad(' détails', l+1)|| ' | '||'code';
RETURN NEXT '----------+------------+-----------+'||lpad('-', l+2, '-') || '+'||'------';

Ce qui donne:





Pas mal, non?

Remarquons que nous n'étions pas obligé de reprendre toutes les colonnes dans l'output. Nous aurions pu par exemple nous passer de la colonne code.


Il est possible de modifier de la même façon la fonction fe2dépenses et fe3dépenses du billet précédent.

mercredi 9 avril 2014

Curseurs et pgsql

Dans le billet précédent, nous avons parlé de l'utilisation des variables dans des instructions SQL.
Nous avons regretté le fait que
SELECT ...
INTO ...
sert à créer une nouvelle table et non pas à donner une valeur à une variable.
En effet nous aurions aimé pouvoir exécuter
SELECT code_u
INTO :code
FROM utilisations
WHERE signification = 'Santé';
et ensuite
SELECT *
FROM dépenses
WHERE code_u = :'code';
Mais cela ne fonctionne pas.

D'où l'idée de créer avec le langage PL/pgSQL une procédure (appelée par une fonction), procédure dans laquelle SELECT... INTO... fonctionne comme nous le voulons.
Voici les instructions qui nous ont servi à créer cette fonction:


CREATE FUNCTION fedépenses (text)
RETURNS SETOF dépenses AS $$
DECLARE
code dépenses.code_u%TYPE;
usage alias for $1;
fedépenses refcursor;
dépenses RECORD;
BEGIN
SELECT code_u
INTO code
FROM utilisations
WHERE signification = usage;
OPEN fedépenses FOR SELECT * FROM dépenses WHERE code_u = code ORDER BY référence;
 LOOP 
 FETCH NEXT FROM fedépenses INTO dépenses;
   EXIT WHEN NOT FOUND;
 RETURN NEXT dépenses;
END LOOP;
CLOSE fedépenses;
RETURN;
END;
$$ LANGUAGE 'plpgsql';

Le 'RETURNS SETOF' est nécessaire si on veut que la fonction puisse retourner un ensemble de rangées.
La variable code est définie avec le même type que le champ dépenses.code_u.
fedépenses est déclaré en tant que curseur non lié. Il est lié à un query seulement lors de son ouverture.
Pour le reste, assez classiquement nous passons en revue avec FETCH les différentes rangées lisibles par le curseur et ce jusqu'à la dernière.
Après avoir créé la fonction, il reste à la tester:


L'output est calamiteux (EDIT: voir plus loin), mais nous avons ce que nous voulons.
Nous avons également testé une variante de la fonction précédente:

CREATE FUNCTION fe2dépenses (text)
RETURNS SETOF dépenses AS $$
DECLARE
code dépenses.code_u%TYPE;
usage alias for $1;
fedépenses CURSOR (code text) IS 
 SELECT * FROM dépenses WHERE code_u = code ORDER BY référence;
BEGIN
SELECT code_u
INTO code
FROM utilisations
WHERE signification = usage;
FOR dépenses IN fedépenses (code) LOOP
RETURN NEXT dépenses;
 END LOOP;
RETURN;
END;
$$ LANGUAGE 'plpgsql';

Le curseur est maintenant déclaré lié à un query paramétré. Le curseur est ouvert automatiquement avec l'instruction FOR (il ne doit pas être ouvert avant) et il est fermé automatiquement lorsque la boucle se termine. Chaque rangée lue par le curseur est successivement assignée au record dépenses qui est créé automatiquement et qui n'existe que pendant la durée de la boucle.
Et voici le test:


Il n'est d'ailleurs pas nécessaire de définir un curseur. Ceci fonctionne également:

CREATE FUNCTION fe3dépenses (text)
RETURNS SETOF dépenses AS $$
DECLARE
code dépenses.code_u%TYPE;
usage alias for $1;
d dépenses%ROWTYPE;
BEGIN
SELECT code_u
INTO code
FROM utilisations
WHERE signification = usage;
 FOR d IN SELECT * FROM dépenses WHERE code_u = code ORDER BY référence
 LOOP 
 RETURN NEXT d;  
END LOOP;
RETURN;
END;
$$ LANGUAGE 'plpgsql';

Mais dans tous les cas, l'output est toujours aussi calamiteux.

EDIT: pour cette fonction (fe3dépenses), SELECT * FROM permet d'éviter l'output calamiteux. Il en est de même pour les deux fonctions précédentes à condition que le type indiqué dans la clause RETURNS soit 'dépenses' et non 'refcursor' qui ne fonctionne plus. Ceci est possible car la sortie de ces fonctions est constituée de rangées complètes (avec toutes les colonnes) d'une seule table.

Alors pourquoi ne pas se servir de PL/pgSQL uniquement pour créer un curseur et utiliser celui dans le terminal psql?
Pour cela, créons la fonction ocdépenses:

CREATE FUNCTION ocdépenses (refcursor, text)
RETURNS refcursor AS $$
DECLARE
usage alias for $2;
code dépenses.code_u%TYPE;
BEGIN
SELECT code_u
INTO code
FROM utilisations
WHERE signification = usage;
OPEN $1 for SELECT * FROM dépenses WHERE code_u = code ORDER BY référence;
RETURN $1;
END;
$$ LANGUAGE 'plpgsql';

Testons:


Ah oui: le curseur n'est vivant que pendant la durée d'une transaction. Or la transaction qui a été implicitement ouverte au lancement du query
select ocdépenses('curs1', 'Carburant')
est fermée automatiquement lorsque celui-ci s'est exécuté.
Corrigeons le tir en ouvrant explicitement la transaction avec BEGIN:


COMMIT termine la transaction et ferme donc le curseur.

Certains diront que l'on pouvait arriver à ce résultat avec:

select *
from dépenses
where code_u =
(select code_u
from utilisations
where signification = 'Carburant')
;
et même avec
select A.*
from dépenses A natural join utilisations B
where signification = 'Carburant'
;

A cela nous répondons:

  1. c'est beaucoup moins amusant
  2. le SQL se trouve alors au niveau du client ce qui dans le cas de traitement lourd peut présenter certains désavantages (ce n'est pas le cas ici)
  3. le but était de parler des variables
  4. comment alors présenter nos amis les curseurs?

Et les curseurs offrent de nombreuses possibilités. On n'est pas obligé de se limiter à 'fetch all'. Par exemple, si curs1 a été défini pour 'Grande surface':


etc...

Suite au 'fetch all', le curseur est positionné après la dernière rangée. C'est pourquoi l'instruction suivante nous amène à l'avant-dernière rangée. Le deuxième 'fetch relative -2' montre que l'on remonte bien de 2 positions.

dimanche 6 avril 2014

Variables dans psql

Il nous est loisible de définir et d'utiliser dans un terminal psql des variables sur le modèle des host variables qui existent pour du SQL embarqué dans un programme C (ou cobol).
Illustrons ce fait à l'aide d'instruction portant sur la table opérations définie ici:


(Remarquons que la méta-commande \pset numericlocale on concerne uniquement le format de sortie des nombres)
Autre exemple:


Malheureusement l'instruction
SELECT ...
INTO ...
sert au niveau de psql à créer une nouvelle table et non pas à stocker le résultat d'une requête dans une variable (contrairement à ce qui existe pour le SQL embarqué).
Il est cependant possible de contourner le problème comme dans cet exemple:


Les tables dépenses et utilisations ont été définies pour l'article Le piège du null. Depuis le libellé 'Médecin' a été remplacé par 'Santé' et un code a été attribué à toutes les dépenses.

Terminons par un exemple montrant les avantages de l'utilisation de telles variables.
Modifions comme ceci le fichier bilan.sql figurant dans ce billet:

\set QUIET
\set an 2013
\set titre 'Bilan ':an
\pset numericlocale on
\pset footer off
\pset linestyle u
\pset title :titre
\pset border 2
\o | awk 'NR==4 {y=$0};/SOUS-TOTAL/ {print y;print;print y};!/SOUS-TOTAL/'
SELECT mois_n ||'-' || mois AS "Mois",  '' AS " ", reference,
date_exec::text, crédit, débit,
  (SELECT SUM(solde)
  FROM opérationsv B
  WHERE B.reference <= A.reference
  AND B.an = A.an
  AND B.mois_n = A.mois_n) AS solde,
  contrepartie AS "usage"
FROM opérationsv A
WHERE an = :'an'
UNION
SELECT mois_n || '-' || mois, 'SOUS-TOTAL', '', '',
SUM(crédit) AS crédit, SUM(débit) AS débit, SUM(solde) AS solde,
'' AS " "
FROM opérationsv
WHERE an = :'an'
GROUP BY mois_n, mois
UNION
SELECT 'Ensemble', 'TOTAL', '', '',
SUM(crédit) AS crédit, SUM(débit) AS débit, SUM(solde) AS solde,
'' AS " "
FROM opérationsv
WHERE an = :'an'
ORDER by 1, 2 , 3;
\pset footer
\pset title
\pset border 1
\unset QUIET
\o

Comparant avec le fichier original, nous constatons que pour passer du bilan 2013 au bilan 2014, il nous suffit maintenant de remplacer 2013 par 2014 en un seul endroit (au lieu de 4).

Une autre possibilité est de supprimer \set an 2013 du fichier bilan.sql: le même fichier d'instructions pourra alors servir pour n'importe quelle année (il faut bien sûr initialiser la variable an):