1 Column 2 SAL 2 Column 3 HIREDATE 12 In this next example, I load up the column information, use the nthcol function to get the information about just one column, deposit it in a locally declared record, and then check the column type. DECLARE cur integer := dbms_sql.open_cursor; rec DBMS_SQL.DESC_REC; 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); rec := desccols.nthcol (1); IF rec.col_type = desccols.varchar2_type THEN DBMS_OUTPUT.PUT_LINE ('Process as string!'); END IF; DBMS_SQL.CLOSE_CURSOR (cur); END; / And I get this output when executed: Process as string! Notice how I have shifted from dealing with the low−level details of the DESCRIBE_COLUMNS built−in to manipulating all that data through a clear, easy−to−use API. It is not as though the code you need to write (and you will find in the body of the next package) is all that complicated. But why bother with this code again and again when you can write it once and then just pass in the pointer to the cursor and let the package do all the work? The implementation of the desccols package is straightforward. /* Filename on companion disk: desccols.spp */* CREATE OR REPLACE PACKAGE BODY desccols IS /* Here is the PL/SQL table holding the column information. */ desctab DBMS_SQL.DESC_TAB; desccnt PLS_INTEGER; firstrow PLS_INTEGER; lastrow PLS_INTEGER; PROCEDURE forcur (cur IN INTEGER) IS BEGIN /* Clear out the PL/SQL table */ desctab.DELETE; /* Fill up the PL/SQL table */ DBMS_SQL.DESCRIBE_COLUMNS (cur, desccnt, desctab); /* Get the first and last row numbers to avoid future lookups */ firstrow := desctab.FIRST; lastrow := desctab.LAST; END; [Appendix A] What's on the Companion Disk? 2.5.3 A Wrapper for DBMS_SQL .DESCRIBE_COLUMNS 106 PROCEDURE show (fst IN INTEGER := 1, lst IN INTEGER := NULL) IS BEGIN IF desccnt > 0 THEN /* Show the specified rows. */ FOR colind IN GREATEST (fst, firstrow) LEAST (NVL (lst, lastrow), lastrow) LOOP /* Add additional lines of output as you desire */ DBMS_OUTPUT.PUT_LINE ('Column ' || TO_CHAR (colind)); DBMS_OUTPUT.PUT_LINE (desctab(colind).col_name); DBMS_OUTPUT.PUT_LINE (desctab(colind).col_type); END LOOP; END IF; END; FUNCTION numcols RETURN INTEGER IS BEGIN RETURN desccnt; END; FUNCTION nthcol (num IN INTEGER) RETURN DBMS_SQL.DESC_REC IS retval DBMS_SQL.DESC_REC; BEGIN /* If a valid row number, retrieve that entire record. */ IF num BETWEEN firstrow AND lastrow THEN retval := desctab(num); END IF; RETURN retval; END; END; / 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL This section examines the kind of code you need to write to perform dynamic SQL Method 4. Method 4, introduced early in this chapter, supports queries that have a variable (defined only at runtime) number of items in the SELECT list and/or a variable number of host variables. Here is an example of Method 4 dynamic SQL: 'SELECT ' || variable_select_list || ' FROM ' || table_name || ' WHERE sal > :minsal ' AND ' || second_clause || order_by_clause Notice that with this SQL statement, I do not know how many columns or expressions are returned by the query. The names of individual columns are "hidden" in the variable select list. I also do not know the full contents of the WHERE clause; the minsal bind variable is obvious, but what other bind variable references might I find in the second_clause string? As a result of this uncertainty, Method 4 dynamic SQL is the most complicated kind of dynamic query to handle with DBMS_SQL. What's so hard about that? Well, if I am going to use the DBMS_SQL package to execute and fetch from such a query, I need to write and compile a PL/SQL program. Specifically, to parse the SQL statement, I need to define the columns in the cursor with calls to DEFINE_COLUMN −− yet I do not know the list of columns at the time I am writing my code. To execute the query, I must associate values to all of my bind variables [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 107 (identifiers with a ":" in front of them) by calling BIND_VARIABLE −− yet I do not know the names of those bind variables at the time I write my code. Finally, to retrieve data from the result set of the query I also need to call COLUMN_VALUE for each column. But, again, I do not know the names or datatypes of those columns up front. Sounds challenging, doesn't it? In fact, working with these incredibly dynamic SQL strings requires some interesting string parsing and some even more creative thinking. When would you run into Method 4? It arises when you build a frontend to support ad−hoc query generation by users, or when you want to build a generic report program, which constructs the report format and contents dynamically at runtime. I also encountered it recently when I decided to build a PL/SQL procedure to display the contents of a table −− any table, as specified by the user at runtime. This section explores what it took for me to implement this "in table" procedure. 2.5.4.1 The "in table" procedural interface Before I dive into the PL/SQL required to create my procedure, I should explore my options. After all, it is certainly very easy for me to build a script in SQL*Plus to display the contents of any table. SELECT * FROM &1 I could even spice it up with a variable select list and WHERE clause as follows: SELECT &1 FROM &2 WHERE &3 ORDER BY &4 In fact, SQL*Plus is a very flexible, powerful front−end tool for SQL scripts. Yet no matter how fancy I get with substitution parameters in SQL*Plus, this is not code I can run from within PL/SQL. Furthermore, PL/SQL gives me more procedural control over how to specify the data I want to see and how to display the data. Finally, if I use PL/SQL, then I get to play with DBMS_SQL! On the downside, however, from within PL/SQL I must rely on DBMS_OUTPUT (described in Chapter 7, Defining an Application Profile) to display my table contents, so I must reckon with the buffer limitations of that built−in package (a maximum of 1,000,000 bytes of data −− you will clearly not use my procedure to display very large quantities of data). So I will use PL/SQL and DBMS_SQL. But before building any code, I need to come up with a specification. How will the procedure be called? What information do I need from my user (a developer, in this case)? What should a user have to type to retrieve the desired output? I want my procedure (which I call "intab" for "in table") to accept the inputs in the following table. Parameter Description Name of the table Required. Obviously, a key input to this program. Maximum length of string displayed Optional. Sets an upper limit on the size of string columns. I do not even attempt to do the kind of string wrapping performed in SQL*Plus. Instead, SUBSTR simply truncates the values. WHERE clause Optional. Allows you to restrict the rows retrieved by the query. If not specified, all rows are retrieved. You can also use this parameter to pass in ORDER BY and HAVING clauses, since they follow immediately after the where clause. Format for date columns Optional. Allows you to set the standard format for date displays. The default includes date and time information. When using SQL*Plus, I find it very irritating to constantly have to use TO_CHAR to see the time portion of my date fields. Given these inputs, the specification for my procedure becomes the following: [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 108 PROCEDURE intab (table_in IN VARCHAR2, string_length_in IN INTEGER := 20, where_in IN VARCHAR2 := NULL, date_format_in IN VARCHAR2 := 'MM/DD/YY HHMISS') Here are some examples of calls to intab: execute intab ('emp'); execute intab ('emp', 20, 'deptno = ' || v_deptno || ' order by sal'); These two calls to intab produce the following output: execute intab ('emp'); −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− Contents of emp −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− EMPNO ENAMEJOBMGR HIREDATESALCOMMDEPTNO −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 7839 KINGPRESIDENT11/17/81 120000500010 7698 BLAKE MANAGER7839 05/01/81 120000285030 7782 CLARKMANAGER7839 06/09/81 120000245010 7566 JONESMANAGER7839 04/02/81 120000297520 7654 MARTINSALESMAN7698 09/28/81 1200001250 140030 7499 ALLENSALESMAN7698 02/20/81 1200001600 3000 7844 TURNERSALESMAN7698 09/08/81 1200001500 0 30 7900 JAMESCLERK7698 12/03/81 12000095030 7521 WARDSALESMAN7698 02/22/81 1200001250 500 30 7902 FORDANALYST7566 12/03/81 120000300020 7369 SMITHCLERK7902 12/17/80 12000080020 7788 SCOTTANALYST7566 12/09/82 120000300020 7876 ADAMSCLERK7788 01/12/83 120000110020 7934 MILLERCLERK7782 01/23/82 120000130010 execute intab ('emp', 20, 'deptno = 10 order by sal'); −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− Contents of emp −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− EMPNO ENAMEJOB MGR HIREDATESALCOMMDEPTNO −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 7934 MILLERCLERK7782 01/23/82 120000130010 7782 CLARKMANAGER7839 06/09/81 120000245010 7839 KINGPRESIDENT11/17/81 120000500010 Notice that the user does not have to provide any information about the structure of the table. My program will get that information itself −− precisely the aspect of intab that makes it a Method 4 dynamic SQL example. While this version of intab will certainly be useful, I am the first to recognize that there are many other possible enhancements to intab, including: • Supplying a list of only those columns you wish to display. This will bypass the full list of columns for the table (which, as you will see, is extracted from the data dictionary). • Supplying a list of those columns you wish to exclude from display. If your table has 50 columns, and you don't want to display three of those columns, it's a lot easier to list the three you don't want than the 47 you do want. • [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 109 Supplying an ORDER BY clause for the output. You could do this through the WHERE clause, but it is certainly more structured to provide a separate input. • Providing a format for numeric data in addition to the date format. So, yes, there is always more one can do, but this one (yours truly) would like to leave some interesting work for his readers. To encourage you to take my intab and "run with it," I will, in this section, step you through the usage of DBMS_SQL required to implement the intab procedure. (The full program is contained on the companion disk.) 2.5.4.2 Steps for intab construction In order to display the contents of a table, follow these steps: 1. Construct and parse the SELECT statement (using OPEN_CURSOR and PARSE). 2. Bind all local variables with their placeholders in the query (using BIND_VARIABLE). 3. Define each column in the cursor for this query (using DEFINE_COLUMN). 4. Execute and fetch rows from the database (using EXECUTE and FETCH_ROWS). 5. Retrieve values from the fetched row and place them into a string for display purposes (using COLUMN_VALUE). Then display that string with a call to the PUT_LINE procedure of the DBMS_OUTPUT package. NOTE: My intab implementation does not currently support bind variables. I assume, in other words, that the where_clause_in argument does not contain any bind variables. As a result, I will not be exploring in detail the code required for step 2. 2.5.4.3 Constructing the SELECT In order to extract the data from the table, I have to construct the SELECT statement. The structure of the query is determined by the various inputs to the procedure (table name, WHERE clause, etc.) and the contents of the data dictionary. Remember that the user does not have to provide a list of columns. Instead, I must identify and extract the list of columns for that table from a data dictionary view. I have decided to use all_tab_columns in intab so the user can view the contents not only of tables he, or she, owns (which are accessible in user_tab_columns), but also any table for which he, or she, has SELECT access. Here is the cursor I use to fetch information about the table's columns: CURSOR col_cur (owner_in IN VARCHAR2, table_in IN VARCHAR2) IS SELECT column_name, data_type, data_length, data_precision, data_scale FROM all_tab_columns WHERE owner = owner_in [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 110 . string! Notice how I have shifted from dealing with the low−level details of the DESCRIBE_COLUMNS built−in to manipulating all that data through a clear, easy−to−use API. It is not as though the. Application Profile) to display my table contents, so I must reckon with the buffer limitations of that built−in package (a maximum of 1,000,000 bytes of data −− you will clearly not use my procedure