< previous page page_260 next page > Page 260 SQL> BEGIN 2 ANALYZE TABLE employee COMPUTE STATISTICS 3 END; 4 / ANALYZE TABLE EMPLOYEE COMPUTE STATISTICS; * ERROR at line 2; ORA-06550: line 2, column 11: PLS-00103: Encountered the symbol TABLE when expecting one of the following: := . ( @ % ; With the earliest versions of PL/SQL, this was a hard-and-fast limitation, but now Oracle provides a PL/SQL package for executing dynamic SQL statements that you can also use to execute DDL. A dynamic SQL statement is one you build up programmatically, and often the exact statement isn't known until runtime. The package name is DBMS_SQL, and you can read about it in the Oracle8 Server Application Developer's Guide.* There are five steps to using the DBMS_SQL package to execute a DDL statement: 1. Allocate a cursor for the statement. 2. Put the statement to be executed in a VARCHAR2 variable. 3. Parse the statement. 4. Execute the statement. 5. Deallocate the cursor. Steps 2 through 4 are repeated for as many statements as you need to execute. The first step is to allocate the cursor. Even though you aren't selecting data, all SQL statements are executed using a cursor. The code to do that looks like this: ddl_cursor_id := DBMS_SQL.OPEN_CURSOR; The variable ddl_cursor_id should be declared as an INTEGER. This integer is a cursor ID, used to refer to the actual cursor, which the DBMS_SQL package maintains internally. The call to DBMS_SQL.OPEN_CURSOR should go outside the loop. You can use the same cursor for multiple SQL statements, so you only need to allocate it once. The next step is to create the SQL statement you want executed. The following statement, placed inside the loop, will do that: analyze_command := ANALYZE TABLE ¦¦ my_table.table_name ¦¦ COMPUTE STATISTICS; * Another good source of information on the DBMS_SQL package is Oracle Built-in Packages, by Steve Feuerstein, Charles Dye, and John Beresniewicz (O'Reilly & Associates, 1998). Not only does this book cover DBMS_SQL, it also covers all the other standard packages provided by Oracle. < previous page page_260 next page > < previous page page_261 next page > Page 261 Be sure not to include a semicolon to terminate the statement you are creating. Semicolons are used to tell SQL*Plus that a SQL statement has ended. They should never be passed to Oracle. If you ever run a PL/SQL block and get an invalid character on the line containing your call to the DBMS_SQL.PARSE procedure, look for this problem. Once you have the SQL statement, pass it to the DBMS_SQL package with a call to the PARSE procedure, like this: DBMS_SQL.PARSE (ddl-cursor_id, analyze_command, DBMS_SQL.NATIVE); The PARSE procedure takes three arguments: the cursor ID, the SQL statement to be executed, and a flag telling it whether you want Oracle6 behavior, Oracle7 behavior, or native behavior. Specifying NATIVE results in the normal behavior for whatever version of Oracle you are connected to. The statement is parsed and stored in the cursor you previously allocated. Next, you execute it with a call like the following to the EXECUTE function: ignore_for_ddl := DBMS_SQL.EXECUTE (ddl_cursor_id); The DBMS_SQL.EXECUTE function's return value indicates the number of rows affected by an INSERT, UPDATE, or DELETE statement, and should be ignored when executing anything else. Currently, DDL statements are actually executed when they are parsed, so the call to DBMS_SQL.EXECUTE is superfluous. Oracle doesn't guarantee this behavior, however, so you shouldn't depend on it. Once you are done executing commands, you should dispose of the cursor with a call like the following to the CLOSE_CURSOR procedure: DBMS_SQL.CLOSE_CURSOR(ddl_cursor_id); The final script to analyze all your tables, using PL/SQL and dynamic SQL, now looks like this: SET ECHO OFF DESCRIPTION Analyze and compute statistics for all tables owned by the user. Turn SERVEROUTPUT on so we see output from the PL/SQL block we are going to execute. SET SERVEROUTPUT ON < previous page page_261 next page > < previous page page_262 next page > Page 262 The following PL/SQL block will build up and execute an ANALYZE TABLE command for each table. The DBMS_SQL package is used for this purpose. DECLARE ddl_cursor_id INTEGER; analyze_command VARCHAR2(80); ignore_for_ddl INTEGER; BEGIN Allocate a cursor to use when executing DDL commands. ddl_cursor_id := DBMS_SQL>OPEN_CURSOR; Execute an ANALYZE command for each table. FOR my_table in ( SELECT table_name FROM user_tables) LOOP Create the command to analyze this table. analyze_command := ANALYZE TABLE ¦¦ my_table.table_name ¦¦ COMPUTE STATISTICS; Display the statement to be executed. DBMS_OUTPUT.PUT_LINE(analyze_command); ignore_for_dd1 := DBMS_SQL.EXECUTE (ddl_cursor_id); END LOOP; Deallocate the cursor. DBMS_SQL.CLOSE_CURSOR(ddl_cursor_id); END; / For a script like this, I think the technique of using SQL to write SQL is a better choice. It's certainly easier to write, and just as easy to understand, if not more so. Still, using PL/SQL gives you more control over the process if you need it. Validating and Parsing User Input. Whenever you ask a user for input, you run the risk that it won't make sense. Maybe you are asking for a number, and the user types in some letters. Maybe you are asking for a date, and the user enters a bad value for the month. The SQL*Plus ACCEPT command offers some support for dealing with these situations. You can do even more, if you need to, with some creative use of SQL. < previous page page_262 next page > < previous page page_263 next page > Page 263 Validating Input with ACCEPT Oracle has been steadily improving the ACCEPT command over the last few releases of SQL*Plus. These improvements all center around the issue of validation, and make it much easier to prevent a user from entering bad data in response to a prompt. The ACCEPT command options illustrated in this section apply to SQL*Plus versions 8.0.3 and above. Not all options will be available under previous releases. The ACCEPT command is one that has changed a lot over the years. Check the documentation for the release you are using to see which options are available to you. Throughout most of this book, the ACCEPT commands have all been written pretty much like this: ACCEPT_my_variable PROMPT Enter a value > This is a least-common-denominator version of the ACCEPT command that should work with any release of SQL*Plus. It will simply take whatever string the user types in and assign it to the variable. If you need to go beyond this, ACCEPT allows you to specify a datatype, and will not accept input that doesn't convert to the type you specify. ACCEPT also allows you to specify a format string that the input data must match. You can take good advantage of these options to make your scripts more bulletproof. ACCEPTing numeric values If you are prompting the user for a number, the first and easiest thing to do is to use the NUMBER keyword with the ACCEPT command. Here's an example: ACCEPT my_variable NUMBER PROMPT Enter a number > When NUMBER is specified, SQL*Plus won't accept any input that can't be converted to a number. Instead, it will keep repeating the prompt until the user gets it right; for example: SQL> ACCEPT my_variable NUMBER PROMPT Enter a number> Enter a number >two two is not a valid number Enter a number >2.2.2 2.2.2 is not a valid number Enter a number > < previous page page_263 next page > < previous page page_264 next page > Page 264 SQL*Plus will accept a null input as a valid number, so if the user just presses ENTER, a 0 will be stored in the variable. Spaces, on the other hand, do not constitute numeric input. Using a FORMAT clause for a number will prevent null input from being accepted. You can gain more control over numeric input by taking advantage of the ACCEPT command's FORMAT clause. With it, you can specify a numeric format string, and ACCEPT will only accept input that matches that format. Supposedly, any format string valid for use with the COLUMN command is also valid for use with the ACCEPT command. In practice, though, the 9, 0, and . are the most useful as input formats. Use 9s when you want to limit the user to entering a certain number of digits. SQL> ACCEPT my_variable NUMBER FORMAT 999 PROMPT Enter a number > Enter a number >1234 1234 does not match input format 999 Enter a number >123 SQL> Note, though, that the user is not forced to enter the maximum number of digits allowed by the format string. The user may enter fewer digits, so long as the result is a valid number. SQL> ACCEPT my_variable NUMBER FORMAT 999 PROMPT Enter a number > Enter a number >12 SQL> One advantage of the FORMAT clause is that the user cannot get away without entering something. A valid number, even if it's zero, must be entered. SQL> ACCEPT my_variable NUMBER FORMAT 999 PROMPT Enter a number > Enter a number > does not match input format 999 Enter a number >0 SQL> If you want to allow a decimal value to be entered, then you must include a decimal point in the format string. The user will be limited to the number of decimal places you specify. SQL> ACCEPT my_variable NUMBER FORMAT 999.99 PROMPT Enter a number > Enter a number >19.76 SQL> ACCEPT my_variable NUMBER FORMAT 999.99 PROMPT Enter a number > Enter a number >19.763 19.763 does not match input format 999.99 Enter a number >19.8 SQL> < previous page page_264 next page > < previous page page_265 next page > Page 265 You can use a leading zero in a format string to force the user to enter a specific number of digits. SQL> ACCEPT my_variable NUMBER FORMAT 099 PROMPT Enter a number > Enter a number >1 1 does not match input format 099 Enter a number >12 12 does not match input format 099 Enter a number >123 SQL> However, you cannot use the zero after the decimal point to force the user to enter a specific number of decimal digits. The user may always enter fewer digits after the decimal than you specify in the format string. For example, the following statement accepts an input with a single decimal digit, even though two are specified in the format string: SQL> ACCEPT my_variable NUMBER FORMAT 099.90 PROMPT Enter a number > Enter a number >123.1 SQL> Negative values are always allowed, regardless of whether the format string specifies a sign or not. The following example uses a format string of 999, but still accepts a negative value: SQL> ACCEPT my_variable NUMBER FORMAT 999 PROMPT Enter a number > Enter a number >-123 SQL> SQL*Plus will allow you to use other characters with the FORMAT clause (see the COLUMN command for a complete list), but they may not work as you would expect, and some don't work at all. The S character, for example, indicates a leading sign, but rather than being an optional sign, it is mandatory, so users must enter positive numbers with a leading +. That behavior may make sense based on a strict interpretation of the manual, but it's unlikely to be what you want. Things get even stranger if you use the dollar sign as part of a format string. Take a look at the following interaction with the ACCEPT command using a format of $ 999 SQL> ACCEPT my_variable NUMBER FORMAT $ 999 PROMPT Enter a number > Enter a number >123 123 does not match input format $999 Enter a number >$123 $123 is not a valid number SQL*Plus seems to be in a Catch-22 situation here. SQL*Plus correctly recognizes that 123 does not have a leading dollar sign, and thus does not match the input format. On the other hand, when you enter $123, the value is not recognized as a number. < previous page page_265 next page > < previous page page_266 next page > Page 266 ACCEPTing date values You can deal with date values in much the same way as numeric values. The first thing to do is tell SQL*Plus you want a date. Do this by using the DATE keyword with the ACCEPT command, like this: ACCEPT my_variable DATE PROMPT Give me a date > The date format accepted by SQL*Plus will depend on your NLS_DATE_FORMAT setting. Often this will be DD- MON-YY, but it could be something different depending on how Oracle is configured at your site. When the DATE option is specified, ACCEPT will reject any input that doesn't evaluate to a valid date; for example: SQL> ACCEPT my_variable DATE PROMPT Give me a date > Give me a date >11/15/61 11/15/61 does not match input format DD-MON-YY Give me a date >November 15, 1961 November 15, 1961 does not match input format DD-MON-YY Give me a date >15-Nov-61 SQL> You can see that if you enter an invalid date, ACCEPT will show you the format it's expecting. As with numbers, you can also specify a format string for dates. Any format string you can use with Oracle's TO_DATE function may also be used with the ACCEPT command. Here are a couple of typical examples: SQL> ACCEPT my_variable DATE FORMAT MM/DD/YY PROMPT Give me a date > Give me a date >15-Nov-1961 15-Nov-1961 does not match input format MM/DD/YY Give me a date >11/15/61 SQL> ACCEPT my_variable DATE FORMAT DD-MON-YYYY PROMPT Give me a date > Give me a date >11/15/61 11/15/61 does not match input format DD-MON-YYYY Give me a date>15-Nov-1961 SQL> Remember that the result of an ACCEPT command is still a character string. The user may enter a date, but it is stored as a character string and will need to be converted again when your script next references that substitution variable. ACCEPT is somewhat liberal when it comes to checking the date a user enters against the specified format. ACCEPT will allow either a two- or four-digit year, regardless of what you specify in the format string. ACCEPT is also not too picky about separators, and will allow hyphens even if your format string specifies slashes. The following examples illustrate this behavior: < previous page page_266 next page > < previous page page_267 next page > Page 267 SQL> ACCEPT my_variable DATE FORMAT DD-MON-YYYY PROMPT Give me a date > Give me a date >15-Nov-61 SQL> ACCEPT my_variable DATE FORMAT MM/DD/YY PROMPT Give me a date > Give me a date >11-15-1961 Time of day is not treated with too much respect by ACCEPT either. You may ask for it in your format string, but ACCEPT will take it or leave it. As long as the user enters a date, ACCEPT doesn't care about the rest; for example: SQL> ACCEPT my_variable DATE FORMAT MM/DD/YYYY HH:MI:SS AM PROMPT Give me a date > Give me a date >11/15/1961 SQL> Bear in mind that the user input in response to an ACCEPT command is always placed into a substitution variable, and that substitution variables are always text. This is true with numbers, and is just as true with dates. Look at the following example: SQL> ACCEPT my_variable DATE FORMAT MM/DD/YY PROMPT Give me a date > Give me a date >7/4/98 SQL> DEFINE my_variable DEFINE MY_VARIABLE =7/4/98 (CHAR) The date entered was July 4, 1998. It is stored as the character string 7/4/98, which matches the input format used with the ACCEPT command. To reference the date later in your script, you must use the TO_DATE function to convert it again, and you must use the same format string you used to ACCEPT the date. Failure to do this could result in the date being misinterpreted. The following SELECT, for example, interprets the same date using the European convention of having the day first, followed by the month and year: SQL> select to_date(&&my_variable,dd/mm/yyyy) from dual; old 1: select to_date(&&my_variable, dd/mm/yyyy) from dual new 1: select to_date(7/4/98, dd/mm/yyyy) from dual TO_DATE(' 07-APR-98 SQL> Suddenly, July 4, 1998 has instead become April 7, 1998! I can't imagine ever wanting that type of behavior in a script. To avoid problems, use the same date format consistently every time you reference a substitution variable, whether it's in an ACCEPT command or somewhere else in your script. Validating Input with SQL The validation you get with the ACCEPT command is rather limited. You can do more, if you need to, with the creative use of SQL (or PL/SQL) together with the < previous page page_267 next page > < previous page page_268 next page > Page 268 branching techniques discussed earlier in this chapter. With a little thought and effort, you can: Code more specific validations than you get with ACCEPT Accept more complicated input from the user You could, for example, write a script that asks the user for a date, and that requires all four digits of the year to be entered. You could also write a script that accepts several values in one string and then pulls apart that string to get at each value. An example of this would be allowing the user to specify a table using the standard owner.tablename syntax and defaulting the owner to the currently logged-on user. If you are going to code a complex edit check using SQL*Plus, you need to be able to do two fundamental things: 1. Decide whether or not the user's input is valid 2. Take different actions depending on the result of that decision The first thing you need to decide is which branching technique you are going to use, because that tends to drive how you structure the query you use for validation. Usually, if I'm in this deep, I will branch using a multilevel file structure. To facilitate this, I'll write the validation query to return all or part of the filename to run next. If the input is no good, the next script file will simply display an error message and quit. The second thing to do is write the SQL query to perform the validation. Implementing the validation requires these four steps: 1. ACCEPT input from the user. 2. Issue a COLUMN command to capture the value returned from the validation query. 3. Execute the validation query. 4. Execute the script file returned by the query, which you captured with the column command. The following short script example illustrates how SQL can be used to validate input by to determining whether or not a date was entered with a four-digit year: Get a date from the user ACCEPT start_date DATE FORMAT DD-MON-YYYY PROMPT Start Date > Get the next file to run, based on whether the date has a four-digit or a two-digit year. COLUMN next_script_file NEW_VALUE next_script_file SELECT DECODE (LENGTH(SUBSTR(&&start_date INSTR(TRANSLATE(&&start_date,/,-),-,-1)+1, LENGTH(&&start_date)- INSTR(TRANSLATE(&&start_date,/,-),-,-1))), < previous page page_268 next page > < previous page page_269 next page > Page 269 4, four_digit_year.sql &&start_date, 2, two_digit_year.sql &&start_date, bad_date.sql) next_script_file FROM dual; Execute the appropriate script @&&next_script_file Admittedly, the DECODE expression is a bit complex, but it serves to illustrate how much you can accomplish with Oracle's built-in functions. Parsing Input with SQL In addition to simply validating input, you can also use SQL and PL/SQL to parse it. Imagine for a moment that you are writing a script to display information about the physical implementation of a table. The script has to know which table you want to look at, and one way to accomplish that is to pass the table name as an argument, like this: @show_physical project_hours That's fine if you always want to run the script on tables you own. But what if you are the DBA and want to examine tables owned by other users? As with the DESCRIBE command, you may want to allow for an optional owner name. Then you could also run the script like this: @show_physical jeff.project_hours The first problem you'll encounter in doing this is that the argument jeff.project_ hours is one string, not two. The second problem is that you can't depend on the owner always to be specified, and when it's not specified, you want it to default to the currently logged-in user. The solution to these problems is to use SQL to parse the input. One way to do that is simply to extend the WHERE clauses of whatever queries are run by your script. Here's a query to return the amount of space used by a particular table: SELECT SUM(bytes) FROM dba_extents WHERE segment_name = DECODE(INSTR(&&1,.), 0,UPPER(&&1), UPPER(SUBSTR(&&1,INSTR(&&1,.)+1))) AND owner = DECODE(INSTR(&&1,.), 0,USER, UPPER(SUBSTR(&&1,1,INSTR(&&1,.)-1))); This solution works, but it can be cumbersome and error-prone because the parsing logic has to be replicated in each query your script executes. A better solution is to write some SQL at the beginning of your script specifically to parse the input. That way you end up with two distinct substitution variables, one for the owner and one for the table name, to use in the rest of your script. There are two steps < previous page page_269 next page > . internally. The call to DBMS _SQL. OPEN_CURSOR should go outside the loop. You can use the same cursor for multiple SQL statements, so you only need to allocate it once. The next step is to create the SQL. character on the line containing your call to the DBMS _SQL. PARSE procedure, look for this problem. Once you have the SQL statement, pass it to the DBMS _SQL package with a call to the PARSE procedure,. semicolon to terminate the statement you are creating. Semicolons are used to tell SQL* Plus that a SQL statement has ended. They should never be passed to Oracle. If you ever run a PL /SQL block and