Intercepter proprement les erreurs SQL en RPG ILE
ven 15 novembre 2019 0 Par IbmiisteSQL, c’est confortable… jusqu’au premier SQLCODE négatif en production.
Par défaut, sur IBM i, une erreur SQL dans un programme RPG ILE peut passer relativement inaperçue : le job continue, ou s’arrête sans contexte exploitable, et il faut aller fouiller dans les spools ou le joblog.
L’objectif de cet article est de montrer une façon simple et reproductible de centraliser la gestion des erreurs SQL : on intercepte les exceptions, on récupère les informations utiles (SQLCODE, SQLSTATE, message), puis on décide quoi faire (journaliser, remonter une erreur RPG, ignorer certains cas…).
1. Rappel : comment SQL signale une erreur en RPG ILE
Quand vous utilisez SQL embarqué dans un source SQLRPGLE, le précompilateur ajoute automatiquement une « zone de communication SQL » (SQLCA) ou, selon les options, des variables SQLCODE/SQLSTATE.
Quelques points à garder en tête :
- SQLCODE est un entier qui retourne le statut de la dernière instruction SQL.
- SQLSTATE est un code sur 5 caractères normalisé entre produits DB2, utile pour tester des classes d’erreurs.
- Par convention :
Sur IBM i, si vous n’utilisez pas SET OPTION SQLCA = *NO, le précompilateur ajoute une structure SQLCA complète.
Si vous utilisez SQLCA = *NO, il ajoute à la place des variables DCL‑S SQLCODE et DCL‑S SQLSTATE que vous pouvez tester directement dans le RPG.
2. Stratégies possibles de gestion des erreurs
En pratique, vous avez plusieurs options pour gérer vos erreurs SQL en RPG ILE :
- Tester SQLCODE / SQLSTATE après chaque instruction SQL.
- Utiliser des gestionnaires d’exception RPG (MONITOR, *PSSR, gestionnaires de procédure) combinés au test SQLCODE/SQLSTATE.
- Utiliser la clause WHENEVER du précompilateur SQL pour centraliser un comportement en cas d’erreur SQL.
L’article se concentre sur une approche pragmatique :
- Encapsuler les accès SQL dans des procédures ou des sous-routines.
- Tester systématiquement SQLCODE / SQLSTATE à la sortie.
- Déléguer la logique de traitement des erreurs à une procédure commune (log, message, éventuelle interruption du traitement).
3. Exemple simple de wrapper SQL en RPG ILE
Supposons que vous ayez un programme SQLRPGLE qui insère des lignes dans une table, et que vous souhaitiez :
- arrêter proprement en cas d’erreur non prévue ;
- ignorer certaines erreurs « attendues » (par exemple, une clé déjà existante ou un enregistrement déjà supprimé) ;
- écrire un message explicite dans le joblog.
Vous pouvez commencer par définir votre module en précisant que vous utilisez SQLCODE/SQLSTATE simples.
exec sql
set option commit = *none,
usropn = *yes,
sqlca = *no;Le précompilateur générera alors en RPG :
dcl-s SQLCODE int(10);
dcl-s SQLSTATE char(5);Ensuite, encapsulez votre opération SQL dans une procédure :
ctl-opt dftactgrp(*no) actgrp(*new);
/* Variables SQL générées par le précompilateur (sqlca = *no) */
dcl-s SQLCODE int(10);
dcl-s SQLSTATE char(5);
dcl-proc InsertClient;
dcl-pi *n;
pIdClient like(IDCLIENT);
pNomClient like(NOMCLIENT);
end-pi;
exec sql
insert into CLIENTS (IDCLIENT, NOMCLIENT)
values (:pIdClient, :pNomClient);
if SQLCODE < 0;
// Erreur SQL bloquante
callp LogSqlError('InsertClient');
// Au choix : lever un retour erreur, définir un indicateur, etc.
endif;
end-proc;Ici :
- La logique métier (InsertClient) reste lisible.
- La logique d’erreur est centralisée dans LogSqlError, que vous pouvez réutiliser partout.
4. Une procédure commune de logging SQL
La procédure LogSqlError peut récupérer SQLCODE, SQLSTATE et éventuellement le texte du message SQL pour tracer l’erreur dans le joblog ou dans une table de logs.
Exemple minimaliste :
dcl-proc LogSqlError;
dcl-pi *n;
pContext char(50);
end-pi;
dcl-s msg char(256);
// Exemple basique : concaténer les infos principales
msg = 'Erreur SQL dans ' + %trim(pContext) +
' SQLCODE=' + %char(SQLCODE) +
' SQLSTATE=' + SQLSTATE;
// Ici vous pouvez :
// - utiliser Qp0zLprintf
// - envoyer un message CPF9898
// - insérer dans une table LOG_SQL
// etc.
// Exemple avec LPRINTF (procédure SQL QSYS2) [web:6]
exec sql
call qsys2.LPRINTF(:msg);
end-proc;
IBM fournit un exemple similaire d’utilisation d’une procédure interne appelée par WHENEVER pour écrire dans le joblog via LPRINTF.
5. Utiliser WHENEVER pour factoriser le traitement des erreurs
À partir des versions récentes du précompilateur SQL ILE RPG, l’instruction WHENEVER est supportée et permet de définir un comportement global en cas d’erreur SQL.
Par exemple :
exec sql
whenever sqlerror call LogSqlErrorProc;- Cette directive indique au précompilateur que, si une instruction SQL renvoie une erreur (SQLCODE négatif), il doit transférer le contrôle vers LogSqlErrorProc.
- LogSqlErrorProc est alors une sous-routine ou une procédure RPG qui examine SQLCODE/SQLSTATE et prend les décisions (log, stop, ignore certaines erreurs).
IBM précise que WHENEVER doit être codé avant les instructions SQL qu’il doit couvrir.
C’est donc intéressant pour appliquer un « contrat » commun à tout un module : par exemple, « toute erreur SQL appelle LogSqlErrorProc ».
6. Tableau récapitulatif des options
7. Points d’attention et bonnes pratiques
Quelques recommandations glanées dans la doc IBM et la littérature technique :
- Ne mélangez pas plusieurs styles de gestion d’erreurs SQL dans un même module (par exemple, WHENEVER + tests manuels + *PSSR partout). Clarifiez une approche dominante.
- Si vous utilisez SQLCA = *NO, ne redéclarez pas vous-même SQLCODE ou SQLSTATE : laissez le précompilateur les définir une seule fois.
- Testez explicitement le cas SQLCODE = 100 pour les FETCH afin de ne pas le traiter comme une erreur.
- Identifiez les erreurs « attendues » (par exemple, clé déjà existante, ligne introuvable) et documentez la décision : ignorer, loguer en warning, ou remonter une erreur métier.
- Surveillez les évolutions du précompilateur SQL (nouvelles options, support WHENEVER, etc.) dans les notes IBM pour adapter vos modèles de code.
8. À adapter selon votre contexte
Les exemples ci‑dessus sont volontairement simples. Dans un vrai projet, vous voudrez probablement :
- factoriser le logging dans un service commun ;
- définir une convention de codes d’erreurs métier (au‑dessus du SQL) ;
- distinguer les comportements selon les modules (batch vs interactif, API vs job interne) ;
- adapter la solution à votre version d’IBM i et aux options de précompilation choisies.
9. Exemple complet : une procédure SQLErreur réutilisable
Pour illustrer concrètement la gestion centralisée des erreurs SQL, voici une procédure SQLErreur que vous pouvez réutiliser dans vos programmes RPG ILE.
Elle reçoit la SQLCA en paramètre, reconstruit les informations d’erreur, envoie un message dans le joblog, et peut, en option, provoquer une erreur *ESCAPE pour stopper le programme appelant.
// Job information
dcl-ds jobi0100 LikeDS(JOBI0100_t);
//----------------------------------------------------------------
// QMHSNDPM - send program messages
//----------------------------------------------------------------
dcl-pr QMHSNDPM extpgm('QMHSNDPM');
*n char(7) const; // MessageId
*n char(20) const; // MessageFile
*n char(512) const options(*varsize); // MessageData
*n bindec(9) const; // MessageDataL
*n char(10) const; // MessageType (*DIAG / *ESCAPE)
*n char(128) const options(*varsize); // CallStkEntry
*n bindec(9) const; // CallStkCount
*n char(4) const; // MessageKey
*n char(120) options(*varsize); // ErrApi
end-pr;
// Données pour l’envoi de message programme
dcl-ds sndpgmmsg;
msgid char(7) inz('CPF9898');
msgfile char(20) inz('QCPFMSG QSYS ');
msgdataL bindec(9) inz(512);
msgtype char(10) inz('*COMP ');
msgmsgq char(11) inz('* ');
msgstack bindec(9) inz(1);
msgkey char(4);
end-ds;
dcl-s msgdata char(512);
//----------------------------------------------------------------
// sql communication area
//----------------------------------------------------------------
dcl-ds SQLCA;
SQLCAID char(8);
SQLCABC bindec(9);
SQLCODE bindec(9);
SQLERRML bindec(4);
SQLERRMC char(70);
SQLERRP char(8);
SQLERRD char(24);
SQLER1 bindec(9) overlay(sqlerrd:1);
SQLER2 bindec(9) overlay(sqlerrd:5);
SQLER3 bindec(9) overlay(sqlerrd:9);
SQLER4 bindec(9) overlay(sqlerrd:13);
SQLER5 bindec(9) overlay(sqlerrd:17);
SQLER6 char(4) overlay(sqlerrd:21);
SQLWARN char(11);
SQLSTATE char(5);
P_SQLCA char(136) pos(1);
end-ds;
dcl-s rc int(10);
//----------------------------------------------------------------
dcl-proc SQLErreur export;
dcl-pi SQLErreur;
pSqlca char(136) const; // SQLCA passée par l’appelant
pStop ind options(*nopass); // *ON => envoi *ESCAPE
end-pi;
dcl-s wStop ind inz(*on);
Jobi0100.Job = '*';
Jobi0100.IntJobID = *Blanks;
Jobi0100.Status = 'JOBI0100';
if %Parms = 2;
wStop = pStop;
endif;
// Recopie la SQLCA passée en paramètre dans la structure locale
P_SQLCA = pSqlca;
select;
// Erreur détectée (SQLCODE < 0)
when SQLCODE < 0;
msgtype = '*DIAG';
// cpf message...
If SQLER1 > 0;
msgid = %editw(%dec(SQLER1:7:0):'0 ');
%subst(msgid:1:3) = 'CPF';
else;
// cpd message...
If SQLER2 > 0;
msgid = %editw(%dec(SQLER2:7:0):'0 ');
%subst(msgid:1:3) = 'CPD';
else;
// message SQLxxx (QSQLMSG)
msgid = %editw(%dec(SQLCODE * -1:7:0):'0 ');
%subst(msgid:1:3) = 'SQL';
%subst(msgfile:1:10) = 'QSQLMSG';
EndIf;
EndIf;
// Avertissement (SQLCODE > 0)
when SQLCODE > 0;
msgid = %editw(%dec(SQLCODE:7:0):'0 ');
%subst(msgid:1:3) = 'SQL';
%subst(msgfile:1:10) = 'QSQLMSG';
// Exécution SQL OK
other;
msgid = 'SQL' + SQLER6;
%subst(msgfile:1:10) = 'QSQLMSG';
endsl;
// Texte du message
If SQLERrml > 0;
msgdata = SQLERrmc;
msgdataL = SQLERrml;
EndIf;
// Retrouve les attributs du Job en cours
rtvJobInf (jobi0100:Jobi0100.BytesRet:Jobi0100.Status:
Jobi0100.Job:Jobi0100.IntJobID:Errapi);
// Message d'échappement si erreur SQL et demande d’arrêt
If SQLCODE < 0 and wStop;
QMHSNDPM (msgid: msgfile: msgdata: msgdataL:
'*ESCAPE': '*': 1: msgkey: ErrApi);
EndIf;
end-proc;Pour l’utiliser, il suffit de l’appeler juste après votre requête SQL.
// Exemple 1 : loguer l’erreur sans stopper le programme
exec sql
update CLIENTS
set NOMCLIENT = :Nom
where IDCLIENT = :Id;
callp SQLErreur(SQLCA); // pStop non passé => pas de *ESCAPE
// Exemple 2 : loguer et stopper en cas d’erreur SQL
exec sql
delete from CLIENTS
where IDCLIENT = :Id;
callp SQLErreur(SQLCA : *on); // pStop = *ON => *ESCAPE si SQLCODE < 0
