Inter­cep­ter pro­pre­ment les erreurs SQL en RPG ILE

Inter­cep­ter pro­pre­ment les erreurs SQL en RPG ILE

ven 15 novembre 2019 0 Par Ibmiiste

SQL, c’est confor­table… jusqu’au pre­mier SQLCODE néga­tif en production.
Par défaut, sur IBM i, une erreur SQL dans un pro­gramme RPG ILE peut pas­ser rela­ti­ve­ment inaper­çue : le job conti­nue, ou s’arrête sans contexte exploi­table, et il faut aller fouiller dans les spools ou le joblog.

L’objectif de cet article est de mon­trer une façon simple et repro­duc­tible de cen­tra­li­ser la ges­tion des erreurs SQL : on inter­cepte les excep­tions, on récu­père les infor­ma­tions utiles (SQLCODE, SQLSTATE, mes­sage), puis on décide quoi faire (jour­na­li­ser, remon­ter une erreur RPG, igno­rer cer­tains cas…).


1. Rap­pel : com­ment SQL signale une erreur en RPG ILE

Quand vous uti­li­sez SQL embar­qué dans un source SQLRPGLE, le pré­com­pi­la­teur ajoute auto­ma­ti­que­ment une « zone de com­mu­ni­ca­tion SQL » (SQLCA) ou, selon les options, des variables SQLCODE/SQLSTATE.

Quelques points à gar­der en tête :

  • SQLCODE est un entier qui retourne le sta­tut de la der­nière ins­truc­tion SQL.
  • SQLSTATE est un code sur 5 carac­tères nor­ma­li­sé entre pro­duits DB2, utile pour tes­ter des classes d’erreurs.
  • Par conven­tion :
    • 0 = succès.
    • 100 = fin de fichier sur un FETCH.
    • Néga­tif = erreur blo­quante (clé dupli­quée, vio­la­tion de contrainte, etc.).
    • Posi­tif ≠ 100 = aver­tis­se­ment (tron­ca­ture, etc.).

Sur IBM i, si vous n’utilisez pas SET OPTION SQLCA = *NO, le pré­com­pi­la­teur ajoute une struc­ture SQLCA complète.
Si vous uti­li­sez SQLCA = *NO, il ajoute à la place des variables DCL‑S SQLCODE et DCL‑S SQLSTATE que vous pou­vez tes­ter direc­te­ment dans le RPG.


2. Stra­té­gies pos­sibles de ges­tion des erreurs

En pra­tique, vous avez plu­sieurs options pour gérer vos erreurs SQL en RPG ILE :

  • Tes­ter SQLCODE / SQLSTATE après chaque ins­truc­tion SQL.
  • Uti­li­ser des ges­tion­naires d’exception RPG (MONITOR, *PSSR, ges­tion­naires de pro­cé­dure) com­bi­nés au test SQLCODE/SQLSTATE.
  • Uti­li­ser la clause WHENEVER du pré­com­pi­la­teur SQL pour cen­tra­li­ser un com­por­te­ment en cas d’erreur SQL.

L’article se concentre sur une approche pragmatique :

  1. Encap­su­ler les accès SQL dans des pro­cé­dures ou des sous-routines.
  2. Tes­ter sys­té­ma­ti­que­ment SQLCODE / SQLSTATE à la sortie.
  3. Délé­guer la logique de trai­te­ment des erreurs à une pro­cé­dure com­mune (log, mes­sage, éven­tuelle inter­rup­tion du traitement).

3. Exemple simple de wrap­per SQL en RPG ILE

Sup­po­sons que vous ayez un pro­gramme SQLRPGLE qui insère des lignes dans une table, et que vous souhaitiez :

  • arrê­ter pro­pre­ment en cas d’erreur non prévue ;
  • igno­rer cer­taines erreurs « atten­dues » (par exemple, une clé déjà exis­tante ou un enre­gis­tre­ment déjà supprimé) ;
  • écrire un mes­sage expli­cite dans le joblog.

Vous pou­vez com­men­cer par défi­nir votre module en pré­ci­sant que vous uti­li­sez SQLCODE/SQLSTATE simples.

exec sql
   set option commit = *none,
              usropn = *yes,
              sqlca  = *no;

Le pré­com­pi­la­teur géné­re­ra alors en RPG :

dcl-s SQLCODE  int(10);
dcl-s SQLSTATE char(5);

Ensuite, encap­su­lez votre opé­ra­tion 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 (Insert­Client) reste lisible.
  • La logique d’erreur est cen­tra­li­sée dans Log­Sq­lEr­ror, que vous pou­vez réuti­li­ser partout.

4. Une pro­cé­dure com­mune de log­ging SQL

La pro­cé­dure Log­Sq­lEr­ror peut récu­pé­rer SQLCODE, SQLSTATE et éven­tuel­le­ment le texte du mes­sage SQL pour tra­cer l’erreur dans le joblog ou dans une table de logs.

