AND table_name = table_in; Using Dot Notation Notice that my cursor takes two parameters, owner_in and table_in, but that my program itself only accepts a single table name parameter. Rather than have the user pass this information in as two separate parameters, she can use standard dot notation, as in SCOTT.EMP, and the intab procedure will parse them as follows: dot_loc := INSTR (table_nm, '.'); IF dot_loc > 0 THEN owner_nm := SUBSTR (table_nm, 1, dot_loc−1); table_nm := SUBSTR (table_nm, dot_loc+1); END IF; You should always try to make the interface as seamless and intelligent as possible for your users. You should also always try to make use of existing programs to implement your own. In this case (as pointed out to me by Dan Clamage, a technical reviewer), you can also use DBMS_UTILITY.NAME_TOKENIZE to parse this object name. With this column cursor, I extract the name, datatype, and length information for each column in the table. How should I store all of this information in my PL/SQL program? To answer this question, I need to think about how that data will be used. It turns out that I will use it in many ways, for example: • I will use the column names to build the select list for the query. • To display the output of a table in a readable fashion, I need to provide a column header that shows the names of the columns over their data. These column names must be spaced out across the line of data in, well, columnar format. So I need the column name and the length of the data for that column. • To fetch data into a dynamic cursor, I need to establish the columns of the cursor with calls to DEFINE_COLUMN. For this, I need the column datatype and length. • To extract the data from the fetched row with COLUMN_VALUE, I need to know the datatypes of each column, as well as the number of columns. • To display the data, I must construct a string containing all the data (using TO_CHAR to convert numbers and dates). Again, I must pad out the data to fit under the column names, just as I did with the header line. Therefore, I need to work with the column information several times throughout my program, yet I do not want to read repeatedly from the data dictionary. As a result, when I query them out of the all_tab_columns view, I will store the column data in three PL/SQL tables. Table Description colname The name of each column. coltype The datatype of each column, a string describing the datatype. collen The number of characters required to display the column data. So if the third column of the emp table is SAL, then colname(3) = `SAL', coltype(3) = `NUMBER', and [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 111 collen(3) = 7, and so forth. The name and datatype information is stored directly from the data dictionary. When I work with the DBMS_SQL built−ins, however, they do not use the strings describing the datatypes (such as "CHAR" and "DATE"). Instead, DEFINE_COLUMN and COLUMN_VALUE rely on PL/SQL variables to infer the correct datatypes. So I use three local functions, is_string, is_date, and is_number, to help me translate a datatype into the correct variable usage. The is_string function, for example, validates that both CHAR and VARCHAR2 are string datatypes: FUNCTION is_string (row_in IN INTEGER) RETURN BOOLEAN IS BEGIN RETURN (coltype(row_in) IN ('CHAR', 'VARCHAR2')); END; Figuring out the appropriate number of characters required to fit the column's data (the contents of the collen PL/SQL table) is a bit more complicated. 2.5.4.4 Computing column length I need to take several different aspects of the column into account: 1. The length of the column name (you don't want the column length to be smaller than the header). 2. The maximum length of the data. If it's a string column, that information is contained in the data_length column of all_tab_columns. 3. If it's a number column, that information is contained in the data_precision column of the view (unless the datatype is unconstrained, in which case that information is found in the data_length column). 4. If it's a date column, the number of characters will be determined by the length of the date format mask. As you can see, the type of data partially determines the type of calculation I perform for the length. Here's the formula for computing a string column's length: GREATEST (LEAST (col_rec.data_length, string_length_in), LENGTH (col_rec.column_name)) The formula for a numeric column length is as follows: GREATEST (NVL (col_rec.data_precision, col_rec.data_length), LENGTH (col_rec.column_name)) Finally, here's the formula for a date column length: GREATEST (LENGTH (date_format_in), LENGTH (col_rec.column_name)) I use these formulas inside a cursor FOR loop that sweeps through all the columns for a table (as defined in all_tab_columns). This loop (shown following) fills my PL/SQL tables: [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 112 FOR col_rec IN col_cur (owner_nm, table_nm) LOOP /* Construct select list for query. */ col_list := col_list || ', ' || col_rec.column_name; /* Save datatype and length for calls to DEFINE_COLUMN. */ col_count := col_count + 1; colname (col_count) := col_rec.column_name; coltype (col_count) := col_rec.data_type; /* Compute the column length with the above formulas in a local module. */ collen (col_count) := column_lengths; /* Store length and keep running total. */ line_length := line_length + v_length + 1; /* Construct column header line. */ col_header := col_header || ' ' || RPAD (col_rec.column_name, v_length); END LOOP; When this loop completes, I have constructed the select list, populated my PL/SQL tables with the column information I need for calls to DEFINE_COLUMN and COLUMN_VALUE, and also created the column header line. Now that was a busy loop! Next step? Construct the WHERE clause. In the following code, I check to see if the "WHERE clause" might actually just be a GROUP BY or ORDER BY clause. In those cases, I'll skip the WHERE part and attach this other information. IF where_clause IS NOT NULL THEN IF (where_clause NOT LIKE 'GROUP BY%' AND where_clause NOT LIKE 'ORDER BY%') THEN where_clause := 'WHERE ' || LTRIM (where_clause, 'WHERE'); END IF; END IF; I have now finished construction of the SELECT statement. Time to parse it, and then construct the various columns in the dynamic cursor object. 2.5.4.5 Defining the cursor structure The parse phase is straightforward enough. I simply cobble together the SQL statement from its processed and refined components. DBMS_SQL.PARSE (cur, 'SELECT ' || col_list || ' FROM ' || table_in || ' ' || where_clause, DBMS_SQL.NATIVE); Of course, I want to go far beyond parsing. I want to execute this cursor. Before I do that, however, I must give some structure to the cursor. Remember: when you open a cursor, you have merely retrieved a handle to a chunk of memory. When you parse the SQL statement, you have associated a SQL statement with that memory. But as a next step, you must define the columns in the cursor so that it can actually store fetched data. With Method 4 dynamic SQL, this association process is complicated. I cannot "hard−code" the number or type of calls to DEFINE_COLUMN in my program; I do not have all the information until runtime. [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 113 Fortunately, in the case of intab, I have kept track of each column to be retrieved. Now all I need to do is issue a call to DEFINE_COLUMN for each row defined in my PL/SQL table colname. Before we go through the actual code, here are some reminders about DEFINE_COLUMN. The header for this built−in procedure is as follows: PROCEDURE DBMS_SQL.DEFINE_COLUMN (cursor_handle IN INTEGER, position IN INTEGER, datatype_in IN DATE|NUMBER|VARCHAR2) There are three things to keep in mind with this built−in: 1. The second argument is a number; DEFINE_COLUMN does not work with column names −− only with the sequential position of the column in the list. 2. The third argument establishes the datatype of the cursor's column. It does this by accepting an expression of the appropriate type. You do not, in other words, pass a string like "VARCHAR2" to DEFINE_COLUMN. Instead, you would pass a variable defined as VARCHAR2. 3. When you are defining a character−type column, you must also specify the maximum length of values retrieved into the cursor. In the context of intab, the row in the PL/SQL table is the Nth position in the column list. The datatype is stored in the coltype PL/SQL table, but must be converted into a call to DEFINE_COLUMN using the appropriate local variable. These complexities are handled in the following FOR loop: FOR col_ind IN 1 col_count LOOP IF is_string (col_ind) THEN DBMS_SQL.DEFINE_COLUMN (cur, col_ind, string_value, collen (col_ind)); ELSIF is_number (col_ind) THEN DBMS_SQL.DEFINE_COLUMN (cur, col_ind, number_value); ELSIF is_date (col_ind) THEN DBMS_SQL.DEFINE_COLUMN (cur, col_ind, date_value); END IF; END LOOP; When this loop is completed, I will have called DEFINE_COLUMN for each column defined in the PL/SQL tables. (In my version this is all columns for a table. In your enhanced version, it might be just a subset of all these columns.) I can then execute the cursor and start fetching rows. The execution phase is no different for Method 4 than it is for any of the other simpler methods, fdbk := DBMS_SQL.EXECUTE (cur); where fdbk is the feedback returned by the call to EXECUTE. Now for the finale: retrieval of data and formatting for display. [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 114 2.5.4.6 Retrieving and displaying data I use a cursor FOR loop to retrieve each row of data identified by my dynamic cursor. If I am on the first row, I will display a header (this way, I avoid displaying the header for a query which retrieves no data). For each row retrieved, I build the line and then display it: LOOP fdbk := DBMS_SQL.FETCH_ROWS (cur); EXIT WHEN fdbk = 0; IF DBMS_SQL.LAST_ROW_COUNT = 1 THEN /* We will display the header information here */ END IF; /* Construct the line of text from column information here */ DBMS_OUTPUT.PUT_LINE (col_line); END LOOP; The line−building program is actually a numeric FOR loop in which I issue my calls to COLUMN_VALUE. I call this built−in for each column in the table (information that is stored in −− you guessed it −− my PL/SQL tables). As you can see below, I use my is_* functions to determine the datatype of the column and therefore the appropriate variable to receive the value. Once I have converted my value to a string (necessary for dates and numbers), I pad it on the right with the appropriate number of blanks (stored in the collen PL/SQL table) so that it lines up with the column headers. col_line := NULL; FOR col_ind IN 1 col_count LOOP IF is_string (col_ind) THEN DBMS_SQL.COLUMN_VALUE (cur, col_ind, string_value); ELSIF is_number (col_ind) THEN DBMS_SQL.COLUMN_VALUE (cur, col_ind, number_value); string_value := TO_CHAR (number_value); ELSIF is_date (col_ind) THEN DBMS_SQL.COLUMN_VALUE (cur, col_ind, date_value); string_value := TO_CHAR (date_value, date_format_in); END IF; /* Space out the value on the line under the column headers. */ col_line := col_line || ' ' || RPAD (NVL (string_value, ' '), collen (col_ind)); END LOOP; There you have it. A very generic procedure for displaying the contents of a database table from within a PL/SQL program. It all fell pretty smoothly into place once I got the idea of storing my column structures in a set of PL/SQL tables. Drawn on repeatedly, those in−memory tables made it easy to implement Method 4 dynamic SQL −− another example of how taking full advantage of everything PL/SQL has to offer strengthens your ability to implement quickly and cleanly. [Appendix A] What's on the Companion Disk? 2.5.4 Displaying Table Contents with Method 4 Dynamic SQL 115 . datatype information is stored directly from the data dictionary. When I work with the DBMS_SQL built−ins, however, they do not use the strings describing the datatypes (such as "CHAR". we go through the actual code, here are some reminders about DEFINE_COLUMN. The header for this built−in procedure is as follows: PROCEDURE DBMS_SQL.DEFINE_COLUMN (cursor_handle IN INTEGER, . INTEGER, datatype_in IN DATE|NUMBER|VARCHAR2) There are three things to keep in mind with this built−in: 1. The second argument is a number; DEFINE_COLUMN does not work with column names −−