Metaprogramming in SQL (Part 2)

In the last post on this subject, we used dynamic SQL to generalize an integer comparison function into a stored procedure that builds comparison functions of any basic type. This served as a simple example, but did not really offer much benefit over using the ‘=’ operator. In today’s post, I will extend the same technique to make functions that compare ROW objects just as easily, even though the ‘=’ operator cannot be used on ROW objects.

First of all, what does it mean to say that two rows are identical? Let’s define row A to be equal to row B when:

  1. The types of row A and row B are the same
  2. Each field in row A has the same value as the same field in row B

The first requirement is satisfied by the strong typing provided by SQL PL. The second will require us to look inside each row for its individual fields to make a proper comparison. In an earlier post, I showed you an example of a comparison procedure for two objects of a ROW type called TEST_T. It looked like this:

<!--start_raw-->CREATE OR REPLACE TYPE TEST_T AS ROW(ONE INTEGER)@
 
CREATE OR REPLACE PROCEDURE EQ(IN LEFT TEST_T, IN RIGHT TEST_T, OUT RESULT INTEGER)
BEGIN
  IF (LEFT.ONE = RIGHT.ONE) THEN
    SET RESULT = 1;
  ELSE
    SET RESULT = 0;
  END IF;
END@<!--end_raw-->

With metaprogramming, we can generalize this into a procedure that creates row comparison functions for any ROW type. But first we need a way of discovering what fields make up a row. Fortunately, there is a catalog view called SYSCAT.ROWFIELDS that tells the name and type of every field in a row. Say we query this catalog view for the TEST_T type we created above:

<!--start_raw-->SELECT VARCHAR(FIELDNAME,10) FIELDNAME,
       VARCHAR(FIELDTYPESCHEMA,10) FIELDTYPESCHEMA,
       VARCHAR(FIELDTYPEMODULENAME,10) FIELDTYPEMODULENAME,
       VARCHAR(FIELDTYPENAME,10) FIELDTYPENAME
FROM SYSCAT.ROWFIELDS
WHERE TYPESCHEMA = (VALUES CURRENT SCHEMA)
  AND TYPEMODULENAME IS NULL
  AND TYPENAME = 'TEST_T'<!--end_raw-->

We get a result like this:

<!--start_raw-->FIELDNAME  FIELDTYPESCHEMA FIELDTYPEMODULENAME FIELDTYPENAME
---------- --------------- ------------------- -------------
ONE        SYSIBM          -                   INTEGER      

  1 record(s) selected.<!--end_raw-->

We’ll put some of this information (specifically, the fieldname) to use in our metaprogram, but let’s follow the process we followed last time and start by turning the above comparison procedure into a metaprogram with hardcoded inputs:

<!--start_raw-->CREATE OR REPLACE PROCEDURE CREATE_TYPE_T_EQ_FUNCTION()
MODIFIES SQL DATA
BEGIN
  DECLARE SQL VARCHAR(1024);
  SET SQL =        'CREATE OR REPLACE FUNCTION EQ(LEFT TEST_T, RIGHT TEST_T) ';
  SET SQL = SQL || 'CONTAINS SQL ';
  SET SQL = SQL || 'RETURNS INTEGER ';
  SET SQL = SQL || 'BEGIN ';
  SET SQL = SQL || 'DECLARE RESULT INTEGER; ';
  SET SQL = SQL || 'IF (LEFT.ONE = RIGHT.ONE) THEN ';
  SET SQL = SQL || '  SET RESULT = 1; ';
  SET SQL = SQL || 'ELSE ';
  SET SQL = SQL || '  SET RESULT = 0; ';
  SET SQL = SQL || 'END IF; ';
  SET SQL = SQL || 'RETURN RESULT; ';
  SET SQL = SQL || 'END';
  EXECUTE IMMEDIATE SQL;
END@<!--end_raw-->

