SQLRPGLE: como combinar la potencia de SQL con la logica de RPG. Host variables, cursores, SQL dinamico, manejo de errores con SQLCODE/SQLSTATE, commitment control y patrones de alto rendimiento.
SQLRPGLE es RPG con sentencias SQL embebidas directamente en el codigo fuente. El precompilador SQL convierte las sentencias SQL en llamadas a las APIs de Db2 antes de compilar el RPG. El resultado es un programa que combina la logica procedural de RPG con la potencia de consultas SQL, similar a como JDBC o ODBC integran SQL en Java o C.
RPG nativo (sin SQL)
SQLRPGLE (SQL embebido)
**free ctl-opt dftactgrp(*no) actgrp(*caller); ctl-opt option(*srcstmt : *nodebugio); // No necesitas DCL-F para tablas accedidas via SQL dcl-s nombre varchar(100); dcl-s saldo packed(11:2); dcl-s clienteId packed(7:0); // Sentencia SQL embebida: prefijo EXEC SQL exec sql SELECT CLINOM, CLISALDO INTO :nombre, :saldo FROM MILIB.CLIENTES WHERE CLIID = :clienteId; if sqlcode = 0; // Registro encontrado, procesar endif; *inlr = *on; return;
/* Compilar SQLRPGLE como programa *PGM */
CRTSQLRPGI OBJ(MILIB/MIPGM)
SRCFILE(MILIB/QRPGLESRC)
SRCMBR(MIPGM)
COMMIT(*NONE)
DBGVIEW(*SOURCE)
OPTION(*SRCSTMT *NODEBUGIO)
CLOSQLCSR(*ENDMOD)
DATFMT(*ISO)
DFTACTGRP(*NO)
ACTGRP(*CALLER)
OBJTYPE(*PGM)
/* Compilar como modulo *MODULE para ILE */
CRTSQLRPGI OBJ(MILIB/MIPGM)
SRCFILE(MILIB/QRPGLESRC)
OBJTYPE(*MODULE)
DBGVIEW(*SOURCE)
COMMIT(*NONE)
/* Compilar desde IFS (stream file) */
CRTSQLRPGI OBJ(MILIB/MIPGM)
SRCSTMF('/home/dev/src/mipgm.sqlrpgle')
OBJTYPE(*PGM)
DBGVIEW(*SOURCE)
COMMIT(*NONE)Las host variables son variables RPG usadas dentro de sentencias SQL. Se identifican con el prefijo : (dos puntos) que indica al precompilador que es una variable del programa host (RPG), no una columna SQL. Es el mismo concepto que los parametros ? en prepared statements de JDBC.
**free
// Variables RPG que se usan como host variables en SQL
dcl-s wkClienteId packed(7:0);
dcl-s wkNombre varchar(100);
dcl-s wkEmail varchar(200);
dcl-s wkSaldo packed(11:2);
dcl-s wkFecha date(*iso);
// Data structures como host variables
dcl-ds clienteDS qualified;
id packed(7:0);
nombre varchar(100);
email varchar(200);
saldo packed(11:2);
fechaAlta date(*iso);
end-ds;
// Uso en SELECT INTO (una fila)
wkClienteId = 1001;
exec sql
SELECT CLINOM, CLIEMAIL, CLISALDO
INTO :wkNombre, :wkEmail, :wkSaldo
FROM MILIB.CLIENTES
WHERE CLIID = :wkClienteId;
// Uso con data structure (una fila completa)
exec sql
SELECT CLIID, CLINOM, CLIEMAIL, CLISALDO, CLIFECALTA
INTO :clienteDS
FROM MILIB.CLIENTES
WHERE CLIID = :wkClienteId;
// Ahora: clienteDS.nombre tiene el valor
dsply clienteDS.nombre;
// Host variables en INSERT
exec sql
INSERT INTO MILIB.CLIENTES
(CLIID, CLINOM, CLIEMAIL, CLISALDO)
VALUES
(:wkClienteId, :wkNombre, :wkEmail, :wkSaldo);
// Host variables en UPDATE
exec sql
UPDATE MILIB.CLIENTES
SET CLISALDO = :wkSaldo,
CLIEMAIL = :wkEmail
WHERE CLIID = :wkClienteId;
// Host variables en DELETE
exec sql
DELETE FROM MILIB.CLIENTES
WHERE CLIID = :wkClienteId;SELECT INTO se usa cuando esperas exactamente una fila como resultado. Si la consulta devuelve cero filas, SQLCODE sera 100 (not found). Si devuelve mas de una fila, SQLCODE sera -811 (result set has more than one row).
dcl-s wkNombre varchar(100);
dcl-s wkSaldo packed(11:2);
dcl-s wkTotal packed(13:2);
dcl-s wkConteo int(10);
dcl-s wkClienteId packed(7:0) inz(1001);
// Lectura simple por clave
exec sql
SELECT CLINOM, CLISALDO
INTO :wkNombre, :wkSaldo
FROM MILIB.CLIENTES
WHERE CLIID = :wkClienteId;
// Verificar resultado
select;
when sqlcode = 0;
// OK: encontrado
dsply ('Cliente: ' + %trim(wkNombre));
when sqlcode = 100;
// No encontrado
dsply 'Cliente no existe';
other;
// Error SQL
dsply ('Error SQL: ' + %char(sqlcode));
endsl;
// Funciones agregadas (siempre retornan una fila)
exec sql
SELECT COUNT(*), SUM(CLISALDO)
INTO :wkConteo, :wkTotal
FROM MILIB.CLIENTES
WHERE CLIACTIVO = 'A';
dsply ('Clientes activos: ' + %char(wkConteo) +
' Total: $' + %char(wkTotal));
// SELECT INTO con JOIN
dcl-s wkFactura packed(10:0) inz(50001);
dcl-s wkCliNom varchar(100);
dcl-s wkMonto packed(11:2);
dcl-s wkFecha date(*iso);
exec sql
SELECT c.CLINOM, f.FACMONTO, f.FACFECHA
INTO :wkCliNom, :wkMonto, :wkFecha
FROM MILIB.FACTURAS f
JOIN MILIB.CLIENTES c ON c.CLIID = f.FACCLIID
WHERE f.FACID = :wkFactura;Los cursores se usan cuando una consulta puede devolver multiples filas. Funcionan como un iterador: se declara, se abre, se leen filas una por una con FETCH, y se cierra. Es el patron equivalente a un ResultSet en JDBC o un cursor de base de datos en cualquier lenguaje.
dcl-s wkId packed(7:0);
dcl-s wkNombre varchar(100);
dcl-s wkSaldo packed(11:2);
dcl-s wkContador int(10) inz(0);
// Paso 1: DECLARE cursor (solo define, no ejecuta)
exec sql
DECLARE curClientes CURSOR FOR
SELECT CLIID, CLINOM, CLISALDO
FROM MILIB.CLIENTES
WHERE CLIACTIVO = 'A'
ORDER BY CLISALDO DESC;
// Paso 2: OPEN cursor (ejecuta la query)
exec sql OPEN curClientes;
if sqlcode <> 0;
dsply 'Error abriendo cursor';
return;
endif;
// Paso 3: FETCH en loop
dow sqlcode = 0;
exec sql
FETCH curClientes
INTO :wkId, :wkNombre, :wkSaldo;
if sqlcode = 0;
wkContador += 1;
// Procesar cada fila
dsply (%char(wkId) + ' ' + %trim(wkNombre) +
' $' + %char(wkSaldo));
endif;
enddo;
// Paso 4: CLOSE cursor
exec sql CLOSE curClientes;
dsply ('Total procesados: ' + %char(wkContador));dcl-ds clienteDS qualified;
id packed(7:0);
nombre varchar(100);
email varchar(200);
saldo packed(11:2);
fechaAlta date(*iso);
end-ds;
dcl-s wkMinSaldo packed(11:2) inz(1000);
exec sql
DECLARE curTopClientes CURSOR FOR
SELECT CLIID, CLINOM, CLIEMAIL, CLISALDO, CLIFECALTA
FROM MILIB.CLIENTES
WHERE CLISALDO >= :wkMinSaldo
ORDER BY CLISALDO DESC
FETCH FIRST 100 ROWS ONLY;
exec sql OPEN curTopClientes;
dow sqlcode = 0;
exec sql FETCH curTopClientes INTO :clienteDS;
if sqlcode = 0;
// Acceso con notacion punto
if clienteDS.saldo > 50000;
exec sql
UPDATE MILIB.CLIENTES
SET CLICATEG = 'PREMIUM'
WHERE CLIID = :clienteDS.id;
endif;
endif;
enddo;
exec sql CLOSE curTopClientes;// Cursor FOR UPDATE: permite UPDATE WHERE CURRENT OF
exec sql
DECLARE curActualizar SCROLL CURSOR FOR
SELECT CLIID, CLINOM, CLISALDO
FROM MILIB.CLIENTES
WHERE CLIACTIVO = 'A' AND CLISALDO < 0
FOR UPDATE OF CLISALDO, CLICATEG;
exec sql OPEN curActualizar;
dow sqlcode = 0;
exec sql
FETCH NEXT FROM curActualizar
INTO :wkId, :wkNombre, :wkSaldo;
if sqlcode = 0;
// Actualizar la fila actual del cursor directamente
exec sql
UPDATE MILIB.CLIENTES
SET CLICATEG = 'MOROSO',
CLISALDO = 0
WHERE CURRENT OF curActualizar;
endif;
enddo;
exec sql CLOSE curActualizar;INSENSITIVESnapshot: no ve cambios posteriores a OPEN
SENSITIVEDinamico: ve cambios en la tabla base
SCROLLPermite navegar hacia adelante y atras (FETCH PRIOR)
FOR UPDATEPermite UPDATE/DELETE WHERE CURRENT OF cursor
WITH HOLDCursor sobrevive un COMMIT (no se cierra)
FOR READ ONLYSolo lectura, permite mejor optimizacion
SQL dinamico permite construir sentencias SQL en tiempo de ejecucion. Se usa cuando la estructura de la consulta (columnas, tablas, condiciones) no se conoce en tiempo de compilacion. Es equivalente a usar prepared statements en JDBC con PreparedStatement.execute().
dcl-s sqlStmt varchar(1000);
dcl-s wkTabla varchar(128);
dcl-s wkBiblio varchar(10);
wkBiblio = 'MILIB';
wkTabla = 'TEMP_PROCESO';
// Construir y ejecutar SQL dinamicamente
sqlStmt = 'DELETE FROM ' + %trim(wkBiblio) + '.' +
%trim(wkTabla) +
' WHERE FECHA < CURRENT_DATE - 30 DAYS';
exec sql EXECUTE IMMEDIATE :sqlStmt;
if sqlcode = 0;
dsply 'Purga completada';
elseif sqlcode = 100;
dsply 'Sin registros para borrar';
else;
dsply ('Error: ' + %char(sqlcode));
endif;dcl-s sqlStmt varchar(1000);
dcl-s wkNombre varchar(100);
dcl-s wkEmail varchar(200);
dcl-s wkId packed(7:0);
// PREPARE: compila el SQL una vez (con ? como placeholders)
sqlStmt = 'UPDATE MILIB.CLIENTES ' +
'SET CLINOM = ?, CLIEMAIL = ? ' +
'WHERE CLIID = ?';
exec sql PREPARE updCliente FROM :sqlStmt;
// EXECUTE: ejecuta multiples veces con distintos valores
wkId = 1001;
wkNombre = 'Fernando Secchi';
wkEmail = 'fernando@ejemplo.com';
exec sql EXECUTE updCliente USING :wkNombre, :wkEmail, :wkId;
wkId = 1002;
wkNombre = 'Maria Lopez';
wkEmail = 'maria@ejemplo.com';
exec sql EXECUTE updCliente USING :wkNombre, :wkEmail, :wkId;
// PREPARE + cursor para SELECT dinamico
dcl-s wkResultNom varchar(100);
dcl-s wkResultSaldo packed(11:2);
sqlStmt = 'SELECT CLINOM, CLISALDO FROM MILIB.CLIENTES ' +
'WHERE CLICATEG = ? ORDER BY CLISALDO DESC';
exec sql PREPARE selCateg FROM :sqlStmt;
exec sql DECLARE curDinamico CURSOR FOR selCateg;
dcl-s wkCategoria char(10) inz('PREMIUM');
exec sql OPEN curDinamico USING :wkCategoria;
dow sqlcode = 0;
exec sql FETCH curDinamico INTO :wkResultNom, :wkResultSaldo;
if sqlcode = 0;
dsply (%trim(wkResultNom) + ': $' + %char(wkResultSaldo));
endif;
enddo;
exec sql CLOSE curDinamico;Las indicator variables (indicadores null) son variables INT(5) que acompanan a una host variable para detectar valores NULL. Si el indicador es -1, el valor SQL es NULL. Si es 0, el valor es valido. Son esenciales cuando las columnas permiten NULL.
dcl-s wkNombre varchar(100);
dcl-s wkEmail varchar(200);
dcl-s wkTelefono varchar(20);
dcl-s wkClienteId packed(7:0) inz(1001);
// Indicadores: INT(5), uno por cada columna nullable
dcl-s indEmail int(5);
dcl-s indTelefono int(5);
exec sql
SELECT CLINOM, CLIEMAIL, CLITELEFONO
INTO :wkNombre, :wkEmail :indEmail,
:wkTelefono :indTelefono
FROM MILIB.CLIENTES
WHERE CLIID = :wkClienteId;
if sqlcode = 0;
dsply ('Nombre: ' + %trim(wkNombre));
if indEmail = 0;
dsply ('Email: ' + %trim(wkEmail));
else;
dsply 'Email: (sin dato)'; // NULL
endif;
if indTelefono = 0;
dsply ('Tel: ' + %trim(wkTelefono));
else;
dsply 'Tel: (sin dato)'; // NULL
endif;
endif;
// Insertar con NULL usando indicador
wkNombre = 'Nuevo Cliente';
wkEmail = '';
indEmail = -1; // Indicar que es NULL
wkTelefono = '11-4567-8900';
indTelefono = 0; // Indicar que tiene valor
exec sql
INSERT INTO MILIB.CLIENTES
(CLINOM, CLIEMAIL, CLITELEFONO)
VALUES
(:wkNombre, :wkEmail :indEmail,
:wkTelefono :indTelefono);dcl-ds clienteDS qualified; id packed(7:0); nombre varchar(100); email varchar(200); telefono varchar(20); saldo packed(11:2); end-ds; // Array de indicadores (uno por campo de la DS) dcl-s indicadores int(5) dim(5); exec sql SELECT CLIID, CLINOM, CLIEMAIL, CLITELEFONO, CLISALDO INTO :clienteDS :indicadores FROM MILIB.CLIENTES WHERE CLIID = 1001; // indicadores(1) -> CLIID null? // indicadores(2) -> CLINOM null? // indicadores(3) -> CLIEMAIL null? // indicadores(4) -> CLITELEFONO null? // indicadores(5) -> CLISALDO null? if indicadores(3) = -1; dsply 'Email es NULL'; endif;
El commitment control en Db2 for i define como se manejan las transacciones. Determina cuando los cambios se hacen permanentes y que nivel de aislamiento tienen las lecturas. Es equivalente a los niveles de aislamiento de transacciones en cualquier base de datos relacional (READ UNCOMMITTED, READ COMMITTED, etc.).
*NONESin transaccionCada sentencia es auto-commit (sin rollback)*CHGREAD UNCOMMITTEDLee datos no confirmados (dirty reads posibles)*CSREAD COMMITTEDSolo lee datos confirmados (cursor stability)*ALLREPEATABLE READBloquea todos los registros leidos*RRSERIALIZABLEAislamiento total, maximo bloqueo// El nivel de commit se define en compilacion:
// CRTSQLRPGI ... COMMIT(*CHG)
// O se puede cambiar en runtime con SET TRANSACTION
dcl-s wkError ind inz(*off);
// Iniciar logica transaccional
exec sql
INSERT INTO MILIB.FACTURAS
(FACID, FACCLIID, FACMONTO, FACFECHA)
VALUES
(50001, 1001, 15000.00, CURRENT_DATE);
if sqlcode <> 0;
wkError = *on;
endif;
if not wkError;
exec sql
UPDATE MILIB.CLIENTES
SET CLISALDO = CLISALDO + 15000.00
WHERE CLIID = 1001;
if sqlcode <> 0;
wkError = *on;
endif;
endif;
// Confirmar o revertir la transaccion completa
if not wkError;
exec sql COMMIT;
dsply 'Transaccion confirmada';
else;
exec sql ROLLBACK;
dsply 'Transaccion revertida por error';
endif;COMMIT(*NONE) por simplicidad y rendimiento. Para aplicaciones interactivas o procesos que modifican multiples tablas relacionadas, usa COMMIT(*CHG) o *CS con COMMIT/ROLLBACK explicito.Despues de cada sentencia SQL embebida, Db2 actualiza las variables de estado SQLCODE y SQLSTATE. Ademas, GET DIAGNOSTICS permite obtener informacion detallada del ultimo error o condicion.
SQLCODE = 0Exito: sentencia ejecutada correctamente
SQLCODE = 100No encontrado: SELECT sin resultados o fin de cursor
SQLCODE > 0Advertencia: sentencia ejecutada con condicion
SQLCODE < 0Error: sentencia fallo (verificar SQLSTATE)
SQLSTATE '00000'OK: ejecucion exitosa sin condiciones
SQLSTATE '02000'Not found: no hay mas filas
dcl-s wkNombre varchar(100);
exec sql
SELECT CLINOM INTO :wkNombre
FROM MILIB.CLIENTES
WHERE CLIID = 9999;
// Verificacion basica con SQLCODE
select;
when sqlcode = 0;
dsply ('Encontrado: ' + %trim(wkNombre));
when sqlcode = 100;
dsply 'Cliente no encontrado';
when sqlcode < 0;
// Error: obtener detalles con GET DIAGNOSTICS
dcl-s wkMsgText varchar(500);
dcl-s wkSQLState char(5);
exec sql
GET DIAGNOSTICS CONDITION 1
:wkMsgText = MESSAGE_TEXT,
:wkSQLState = RETURNED_SQLSTATE;
dsply ('Error SQL ' + %char(sqlcode) +
' Estado: ' + wkSQLState);
dsply ('Mensaje: ' + %trim(wkMsgText));
endsl;
// Obtener numero de filas afectadas
exec sql
DELETE FROM MILIB.TEMPORAL
WHERE FECHA < CURRENT_DATE - 90 DAYS;
dcl-s wkRowCount int(10);
exec sql
GET DIAGNOSTICS :wkRowCount = ROW_COUNT;
dsply ('Registros eliminados: ' + %char(wkRowCount));dcl-proc logErrorSQL;
dcl-pi *n;
operacion varchar(50) const;
end-pi;
dcl-s msgText varchar(500);
dcl-s sqlSt char(5);
dcl-s sqlCd int(10);
sqlCd = sqlcode;
exec sql
GET DIAGNOSTICS CONDITION 1
:msgText = MESSAGE_TEXT,
:sqlSt = RETURNED_SQLSTATE;
// Registrar en tabla de errores
exec sql
INSERT INTO MILIB.ERROR_LOG
(ERRTIMESTAMP, ERROPERACION, ERRSQLCODE,
ERRSQLSTATE, ERRMENSAJE)
VALUES
(CURRENT_TIMESTAMP, :operacion, :sqlCd,
:sqlSt, :msgText);
// Commit del log independiente de la transaccion
exec sql COMMIT;
end-proc;
// Uso del procedimiento:
exec sql
UPDATE MILIB.CLIENTES SET CLISALDO = :nuevoSaldo
WHERE CLIID = :clienteId;
if sqlcode < 0;
logErrorSQL('UPDATE CLIENTES id=' + %char(clienteId));
exec sql ROLLBACK;
endif;SQL embebido en RPG puede ser extremadamente eficiente si se siguen buenas practicas. El optimizador de Db2 for i es muy sofisticado, pero necesita ayuda del programador para trabajar al maximo.
// 1. SIEMPRE usar host variables (no concatenar valores)
// MAL: Db2 no puede reutilizar el plan de acceso
sqlStmt = 'SELECT * FROM CLIENTES WHERE CLIID = ' +
%char(wkId);
exec sql EXECUTE IMMEDIATE :sqlStmt;
// BIEN: Db2 reutiliza el plan para cualquier valor de :wkId
exec sql
SELECT CLINOM, CLISALDO INTO :wkNom, :wkSaldo
FROM MILIB.CLIENTES
WHERE CLIID = :wkId;
// 2. Usar FETCH FIRST n ROWS ONLY (como LIMIT en MySQL)
exec sql
DECLARE curTop CURSOR FOR
SELECT CLINOM, CLISALDO
FROM MILIB.CLIENTES
ORDER BY CLISALDO DESC
FETCH FIRST 10 ROWS ONLY;
// 3. Seleccionar solo columnas necesarias (nunca SELECT *)
// MAL: trae todas las columnas, I/O innecesario
exec sql SELECT * INTO :clienteDS FROM MILIB.CLIENTES ...;
// BIEN: solo trae lo que necesitas
exec sql SELECT CLINOM, CLISALDO INTO :wkNom, :wkSaldo ...;
// 4. Blocked FETCH: leer multiples filas de una vez
dcl-ds arrayClientes qualified dim(100);
id packed(7:0);
nombre varchar(100);
saldo packed(11:2);
end-ds;
dcl-s wkFetched int(10);
exec sql
DECLARE curBloque CURSOR FOR
SELECT CLIID, CLINOM, CLISALDO
FROM MILIB.CLIENTES WHERE CLIACTIVO = 'A';
exec sql OPEN curBloque;
dow sqlcode = 0;
exec sql
FETCH curBloque FOR 100 ROWS
INTO :arrayClientes;
exec sql GET DIAGNOSTICS :wkFetched = ROW_COUNT;
for i = 1 to wkFetched;
// Procesar arrayClientes(i) - 100 filas por viaje
endfor;
enddo;
exec sql CLOSE curBloque;
// 5. OPTIMIZE FOR n ROWS (hint al optimizador)
exec sql
DECLARE curOpt CURSOR FOR
SELECT CLIID, CLINOM FROM MILIB.CLIENTES
WHERE CLIACTIVO = 'A'
OPTIMIZE FOR 50 ROWS;Practicas que degradan rendimiento
Practicas que mejoran rendimiento
Un programa SQLRPGLE completo que genera un reporte de clientes con deuda, incluyendo cursores, manejo de errores, data structures y actualizacion de categorias basada en reglas de negocio.
**free
// ---------------------------------------------------------------
// RPTDEUDA: Reporte de clientes con saldo deudor
// Genera reporte y actualiza categorias
// ---------------------------------------------------------------
ctl-opt dftactgrp(*no) actgrp(*caller);
ctl-opt option(*srcstmt : *nodebugio);
// ---------------------------------------------------------------
// Interfaz del programa
// ---------------------------------------------------------------
dcl-pi *n;
prmMinDeuda packed(11:2); // Deuda minima a reportar
prmConteo packed(7:0); // Retorno: registros procesados
end-pi;
// ---------------------------------------------------------------
// Variables de trabajo
// ---------------------------------------------------------------
dcl-ds deudorDS qualified;
id packed(7:0);
nombre varchar(100);
email varchar(200);
saldo packed(11:2);
diasDeuda int(10);
end-ds;
dcl-s indEmail int(5);
dcl-s wkContador packed(7:0) inz(0);
dcl-s wkTotalDeuda packed(13:2) inz(0);
dcl-s wkCategoria char(10);
dcl-s wkMsgText varchar(500);
// ---------------------------------------------------------------
// Cursor: clientes con deuda, con JOIN a pagos
// ---------------------------------------------------------------
exec sql
DECLARE curDeudores CURSOR FOR
SELECT c.CLIID,
c.CLINOM,
c.CLIEMAIL,
c.CLISALDO,
DAYS(CURRENT_DATE) - DAYS(
COALESCE(p.ULTIMO_PAGO, c.CLIFECALTA)
) AS DIAS_DEUDA
FROM MILIB.CLIENTES c
LEFT JOIN (
SELECT PAGCLIID,
MAX(PAGFECHA) AS ULTIMO_PAGO
FROM MILIB.PAGOS
GROUP BY PAGCLIID
) p ON p.PAGCLIID = c.CLIID
WHERE c.CLISALDO < :prmMinDeuda * -1
AND c.CLIACTIVO = 'A'
ORDER BY c.CLISALDO ASC;
// ---------------------------------------------------------------
// Logica principal
// ---------------------------------------------------------------
exec sql OPEN curDeudores;
if sqlcode <> 0;
exec sql GET DIAGNOSTICS CONDITION 1
:wkMsgText = MESSAGE_TEXT;
dsply ('Error cursor: ' + %trim(wkMsgText));
prmConteo = 0;
*inlr = *on;
return;
endif;
dow sqlcode = 0;
exec sql
FETCH curDeudores
INTO :deudorDS.id, :deudorDS.nombre,
:deudorDS.email :indEmail,
:deudorDS.saldo, :deudorDS.diasDeuda;
if sqlcode <> 0;
leave;
endif;
wkContador += 1;
wkTotalDeuda += %abs(deudorDS.saldo);
// Categorizar segun dias de deuda
select;
when deudorDS.diasDeuda > 180;
wkCategoria = 'INCOBRABLE';
when deudorDS.diasDeuda > 90;
wkCategoria = 'CRITICO';
when deudorDS.diasDeuda > 30;
wkCategoria = 'MOROSO';
other;
wkCategoria = 'PENDIENTE';
endsl;
// Actualizar categoria
exec sql
UPDATE MILIB.CLIENTES
SET CLICATEG = :wkCategoria
WHERE CLIID = :deudorDS.id;
// Insertar en tabla de reporte temporal
exec sql
INSERT INTO QTEMP.RPT_DEUDA
(RPTCLIID, RPTNOMBRE, RPTSALDO,
RPTDIASDEUDA, RPTCATEGORIA)
VALUES
(:deudorDS.id, :deudorDS.nombre,
:deudorDS.saldo, :deudorDS.diasDeuda,
:wkCategoria);
enddo;
exec sql CLOSE curDeudores;
prmConteo = wkContador;
*inlr = *on;
return;