An RPG program can contain SQL directly in the code. The source takes member type
SQLRPGLE, a precompiler turns the EXEC SQL statements into calls before the compiler
sees the code, and the result is an ordinary program object.
CRTSQLRPGI OBJ(PRODLIB/INVOICES) SRCFILE(PRODLIB/QRPGLESRC) +
COMMIT(*NONE) DBGVIEW(*SOURCE)
How it is written
**FREE
dcl-s total packed(11:2);
dcl-s customer char(10) inz('C0001');
exec sql
SELECT SUM(ORDAMT)
INTO :total
FROM ORDERS
WHERE ORDCUS = :customer
AND ORDDAT >= CURRENT DATE - 1 YEAR;
Program variables are used inside the SQL prefixed with a colon: they are called host
variables, and they are the bridge between the two worlds. They work both inbound — like
:customer above — and outbound, with INTO.
More than one row needs a cursor:
exec sql
DECLARE C1 CURSOR FOR
SELECT ORDNUM, ORDAMT
FROM ORDERS
WHERE ORDCUS = :customer
ORDER BY ORDDAT DESC;
exec sql OPEN C1;
dow sqlstate < '02000';
exec sql FETCH C1 INTO :number, :amount;
if sqlstate < '02000';
// process
endif;
enddo;
exec sql CLOSE C1;
How the outcome is checked
And this is where people go wrong most often. After every SQL statement the system fills two variables, which the precompiler declares by itself:
SQLCODE— numeric:0fine,100no rows found, negative an error.SQLSTATE— five characters:'00000'fine,'02000'no rows, everything else worth a look.
Warning
SQLCODE = 0 does not mean everything went well. It means there was no
error. Warnings — truncation into a variable that is too short, an approximate conversion
— pass with SQLCODE positive or zero, and the program carries on with wrong data without
anyone noticing.
The check that holds is on SQLSTATE, which separates the three cases: '00000'
succeeded, '02000' no rows, class '01' warning, everything else an error. The
convention if sqlstate < '02000' covers success and warnings by treating them alike, and
is the form most often seen: fine for a read loop, not fine for a write.
On an UPDATE or DELETE, moreover, "no error" does not mean "it did something": the
number of rows affected is in SQLERRD(3), and a change that found nothing to change is a
case to handle, not a success.
The options that matter
exec sql SET OPTION
commit = *none,
closqlcsr = *endmod,
datfmt = *iso,
naming = *sys;
Four lines that settle four categories of surprise:
commit— with no commitment control,*none. Left at the default, the program opens transactions nobody closes, and records stay locked.closqlcsr— when cursors close.*endmodcloses them at the end of the module instead of the end of the program, which is what a service program needs.datfmt— the date format.*isoremoves the ambiguity between day and month, which otherwise depends on the job's settings.naming—*sysusesLIBRARY/OBJECTand the library list;*sqlusesSCHEMA.OBJECT. Choosing beats inheriting: where the program looks for its files depends on this.
Dynamic SQL, and injection
When the query is composed at run time you need dynamic SQL, and here the risk is exactly that of any other platform:
// Wrong: the contents of :filter end up inside the statement
sql = 'SELECT * FROM ORDERS WHERE ORDCUS = ''' + filter + '''';
exec sql PREPARE S1 FROM :sql;
// Right: the value stays a value
sql = 'SELECT * FROM ORDERS WHERE ORDCUS = ?';
exec sql PREPARE S1 FROM :sql;
exec sql DECLARE C2 CURSOR FOR S1;
exec sql OPEN C2 USING :filter;
Important
the idea that SQL injection is a web problem, and therefore not one here,
has been wrong ever since interfaces started bringing outside data into the application: a
web service, a procedure called from another system, a file imported from a supplier. The
? marker costs one extra line and removes the whole class of problems, besides letting
the database reuse the access plan instead of recomputing it on every call.
When it is worth it and when not
Embedded SQL does not replace native reads: it sits alongside them, and the choice is made case by case.
It is right for totals and groupings, for reading from several files at once, for
filters spanning many columns, and for every set operation — an UPDATE touching ten
thousand rows is one line of SQL against a whole loop.
It is not right for fetching one specific record whose key you know. There CHAIN is
more direct, more readable and no slower.
Tip
this is the modernisation you can do one program at a time without asking anyone's permission. It does not change the files, does not change the interfaces, does not force a recompile of the rest of the application: you change how a single program reads its data, and you measure.
Comments
No comments yet. Be the first to comment!
You need an account to comment. Log in · Sign up