Exemple mini­ma­liste :

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 four­nit un exemple simi­laire d’utilisation d’une pro­cé­dure interne appe­lée par WHENEVER pour écrire dans le joblog via LPRINTF.


5. Uti­li­ser WHENEVER pour fac­to­ri­ser le trai­te­ment des erreurs

À par­tir des ver­sions récentes du pré­com­pi­la­teur SQL ILE RPG, l’instruction WHENEVER est sup­por­tée et per­met de défi­nir un com­por­te­ment glo­bal en cas d’erreur SQL.

Par exemple :

exec sql
   whenever sqlerror call LogSqlErrorProc;
  • Cette direc­tive indique au pré­com­pi­la­teur que, si une ins­truc­tion SQL ren­voie une erreur (SQLCODE néga­tif), il doit trans­fé­rer le contrôle vers LogSqlErrorProc.
  • Log­Sq­lEr­ror­Proc est alors une sous-rou­tine ou une pro­cé­dure RPG qui exa­mine SQLCODE/SQLSTATE et prend les déci­sions (log, stop, ignore cer­taines erreurs).

IBM pré­cise que WHENEVER doit être codé avant les ins­truc­tions SQL qu’il doit couvrir.
C’est donc inté­res­sant pour appli­quer un « contrat » com­mun à tout un module : par exemple, « toute erreur SQL appelle LogSqlErrorProc ».


6. Tableau réca­pi­tu­la­tif des options

ApprochePrin­cipeAvan­tagesLimites prin­ci­pales
Test manuel SQLCODE / SQLSTATEIF SQLCODE … après chaque SQLTrès fin, contrôle total. Beau­coup de code répétitif. 
Ges­tion­naires d’exception RPGMONITOR, *PSSR, etc. + SQLCODEIntègre bien la logique RPG. Plus com­plexe à structurer. 
WHENEVER SQLERROR / SQLWARNINGDirec­tive pré­com­pi­la­teur SQLCen­tra­li­sa­tion, code plus lisible. Peut mas­quer cer­taines erreurs si mal paramétré. 
Pro­cé­dure de log­ging communeLog­Sq­lEr­ror / LogSqlErrorProcRéuti­li­sable, homo­gène en production. Néces­site une dis­ci­pline d’équipe. 

7. Points d’attention et bonnes pratiques

Quelques recom­man­da­tions gla­nées dans la doc IBM et la lit­té­ra­ture technique :

  • Ne mélan­gez pas plu­sieurs styles de ges­tion d’erreurs SQL dans un même module (par exemple, WHENEVER + tests manuels + *PSSR par­tout). Cla­ri­fiez une approche dominante.
  • Si vous uti­li­sez SQLCA = *NO, ne redé­cla­rez pas vous-même SQLCODE ou SQLSTATE : lais­sez le pré­com­pi­la­teur les défi­nir une seule fois.
  • Tes­tez expli­ci­te­ment le cas SQLCODE = 100 pour les FETCH afin de ne pas le trai­ter comme une erreur.
  • Iden­ti­fiez les erreurs « atten­dues » (par exemple, clé déjà exis­tante, ligne introu­vable) et docu­men­tez la déci­sion : igno­rer, loguer en war­ning, ou remon­ter une erreur métier.
  • Sur­veillez les évo­lu­tions du pré­com­pi­la­teur SQL (nou­velles options, sup­port WHENEVER, etc.) dans les notes IBM pour adap­ter vos modèles de code.

8. À adap­ter selon votre contexte

Les exemples ci‑dessus sont volon­tai­re­ment simples. Dans un vrai pro­jet, vous vou­drez probablement :

  • fac­to­ri­ser le log­ging dans un ser­vice commun ;
  • défi­nir une conven­tion de codes d’erreurs métier (au‑dessus du SQL) ;
  • dis­tin­guer les com­por­te­ments selon les modules (batch vs inter­ac­tif, API vs job interne) ;
  • adap­ter la solu­tion à votre ver­sion d’IBM i et aux options de pré­com­pi­la­tion choisies.

9. Exemple com­plet : une pro­cé­dure SQLEr­reur réutilisable

Pour illus­trer concrè­te­ment la ges­tion cen­tra­li­sée des erreurs SQL, voi­ci une pro­cé­dure SQLEr­reur que vous pou­vez réuti­li­ser dans vos pro­grammes RPG ILE.
Elle reçoit la SQLCA en para­mètre, recons­truit les infor­ma­tions d’erreur, envoie un mes­sage dans le joblog, et peut, en option, pro­vo­quer une erreur *ESCAPE pour stop­per le pro­gramme 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’u­ti­li­ser, il suf­fit de l’ap­pe­ler 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