Hello everyone and welcome to part 2 of SQL Data Types in ILE RPG.
In this second part of SQL Data Types in ILE RPG there is only one target: XML_CLOB_FILE. I think it is important enough to dedicate a whole post to it. Through two examples we will see two different ways to generate and save well-formed XML content in the archive in IFS.
If you remember Part 1 which can be found at ILE RPG and SQL Data Type I showed you how you could write data to an IFS file. That data could be character or binary. For character data type it could be plain text or, as seen in one of the examples in the article, an HTML file that could be opened in any browser. Perhaps, seeing that you could write in a markup language like HTML, you came up with the idea of writing XML data in the file. If that was the case you will have realized that things did not go as expected and that the file did not save well. In any case, if you want to know how to save an XML file in the IFS using the XML_CLOB_FILE data type, read on.
This is exactly the same information that was published at ILE RPG and SQL Data Type
LOB files are used to transfer data from and to IFS files.
They represent a file, they do not contain it. You can use database queries, updates and inserts to store or to retrieve LOB values without the need of routines to read and write files. LOB files are allowed in host data structures and cannot be initialized.
LOB Files variables can be CLOB, DBCLOB or BLOB data type. The precompiler will generate a data structure with subfileds relative to the file using suffixes as follow:
The precompiler will also generate four constants:
The declaration of:
dcl-s xmlClobFile sqltype(xml_clob_file);
generates this code:
DCL-DS XMLCLOBFILE;
XMLCLOBFILE_NL UNS(10);
XMLCLOBFILE_DL UNS(10);
XMLCLOBFILE_FO UNS(10);
XMLCLOBFILE_NAME CHAR(255) CCSID(*JOBRUNMIX);
END-DS XMLCLOBFILE;
At first glance it appears that there is no change from a normal CLOB FILE or non-XML but there is. Let’s go a little further.
XMLPARSE(DOCUMENT argument {STRIP WHITESPACE|PRESERVE WHITESPACE})
The XMLGROUP SQL function on IBM i is used to generate an XML document from a set of rows. It’s an aggregation function, meaning it operates on a group of rows and returns a single XML value. This function is particularly useful when you need to transform relational data into a hierarchical XML structure.
Key Features and Limitations of XMLGROUP:
The syntax is:
XMLGROUP (
expression [ AS elementName ] [, expression [ AS elementName ] ... ]
[ ORDER BY sortSpec [, sortSpec ... ] ]
[ OPTION ROW "rowElementName" ]
[ OPTION ROOT "rootElementName" ]
[ OPTION AS ATTRIBUTES ]
)
So we have an xml_clob_file which is an sqltype in RPGLE, XMLPARSE which is an scalar SQL Function that takes a character string that contains well-formed XML and turns it into an XML document object that can be used with SQL, XMLGROUP which is an aggregate function that access a table or physical file and generate an XML with its contents and we have the IFS which is the place where we are going to write the XML file. So let me put all these things together in two different and complete examples.
In this first example I will use the XMLPARSE function together with a variable of type varchar that will contain the XML. There are many ways to achieve this but on this case I am going to use manual construction of the xml into a string, parse it to an XML document and save it to the IFS via an xml_clob_file.
XMLCLOBFIL.SQLRPGLE:
**free
ctl-opt option(*nodebugio:*srcstmt:*nounref);
dcl-s xmlBooks varchar(32000) ccsid(*utf8);
dcl-s ifsFile1 sqltype(xml_clob_file);
// The compiler will generate the following:
//
// dcl-ds ifsFile1 inz;
// ifsFile1_nl uns(10);
// ifsFile1_dl uns(10);
// ifsFile1_fo uns(10);
// ifsFile1_name char(255);
// end-ds;
//
// _name = Full path of the file name
// _nl = Length of the file name
// _fo = Operation to be performed on the file (see constants below)
// _dl = Not used
//
// The compiler also adds these constants:
// SQFRD = 2 = Read the file
// SQFCRT = 4 = Create the file
// SQFOVR = 8 = Overwrite the file
// SQFAPP = 16 = Append to the end of the file
// XML start
xmlBooks = '' +
'' +
'' +
'RPG IV for the Brave ' +
'Elliot Randall ' +
'CodeForge Press ' +
'2020-03-15 ' +
'9781111111111 ' +
'Programming ' +
'450 ' +
'59.99 ' +
'Y ' +
' ' +
'' +
'The IBM i Journey ' +
'Nora Blake ' +
'IronCore Publishing ' +
'2018-07-01 ' +
'9781111111112 ' +
'Systems ' +
'300 ' +
'39.95 ' +
'Y ' +
' ' +
'' +
'SQL Spells for Db2 ' +
'Liam Carter ' +
'DataScroll Books ' +
'2019-11-10 ' +
'9781111111113 ' +
'Database ' +
'380 ' +
'49.95 ' +
'Y ' +
' ' +
' ';
ifsFile1_name = '/home/asalcedo/xml_test/books.xml';
ifsFile1_nl = %len(%trim(ifsFile1_name));
ifsFile1_fo = SQFOVR;
exec sql set :ifsFile1 = XMLParse(Document :xmlBooks);
if (sqlcode = 0);
snd-msg 'File books.xml created in ' + ifsFile1_name %target(*self:2);
else;
snd-msg 'Error creating books.xml. SQLCODE = ' + %char(sqlcode) %target(*self:2);
endif;
*inlr = *on;
Explanation:
Line 1: Code will be fully free.
Line 3: Control options I usually use.
Line 5: xmlBooks varchar is declared with enough length to hold the XML and more but remember that the length of a varchar variable is only the length of its contents plus 2 or 4 bytes depending on the maximum declared, up to 65535 2 bytes and > 65535 4 bytes. As it will be used to hold an XML the character code identifier is set to UTF-8.
Line 6: ifsFile1 file is declared. Its type is SQL type xml_clob_file so it will hold an XML file.
Lines 7-25: The compiler will generate a data structure to hold the information for the xml_clob_file so I have written, commented and explained, what the compiler generate for us.
Lines 28-63: I am assigning a character string that represent a well-formed XML with the information of three books to xmlBooks.
Line 65: I set the name of the file using a full path in which I want to save the XML in the IFS.
Line 66: I need to set the length of the previous path.
Line 67: Finally I need to set how I want to use the file. If you see the previous comments in the code you can see that the constant SQFOVR means that the file will be created if it doesn’t exists or will be overwritten if it exists.
Line 69: This is the most important statement in the program. First, the XMLParse function will check that the content of xmlBooks is a well-formed XML and return the document. Next I will assign this document to the file using the SQL set statement. At this point the system will check for the existence of the books.xml file in the /home/asalcedo/xml_test directory. If any of these directories are missing, an error will occur. If all of them exist, the books.xml file will be created or overwritten and its content will be set to the document returned by the XMLParse function.
Lines 71-75: If everything went well a message is sent informing that the file has been created correctly and where. If there has been an error, the error code is reported. The snd-msg command uses the %target(*self:2) function to specify that the message, in addition to the joblog, will be received by the program itself but with an offset in the call stack of 2 positions resulting in it being on the message line of the screen.
Once the program runs, a message appears confirming the XML creation. The XML content is shown below
BOOKS.XML:
RPG IV for the Brave
Elliot Randall
CodeForge Press
2020-03-15
9781111111111
Programming
450
59.99
Y
The IBM i Journey
Nora Blake
IronCore Publishing
2018-07-01
9781111111112
Systems
300
39.95
Y
SQL Spells for Db2
Liam Carter
DataScroll Books
2019-11-10
9781111111113
Database
380
49.95
Y
In this second example I will use the XMLGROUP function to access a database table and process all the records in an XML file. The XMLGROUP function allows us to select only the records we need as in an SQL SELECT statement.
Take a look at the contents of the book_info table. When the program generates the XML file the content should be the same.
One constraint is that only books published in 2019 or later will be included in the XML file; therefore, books 2, 4, 7, 10, and 14 should not be part of the XML file.
XMLCLOBFI2.SQLRPGLE:
**free
ctl-opt option(*nodebugio:*srcstmt:*nounref);
dcl-s ifsFile1 sqltype(xml_clob_file);
// Initialize the XML_CLOB_FILE variable
ifsFile1_name = '/home/asalcedo/xml_test/books2.xml';
ifsFile1_nl = %len(%trim(ifsFile1_name));
ifsFile1_fo = SQFOVR;
// Generate the XML and save it to the XML_CLOB_FILE
exec sql
values(select xmlgroup(
book_id as "id",
title as "title",
author as "author",
publisher as "publisher",
published_date as "published_date",
isbn as "isbn",
genre as "genre",
pages as "pages",
price as "price",
available as "available"
order by book_id option row "book" root "books")
from asalcedo1.book_info
where published_date >= '2019-01-01')
into :ifsFile1;
if (sqlcode = 0);
snd-msg 'File books2.xml created in ' + ifsFile1_name %target(*self:2);
else;
snd-msg 'Error creating books2.xml. SQLCODE = ' + %char(sqlcode) %target(*self:2);
endif;
*inlr = *on;
Explanation:
Line 1: Code will be fully free.
Line 2: Control options I usually use.
Line 5: ifsFile1 file is declared. Its type is SQL type xml_clob_file so it will hold an XML file.
Line 8: I set the name of the file using a full path in which I want to save the XML in the IFS.
Line 9: I need to set the length of the previous path.
Line 10: Finally I need to set how I want to use the file. If you see the previous comments in the code you can see that the constant SQFOVR means that the file will be created if it doesn’t exists or will be overwritten if it exists.
Lines 14-28: The SQL statement uses the XMLGROUP function to generate XML elements, mapping table column values to XML tag values. The AS clause assigns specific tag names to each column—for example, the value of the book_id column becomes the value of the <id> tag, and so on.
Each row is represented as a group of XML tags, and by specifying OPTION ROW “book”, each group is wrapped in a <book> element.
To enclose all <book> elements within a single parent element named <books>, the OPTION ROOT “books” clause is used.
Finally, to ensure that the <book> elements are ordered by book_id, the ORDER BY book_id clause must be placed inside the XMLGROUP function, rather than after the WHERE clause of the outer SELECT.
The resulting XML is then stored in the IFS file ifsFile1.
Line 30-34: If everything went well a message is sent informing that the file has been created correctly and where. If there has been an error, the error code is reported. The snd-msg command uses the %target(*self:2) function to specify that the message, in addition to the joblog, will be received by the program itself but with an offset in the call stack of 2 positions resulting in it being on the message line of the screen.
Once the program runs, a message appears confirming the XML creation. The XML content is shown below
BOOKS2.XML:
1
RPG IV for the Brave
Elliot Randall
CodeForge Press
2020-03-15
9781111111111
Programming
450
59.99
Y
3
SQL Spells for Db2
Liam Carter
DataScroll Books
2019-11-10
9781111111113
Database
380
49.95
Y
5
Node & RPG Synergy
Derek Vaughn
FusionStack Press
2022-09-12
9781111111115
Web Development
410
55.00
Y
6
Microservices on the i
Maya Stevens
CloudCore Books
2023-01-30
9781111111116
Architecture
290
47.50
Y
8
ILE RPG Demystified
Chloe Bennett
NextByte Press
2021-06-01
9781111111118
Programming
330
42.95
Y
9
IBM i Security Blueprints
Julian Foster
SecureLayer Publishing
2019-02-05
9781111111119
Security
310
46.75
Y
11
REST & JSON on IBM i
Landon Myers
ServiceStack Press
2020-12-12
9781111111121
Web Services
295
44.00
Y
12
DevOps for Legacy Systems
Tessa Rayner
Pipeline Media
2021-11-11
9781111111122
DevOps
220
37.99
Y
13
The Open Source iBox
Isaac Tremblay
FreedomCode Books
2023-03-14
9781111111123
Open Source
340
53.25
Y
15
API Design with RPG
Nathan Cross
EndPoint Books
2022-06-20
9781111111125
API Development
390
57.50
Y
As you can see, the XML contains only the records returned by the SQL query.
If you want to revisit part 1 of SQL Data Types you can do it here: