The function assumes that the datatype of the foreign key is INTEGER; I have encountered many tables with VARCHAR2 foreign keys. True, the reasons are usually suspect, but it happens just the same. Can I change fk_name to handle some of these concerns? I can certainly add a parameter with a maximum name length to use in the call to COLUMN_VALUE. I can support VARCHAR2 datatypes for foreign keys by placing the function inside a package and overloading the definition of the function. What about all those parameters and the naming conventions? Ideally, I would like to allow a developer to call fk_name with no more information than in the following examples: :call.name := fk_name (:call.caller_id, 'caller'); :call.call_type_ds := fk_name (:call.call_type_id, 'call_type'); In this scenario, the function would use the name of the table to generate the names of the key and name columns and stuff them into the SQL statement. Sounds reasonable to me. The second version of fk_name supports default names for these two columns. In this version, the last three parameters have default values. If the user does not specify an ID column name, the default is the table name with the default suffix. The same goes for the fk_name column. If the user includes a value for either of these arguments, then if the value starts with an underscore ( _ ), it will be used as a suffix to the table name. Otherwise, the value will be used as a complete column name. The following table shows how parameter values will be converted inside the program. Column to fk_name Converted Value ID column NULL caller_id Name column NULL caller_nm ID column caller_number caller_number ID column caller_name caller_name ID column _# caller_# Name column _fullname caller_fullname Here, then, in Version 2, we have an even more generic function to return foreign key names. /* Filename on companion disk: fkname2.sf */* CREATE OR REPLACE FUNCTION fk_name (fk_id_in IN INTEGER, fk_table_in IN VARCHAR2, fk_id_col_in IN VARCHAR2 := '_ID', fk_nm_col_in IN VARCHAR2 := '_NM', max_length_in IN INTEGER := 100) RETURN VARCHAR2 /* I will not repeat any comments from first version of fk_name. */ IS /* || Local variables to hold column names, since I must construct || those names based on the values provided. If the column names || are NULL, then fall back on the defaults. */ fk_id_column VARCHAR2(60) := NVL (fk_id_col_in, '_ID'); fk_nm_column VARCHAR2(60) := NVL (fk_nm_col_in, '_NM'); cur INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk INTEGER; /* || The return value of the function. Notice that even though one || of the parameters now specifies a maximum size for the return || value, I still do have to hardcode a size in my declaration. [Appendix A] What's on the Companion Disk? 2.5.2 A Generic Foreign Key Lookup Function 101 */ return_value VARCHAR2(100) := NULL; /*−−−−−−−−−−−−−−−−−−−−−− Local Module −−−−−−−−−−−−−−−−−−−−−−−−−−−*/ PROCEDURE convert_column (col_name_inout IN OUT VARCHAR2) /* || Construct the column name. If the argument begins with a "_", || use as suffix to table name. Otherwise, substitute completely. */ IS BEGIN IF SUBSTR (col_name_inout, 1, 1) = '_' THEN col_name_inout := fk_table_in || col_name_inout; ELSE /* Default value on variable declaration already handles it */ NULL; END IF; END; BEGIN /* Convert the column names as necessary based on arguments */ convert_column (fk_id_column); convert_column (fk_nm_column); /* Parse statement using converted column names */ DBMS_SQL.PARSE (cur, 'SELECT ' || fk_nm_column || ' FROM ' || fk_table_in || ' WHERE ' || fk_id_column || ' = :fk_value', DBMS_SQL.NATIVE); DBMS_SQL.BIND_VARIABLE (cur, 'fk_value', fk_id_in); DBMS_SQL.DEFINE_COLUMN (cur, 1, fk_nm_column, max_length_in); fdbk := DBMS_SQL.EXECUTE (cur); fdbk := DBMS_SQL.FETCH_ROWS (cur); IF fdbk > 0 THEN DBMS_SQL.COLUMN_VALUE (cur, 1, return_value); END IF; DBMS_SQL.CLOSE_CURSOR (cur); RETURN return_value; END; With this new version of fk_name, I can certainly retrieve the caller's name and the call type description without specifying all the columns, assuming that their columns match my conventions. • Assume that table caller has caller_id and caller_nm columns: :call.name := fk_name (:call.caller_id, 'caller'); • Assume that call_type table has call_type_id and call_type_nm columns: :call.call_type_ds := fk_name (:call.call_type_id, 'call_type'); Of course, conventions do not hold so consistently in the real world. In fact, I have found that database administrators and data analysts will often treat an entity like caller, with its caller ID number and caller name, differently from the way they would treat a caller type, with its type code and description. The columns for the caller type table are more likely to be caller_typ_cd and caller_typ_ds. Fortunately, fk_name will still handle [Appendix A] What's on the Companion Disk? 2.5.2 A Generic Foreign Key Lookup Function 102 this situation as follows: • A full set of defaults works just fine: :call.name := fk_name (:call.caller_id, 'caller'); • Use alternative suffixes for a code table: :call.call_type_ds := fk_name (:call.call_type_id, 'call_type', '_cd', '_ds'); You might scoff and say, "Why bother providing just the suffixes? Might as well go ahead and provide the full column names." But there is a value to this approach: if the data analysts have adopted standards for their naming conventions of tables and key columns, the fk_name interface supports and reinforces these standards, and avoids supplying redundant information. Is that it, then? Have we gone as far as we can go with fk_name? Surely some of you have looked at those rather simple SELECT statements and thought, "Gee, very few of my lookups actually resemble such queries." I agree. Sometimes you will need to check an additional column on the table, such as a "row active?" flag. You might even have several records, all for the same primary key, but active for different periods. So you should also pass a date against which to check. How can you handle these application−specific situations? When in doubt, just add another parameter! Sure. Why not add a parameter containing either a substitute WHERE clause for the SQL statement, or a clause to be appended to the rest of the default WHERE clause? The specification for fk_name would then change to the following: /* Filename on companion disk: fkname3.sf */* FUNCTION fk_name (fk_id_in IN INTEGER, fk_table_in IN VARCHAR2, fk_id_col_in IN VARCHAR2 := '_ID', fk_nm_col_in IN VARCHAR2 := '_NM', max_length_in IN INTEGER := 100, where_clause_in IN VARCHAR2 := NULL) RETURN VARCHAR2; The rule for this WHERE clause would be as follows: if the string starts with the keywords AND or OR, then the text is appended to the default WHERE clause. Otherwise, the argument substitutes completely for the default WHERE clause. Rather than repeat the entire body of fk_name, I offer only the modifications necessary to PARSE, and thus effect this change in the following code: IF UPPER (where_clause_in) LIKE 'AND%' OR UPPER (where_clause_in) LIKE 'OR%' THEN /* Append the additional Boolean expressions to default */ where_clause := ' WHERE ' || fk_id_column || ' = :fk_value ' || where_clause_in; ELSIF where_clause_in IS NOT NULL THEN /* Substitute completely the WHERE clause */ where_clause := ' WHERE ' || where_clause_in; [Appendix A] What's on the Companion Disk? 2.5.2 A Generic Foreign Key Lookup Function 103 ELSE /* Just stick with default */ where_clause := ' WHERE ' || fk_id_column || ' = :fk_value'; END IF; /* Now the call to PARSE uses the pre−processed WHERE clause */ DBMS_SQL.PARSE (cur, 'SELECT ' || fk_nm_column || ' FROM ' || fk_table_in || where_clause, DBMS_SQL.NATIVE); Using this final version of fk_name, I can perform lookups as follows: • Retrieve only the description of the call type if that record is still flagged as active. Notice that I must stick several single quotes together to get the right number of quotes in the evaluated argument passed to fk_name. :call.call_type_ds := fk_name (:call.call_type_id, 'call_type', '_cd', '_ds', 25, 'AND row_active_flag = ''Y'''); • Retrieve the name of the store kept in the record for the current year. Notice that the ID and name arguments in the call to fk_name are NULL. I have to include values here since I want to provide the WHERE clause, but I will pass NULL and thereby use the default values (without having to know what those defaults are!). /* Only the record for this year should be used */ year_number := TO_CHAR (SYSDATE, 'YYYY'); /* || Pass check for year to WHERE clause. */ :store.description := fk_name (:store.store_id, 'store_history', NULL, NULL, 60, 'AND TO_CHAR (eff_date, ''YYYY'') = ''' || year_number || ''''); The fragment of the WHERE clause passed to fk_name can be arbitrarily complex, including subselects, correlated subqueries, and a whole chain of conditions joined by ANDs and ORs. 2.5.3 A Wrapper for DBMS_SQL .DESCRIBE_COLUMNS The DESCRIBE_COLUMNS procedure provides a critical feature for those of us writing generic, flexible code based on dynamic SQL. With earlier versions of DBMS_SQL, there was no way to query runtime memory to find out the internal structure of a cursor. Now you can do this with DESCRIBE_COLUMNS, but it is very cumbersome. As shown in the section Section 2.3.11, "Describing Cursor Columns "," you must declare a PL/SQL table, read the cursor structure into that table, and then traverse the table to get the information you need. A much better approach is to write the code to perform these steps once and then encapsulate all that knowledge into a package. Then you can simply call the programs in the package and not have to worry about all the internal data structures and operations that have to be performed. You will find an example of this "wrapper" around DESCRIBE_COLUMNS on your companion disk. Here is the specification of that package: /* Filename on companion disk: desccols.spp */* CREATE OR REPLACE PACKAGE desccols IS [Appendix A] What's on the Companion Disk? 2.5.3 A Wrapper for DBMS_SQL .DESCRIBE_COLUMNS 104 varchar2_type CONSTANT PLS_INTEGER := 1; number_type CONSTANT PLS_INTEGER := 2; date_type CONSTANT PLS_INTEGER := 12; char_type CONSTANT PLS_INTEGER := 96; long_type CONSTANT PLS_INTEGER := 8; rowid_type CONSTANT PLS_INTEGER := 11; raw_type CONSTANT PLS_INTEGER := 23; mlslabel_type CONSTANT PLS_INTEGER := 106; clob_type CONSTANT PLS_INTEGER := 112; blob_type CONSTANT PLS_INTEGER := 113; bfile_type CONSTANT PLS_INTEGER := 114; PROCEDURE forcur (cur IN INTEGER); PROCEDURE show (fst IN INTEGER := 1, lst IN INTEGER := NULL); FUNCTION numcols RETURN INTEGER; FUNCTION nthcol (num IN INTEGER) RETURN DBMS_SQL.DESC_REC; END desccols; / Before we look at the implementation of this package, let's explore how you might use it. I declare a set of constants that give names to the various column types. This way, you don't have to remember or place in your code the literal values. Now notice that there are no other data structures defined in the specification. Most importantly, there is no declaration of a PL/SQL table based on DBMS_SQL.DESC_T to hold the description information. That table is instead hidden away inside the package body. You call the desccols.forcur procedure to "describe the columns for a cursor," passing it your cursor ID or handle, to load up that table by calling DESCRIBE_COLUMNS. You then can take any of the following actions against that PL/SQL table of column data: • Show the column information by calling desccols.show (the prototype on disk shows only the column name and column type). • Retrieve the total number of columns in the table with a call to desccols.numcols. • Retrieve all the information for a specific column by calling the desccols.nthcol function. The following script defines a cursor, extracts the cursor information with a call to desccols.forcur, and then shows the cursor information: /* Filename on companion disk: desccols.tst */* DECLARE cur INTEGER := DBMS_SQL.OPEN_CURSOR; BEGIN DBMS_SQL.PARSE (cur, 'SELECT ename, sal, hiredate FROM emp', DBMS_SQL.NATIVE); DBMS_SQL.DEFINE_COLUMN (cur, 1, 'a', 60); DBMS_SQL.DEFINE_COLUMN (cur, 1, 1); DBMS_SQL.DEFINE_COLUMN (cur, 1, SYSDATE); desccols.forcur (cur); desccols.show; DBMS_SQL.CLOSE_CURSOR (cur); END; / Here is the output in SQL*Plus: Column 1 ENAME [Appendix A] What's on the Companion Disk? 2.5.3 A Wrapper for DBMS_SQL .DESCRIBE_COLUMNS 105