Next, using the SYSCAT.ROWFIELDS catalog view, we generalize the above function to take the ROW type as an argument. Since the type name needs to be qualified by the schema and the module name, we have an extra challenge to deal with. The module name is NULL when the type is not defined inside a module and NULLs cannot be compared using the ‘=’ operator. One solution is to write two queries of the SYSCAT.ROWFIELDS view, one that assumes the module name is non-NULL and a second that assumes the module name is NULL. We choose which query to use when we check whether the module name argument is NULL.

<!--start_raw-->CREATE OR REPLACE PROCEDURE CREATE_EQ_FUNCTION(TYPE_SCHEMA VARCHAR(128),
                                               TYPE_MODULE_NAME VARCHAR(128),
                                               TYPE_NAME VARCHAR(128))
MODIFIES SQL DATA
BEGIN
  DECLARE SQL VARCHAR(1024);
  DECLARE FULLYQUALIFIEDTYPE VARCHAR(386);
  DECLARE AT_END INT DEFAULT 0;
  DECLARE FIELD_NAME VARCHAR(128);
  DECLARE ROWFIELDSCURSOR CURSOR FOR SELECT FIELDNAME
                                            FROM SYSCAT.ROWFIELDS 
                                            WHERE TYPESCHEMA = TYPE_SCHEMA
                                              AND TYPEMODULENAME IS NULL
                                              AND TYPENAME = TYPE_NAME;
  DECLARE ROWFIELDSMODCURSOR CURSOR FOR SELECT FIELDNAME
                                            FROM SYSCAT.ROWFIELDS 
                                            WHERE TYPESCHEMA = TYPE_SCHEMA
                                              AND TYPEMODULENAME = TYPE_MODULE_NAME
                                              AND TYPENAME = TYPE_NAME;
  DECLARE CONTINUE HANDLER FOR NOT FOUND
    SET AT_END = 1;

  -- Types need to be qualified by their schema, and if they are in a module, by the module too.
  IF (TYPE_MODULE_NAME IS NULL) THEN
    SET FULLYQUALIFIEDTYPE = TYPE_SCHEMA || '.' || TYPE_NAME;
  ELSE
    SET FULLYQUALIFIEDTYPE = TYPE_SCHEMA || '.' || TYPE_MODULE_NAME || '.' || TYPE_NAME || ' ';
  END IF;
  SET SQL =        'CREATE OR REPLACE FUNCTION EQ(LEFT ' || FULLYQUALIFIEDTYPE;
  SET SQL = SQL || ', RIGHT ' || FULLYQUALIFIEDTYPE || ') ';
  SET SQL = SQL || 'READS SQL DATA ';
  SET SQL = SQL || 'RETURNS INTEGER ';
  SET SQL = SQL || 'BEGIN ';
  SET SQL = SQL || 'DECLARE RESULT INTEGER; ';
  SET SQL = SQL || 'IF (';

  IF (TYPE_MODULE_NAME IS NULL) THEN
    -- Iterate over the row fields to build the Boolean expression
    OPEN ROWFIELDSCURSOR;
    FETCH ROWFIELDSCURSOR INTO FIELD_NAME;
    WHILE AT_END = 0 DO
      SET SQL = SQL || 'LEFT.' || FIELD_NAME || ' = ' || 'RIGHT.' || FIELD_NAME;
      FETCH ROWFIELDSCURSOR INTO FIELD_NAME;
      IF (AT_END = 0) THEN
        SET SQL = SQL || ' AND ';
      END IF;
    END WHILE;
    CLOSE ROWFIELDSCURSOR;
  ELSE
    -- Iterate over the row fields to build the Boolean expression
    OPEN ROWFIELDSMODCURSOR;
    FETCH ROWFIELDSMODCURSOR INTO FIELD_NAME;
    WHILE AT_END = 0 DO
      SET SQL = SQL || 'LEFT.' || FIELD_NAME || ' = ' || 'RIGHT.' || FIELD_NAME;
      FETCH ROWFIELDSMODCURSOR INTO FIELD_NAME;
      IF (AT_END = 0) THEN
        SET SQL = SQL || ' AND ';
      END IF;
    END WHILE;
    CLOSE ROWFIELDSMODCURSOR;
  END IF;

  SET SQL = SQL || ') THEN ';
  SET SQL = SQL || '    SET RESULT = 1; ';
  SET SQL = SQL || '  ELSE ';
  SET SQL = SQL || '  SET RESULT = 0; ';
  SET SQL = SQL || 'END IF; ';
  SET SQL = SQL || 'RETURN RESULT; ';
  SET SQL = SQL || 'END';
  CALL DBMS_OUTPUT.PUT_LINE(SQL);
  EXECUTE IMMEDIATE SQL;
END@<!--end_raw-->

Notice that I put in a CALL DBMS_OUTPUT.PUT_LINE to print the SQL before the EXECUTE IMMEDIATE. This makes it easy to debug whether the metaprogram is writing the function properly by inspecting the code it produces. Once the above function is created, we can call it on our TEST_T type and use the resulting EQ function to compare two rows of type TEST_T, as in this example program:

<!--start_raw-->SET SERVEROUTPUT ON@
CREATE OR REPLACE TYPE TEST_T AS ROW(ONE INTEGER)@

CREATE OR REPLACE MODULE TEST_MOD@
ALTER MODULE TEST_MOD PUBLISH
TYPE TEST_T AS ROW(TWO INTEGER)@

CALL CREATE_EQ_FUNCTION('KMCDONAL', NULL, 'TEST_T')@
CALL CREATE_EQ_FUNCTION('KMCDONAL', 'TEST_MOD', 'TEST_T')@

BEGIN
  DECLARE T1 TEST_T;
  DECLARE T2 TEST_T;
  DECLARE T3 TEST_MOD.TEST_T;
  DECLARE T4 TEST_MOD.TEST_T;

  SET T1.ONE = 1;
  SET T2.ONE = 1;
  CALL DBMS_OUTPUT.PUT_LINE('EQ(t1, t2) = ' || EQ(t1, t2));
  SET T2.ONE = 2;
  CALL DBMS_OUTPUT.PUT_LINE('EQ(t1, t2) = ' || EQ(t1, t2));

  SET T3.TWO = 1;
  SET T4.TWO = 1;
  CALL DBMS_OUTPUT.PUT_LINE('EQ(t3, t4) = ' || EQ(t3, t4));
  SET T4.TWO = 2;
  CALL DBMS_OUTPUT.PUT_LINE('EQ(t3, t4) = ' || EQ(t3, t4));

END@<!--end_raw-->

The above test produces the following output:

<!--start_raw-->DB20000I  The SET SERVEROUTPUT command completed successfully.

DB20000I  The SQL command completed successfully.

DB20000I  The SQL command completed successfully.

DB20000I  The SQL command completed successfully.


  Return Status = 0

CREATE OR REPLACE FUNCTION EQ(LEFT KMCDONAL.TEST_T, RIGHT KMCDONAL.TEST_T) CONTAINS SQL RETURNS INTEGER BEGIN DECLARE RESULT INTEGER; IF (LEFT.ONE = RIGHT.ONE) THEN     SET RESULT = 1;   ELSE   SET RESULT = 0; END IF; RETURN RESULT; END


  Return Status = 0

CREATE OR REPLACE FUNCTION EQ(LEFT KMCDONAL.TEST_MOD.TEST_T , RIGHT KMCDONAL.TEST_MOD.TEST_T ) CONTAINS SQL RETURNS INTEGER BEGIN DECLARE RESULT INTEGER; IF (LEFT.TWO = RIGHT.TWO) THEN     SET RESULT = 1;   ELSE   SET RESULT = 0; END IF; RETURN RESULT; END

DB20000I  The SQL command completed successfully.

EQ(t1, t2) = 1
EQ(t1, t2) = 0
EQ(t3, t4) = 1
EQ(t3, t4) = 0<!--end_raw-->

In Metaprogramming in SQL (Part 3), we will use the above EQ function to build a more powerful EQ function that can compare objects of the ARRAY type, which is one of the new types in DB2 9.7.