Professional Documents
Culture Documents
Extending PL/SQL
File I/O Capabilities
with Java
Steven Feuerstein
For many years, PL/SQL developers complained—with much justification—about
the UTL_FILE package, which was notable more for what it was missing than what it
contained. A number of deficiencies were corrected in Oracle9i Release 2, but there June 2004
are still problems. Steven Feuerstein provides the solutions, showing you how to get Volume 11, Number 6
answers from within PL/SQL.
1 Extending PL/SQL File I/O
O
RACLE9i Release 2 corrects many of the longstanding UTL_FILE Capabilities with Java
Steven Feuerstein
package deficiencies—now we can delete files from within native
PL/SQL code, as well as copy files, rename files, and get some 5 Pipelined Table Functions: An
attribute information. There’s still an awful lot of information “out there,” Example with Pipes and Views
however, of which UTL_FILE is painfully ignorant, and an awful lot of Gary Menchen
operations that it simply cannot perform. Consider the following list: 9 Oracle10g: Regular Expressions
• Can I read from a file? Write to a file? Is the named item a file or Parin Jhaveri
a directory?
• What is the parent directory of a file? 15 Tip: Current_schema
and user_tables
• What are the names of all of the files in a directory that match a Daniel Clamage
specified filter?
• Can I make a directory? 16 June 2004 Downloads
• What is the character that separates the directory from the file name?
This article explains how to get answers to all of these questions from
within PL/SQL. Well, to be perfectly honest, while you will indeed be able to Indicates accompanying files are available online at
call PL/SQL programs to get those answers, you’ll in fact be using Java classes www.pinnaclepublishing.com.
and PL/SQL wrappers on top of those classes to get the named File.separator that returns the value I desire.
job done. To take advantage of that method, I’ll need to create
By the way, if you’re not yet taking advantage of my own class, which I’ll call JFile. This class will, broadly,
Java in your PL/SQL development, and if you’re not contain a method (procedure or function) for each of
comfortable reading Java code and writing at least simple the File methods I want to use. My own method will
classes, then I urge you to buy a good Java book (such as instantiate an object of class File, and then apply the
Thinking in Java, by Bruce Eckels) and put aside a half day desired method to that object. These steps are shown
of your time for Java. here in my first, simplest JFile implementation:
The panic that some of us felt when Oracle first
announced support for Java in the database (“Oh my CREATE OR REPLACE AND RESOLVE
JAVA SOURCE NAMED "JFile" AS
gosh, is PL/SQL going away?”) has largely subsided. import java.io.File;
public class JFile {
Obviously, Oracle is very committed to PL/SQL. There
are, however, situations when PL/SQL quite simply won’t public static String separator (String fileName) {
File myFile = new File (fileName);
do the trick, but Java will. For such scenarios, you should return myFile.separator;
be ready to throw together a simple Java class, drop a }
}
PL/SQL wrapper on top of it, and solve your problems. /
That is, after all, the whole point to our work
as developers: Solve a problem, fulfill a business I can run this DDL statement directly in SQL*Plus to
requirement. define the class within the Oracle database. Once I’ve
You’ll find two files in the Download for this article: done that, I can create a function in PL/SQL that invokes
• JFile.java—A Java class that draws together various the separator method:
pieces of information about operating system files
and offers it through an API accessible from PL/SQL. CREATE OR REPLACE PACKAGE xfile
IS
• xfile.pkg—The PL/SQL package that wraps the JFile FUNCTION separator (FILE IN VARCHAR2)
RETURN VARCHAR2;
class. Stands for “eXtra stuff for FILEs.” END;
/
Let’s take a look at the different elements within CREATE OR REPLACE PACKAGE BODY xfile
the JFile/xfile partnership, from the simplest to the IS
FUNCTION separator (FILE IN VARCHAR2)
more complicated. RETURN VARCHAR2
AS
LANGUAGE JAVA
What is that operating system delimiter? NAME 'JFile.separator (java.lang.String)
It’s always struck me as more than a little bit odd that return java.lang.String';
END;
UTL_FILE isn’t able to parse a file specification that /
combines the directory and file name, as in:
Notice that my function implementation doesn’t
d:\temp\myfile.txt. contain any PL/SQL code; instead, it specifies the
language as Java and then provides a string that specifies
But sadly it’s the case that UTL_FILE doesn’t know
how to call the Java method. The number and datatypes
the character used by the operating system to separate
of my function parameters must map to the parameter
the two. And that’s why when we open a file, we have
list of the Java method.
to provide the location and name separately. (I’ve been
I won’t review the various mappings in this article;
told that Oracle bought the underlying operating-system
I suggest you check the Oracle documentation for the
independent file I/O technology that first defined
details. Within this article, however, I will use the
UTL_FILE from a third party; I hope it was the only
mappings from Table 1.
option available at the time. That would be a good
excuse, anyway.)
Well, suppose that I really, really need to be able to Table 1. Datatype mappings.
obtain that separator in my PL/SQL code. What’s a
developer to do? PL/SQL datatype Java datatype
VARCHAR2 java.lang.string
Just like Oracle has built-in packages, Java has INTEGER float
“foundation classes”—lots of them. So if I’m going to BOOLEAN int
use Java to solve my problem, my first step is to find a
foundation class that already gives me what I need. I bet you’re surprised by that last one; for some
There are many different classes that perform file I/O. reason, the BOOLEAN datatype of PL/SQL doesn’t map
A good place to start is the File class. And, in fact, when I directly to the Java boolean type. This complicates some
check out the class definition, I find that there’s a method of our work, as you’ll see.
public static String dirContents (String dir) { Table 2. Some of the programs found in the xfile package.
File myDir = new File (dir);
String[] filesList = myDir.list();
Program Description
String contents = new String();
getDirContents Allows the user to pass a filter (such as “*.tmp”
for (int i = 0; i < filesList.length; i++) (the filter version) or “%.tmp”) and retrieve only files that match
contents = contents + listDelimiter + filesList[i]; the filter. The underscore character ( _ ) will be
return contents; treated as a single-character wildcard, following
}
the SQL standard.
This method instantiates a File object called myDir showDirContents Displays all of the files found in the specified
and then assigns the myDir.list() to a String array called directory, matching your filter.
filesList. I then use a Java “for loop” to concatenate
Chgext Changes the extension of the specified files.
each of the files into a single String, separated by the
listDelimiter, and return that String. Over on the You’ll also find in the xfile package all of the entry
PL/SQL side of the world, I’ll create a wrapper that points of the UTL_FILE package, such as FOPEN and
calls this method: PUT_LINE. I added those so that you can avoid the use
FUNCTION dirContents (dir IN VARCHAR2)
of UTL_FILE for anything but declarations of file handles
RETURN VARCHAR2 as UTL_FILE.FILE_TYPE.
AS LANGUAGE JAVA
NAME 'JFile.dirContents (java.lang.String)
return java.lang.String'; Make sure you have at least two hammers at
your disposal
But what am I to do with this string? Let’s build
You know the saying: If the only tool you have is a
some additional code elements on top of my wrapper
hammer, then everything looks like a nail. PL/SQL is
functions to make the information more developer-
an absolutely wonderful language (I’d be one miserable
friendly. First, I’d like to let users of xfile manipulate files
fellow if I didn’t believe that, given how much of my
either as a string list or as a nested table (much more
life is devoted to PL/SQL), but we must be honest with
structured data; easier to scan and manipulate). So I’ll
ourselves: If it’s the only tool we’ve got, we’re going
define a nested table type as follows:
to end up with a lot of bent nails and sore thumbs.
CREATE TYPE file_list_t IS TABLE OF VARCHAR2(2000); PL/SQL simply doesn’t offer natively the full range of
/ functionality needed for general-purpose programming.
In such situations, with file I/O being a pretty
And then I define a procedure to return the files good example, it’s good to have another hammer in the
in a directory in a nested table of this type. Note the toolbox, perhaps with a different heft and shape. In the
call to the dirContents wrapper function and also the case of the Oracle RDBMS, Java fits the bill. Consequently,
reference to g_listdelim, which contains the delimiter all PL/SQL developers should be conversant with Java
passed back from JFile (just like the numeric values for basics, and capable of writing the simple classes necessary
TRUE and FALSE): to solve their problems. ▲
PROCEDURE getDirContents (
dir IN VARCHAR2, 406FEUER.ZIP at www.pinnaclepublishing.com
files IN OUT file_list_t)
IS
file_list VARCHAR2(32767); Steven Feuerstein is considered one of the world’s leading experts on the
next_delim PLS_INTEGER; Oracle PL/SQL language, having written nine books on PL/SQL, including
start_pos PLS_INTEGER := 1;
BEGIN Oracle PL/SQL Programming and Oracle PL/SQL Best Practices (all from
files.DELETE; O’Reilly Media). Steven has been developing software since 1980 and
file_list := dirContents (dir);
LOOP serves as a senior technology advisor to Quest Software. His current
next_delim := projects include Swyg (www.Swyg.com) and the Refuser Solidarity
INSTR (file_list, g_listdelim, start_pos);
EXIT WHEN next_delim = 0;
Network (www.refusersolidarity.net), which supports the Israeli military
files.EXTEND; refuser movement. steven@stevenfeuerstein.com.
O
RACLE’S pipelined table functions allow any form
COLUMN_VALUE
of consistently structured data to be returned as ------------------------------------------------
a collection type. Since collection types can be I am nested table row 5
I am nested table row 15
cast to a TABLE, this provides a very convenient method I am nested table row 25
of retrieving data that isn’t normally accessible via any
SQL>
SQL query.
Let’s look at a rudimentary pipelined function (see Now let’s take a look at a more useful example of
Listing 1). We’ll use, as a collection type, a TABLE of pipelined functions.
VARCHAR2, but we could just as well have used a
VARRAY of VARCHAR2. Pipes and pipelined functions
Oracle pipes and pipelined table functions share a bit
Listing 1. A simple pipelined function. more than just a common syllable. Both are described in
Oracle documentation as producing data that’s meant to
CREATE OR REPLACE be consumed. For an Oracle pipe, that means that when
TYPE TEXTTBL
AS TABLE OF VARCHAR2(2048) data is read from a pipe it’s gone forever; for pipelined
/ table functions, it means that the results are meant to be
CREATE OR REPLACE
function SIMPLETEXTTBL(pRows in integer) consumed in a similar way. Oracle doesn’t buffer the
return TextTbl Pipelined
-- returns a nested table
contents of a pipelined function unless it’s marked as
is Deterministic. (This raises an interesting possibility—
begin
for i in 1..pRows loop should all of the individual configuration elements in
pipe row ('I am nested table row '||to_Char(i)); an application be placed in a single row returned by a
end loop;
return; deterministic pipelined function, ready to join to any
end; query in the same manner DUAL is?)
/
Listing 2. Declaring an object type and a collection type for the Accessing the data through the pipelined function is
pipelined function. simply a matter of casting it to a table—see Listing 4.
CREATE OR REPLACE
TYPE PIPEDTEXT Listing 4. Reading data from a pipe in a query.
AS OBJECT
(
SQL> begin
sessionid number,
2 for i in 1..3 loop
date_sent date,
3 pipelined.writeToPipe('gmenchen','Row number '
seq number,
4 || to_char(i));
text varchar2(2000) 5 end loop;
) 6 end;
/ 7 /
CREATE OR REPLACE
TYPE PIPEDTEXTTBL PL/SQL procedure successfully completed.
AS TABLE OF "PIPES"."PIPEDTEXT"
/
SQL> select text
2 from table(pipelined.readPipe('gmenchen'))
Next, I’ll declare a package to hold the pipelined 3 /
function itself (see Listing 3). As a convenience, I’ve
TEXT
included a procedure that will send some data down the ---------------------------------------------------
pipe to be read. Row number 1
Row number 2
Row number 3
You can then retrieve the contents of the pipe by One of the implications of this is that we can
selecting from the view: provide much more flexible security on pipes than is
available when using just PL/SQL to access pipe content.
SQL> select seq,sessionid,date_sent We can, for example, create a private pipe, then create a
2, substr(text,1,20) text view on the pipe content through a pipelined procedure,
3 from pipeview
4 / and make standard grants on that view to other schemas
SEQ SESSIONID DATE_SENT TEXT or roles. This would provide the same level of security
---------- ---------- --------- -------------------- on pipes that’s available for tables.
7 2165 10-APR-04 Row number 1
8 2165 10-APR-04 Row number 2
9 2165 10-APR-04 Row number 3 Using an INSTEAD OF trigger to insert data
into a pipe
However, I found I was unable to make a grant on the Another interesting result of having a view on the
view to a role or to another schema (see Listing 5). contents of the pipe is that we can use an INSTEAD
OF trigger on that view to insert data into the pipe:
Listing 5. Trying to grant SELECT on the view.
CREATE OR REPLACE TRIGGER PIPEVIEW_INS
INSTEAD OF
SQL> grant execute on pipedtext to public INSERT
2 / ON PIPEVIEW
Grant succeeded. REFERENCING NEW AS NEW OLD AS OLD
SQL> grant execute on pipedTextTbl to public begin
2 / pipelined.writeToPipe('gmenchen',:new.text);
Grant succeeded. end;
SQL> grant execute on pipelined to public /
2 /
Grant succeeded.
SQL> grant select on pipeview to public We can insert into the view specifying the text
2 /
grant select on pipeview to public column, as in the following:
*
ERROR at line 1: sql> insert into pipeview(text) values('hello world');
ORA-22905: cannot access rows from a 1 row created.
non-nested table item sql> select text from pipeview
2 /
TEXT
Creating a view of a pipelined table function -------------------------------------------------------
that’s grantable hello world
I included grants on every possible dependent object
(as you can see from Listing 5) and also tried various Or we can use a values clause that has contents for all
permutations, including making the view an object view, of the columns in the view:
but with the same results. Finally, I stumbled across an
sql> insert into pipeview(sessionid,date_sent,seq,text)
example in the “PL/SQL Collections and Record Types” 2 values (1, sysdate,1,'hello #2');
section of PL/SQL User’s Guide and Reference and found
1 row created.
the following statement:
sql> select sessionid,date_sent
2 ,seq,substr(text,1,24) text
To perform DML operations on a PL/SQL nested table, 3 from pipeview
4 /
use the operators TABLE and CAST.
SESSIONID DATE_SENT SEQ TEXT
---------- --------- ---------- ----------------------
Clearly there’s a fundamental difference between 1909 21-MAR-04 3 hello #2
creating a view and selecting from it in one schema,
and making a grant on that view to a role or to another Because we’re using an INSTEAD OF trigger, it
schema. It seems that when you use the TABLE doesn’t really matter that we’re including all of the
operator by itself, it causes the resulting recordset to columns in the view—only the TEXT column is processed
lose its tight identification with the collection type within the trigger; the other columns are populated by the
defined at the schema level, and the exact form of the WriteToPipe procedure called from the trigger body.
table is determined at runtime. This is true with Oracle9i We can now grant INSERT on this view, and allow
tables used to manipulate data into a structure that can SQL> select pipedObjects.getCtr from dual
2 /
be used to produce rows in a report. Pivot queries,
collapsing and expanding the level of detail (week, GETCTR
----------
month, quarter, fiscal year combined with department, 2
division, corporation, and so on) usually result in either
creating many new database objects or extensive use of Are pipelined table functions ready for
dynamic SQL that can be difficult for others to modify. prime time?
Inside a pipelined function, one can assemble a VARRAY Anyone planning to put pipelined table functions
or NESTED TABLE of the rows for a report, using into production does need to review the various bug
multiple passes to populate the columns. reports on Oracle’s Web site—in particular, Bug 2883743
(default parameters in pipelined table functions cause
Implications of using queries inside pipelined memory leak) and Bug 2403065 (a problem with the
table functions no_data_needed exception). The most serious problems
If it’s likely that data will be retrieved from a pipelined are caused when the collection type returned by the
table function using a where clause: function is defined inside a package rather than at the
schema level—it may compile, but it’s certain to cause
Select * from table( <my_pipelined_function>) serious problems in use. The memory leak problem
where <expression>
occurs when the parameters of a pipelined function have
then what happens to the processing of the cursor inside default values—to avoid the issue, don’t use default
the pipelined function? Let’s look at a package that values. This Bug was recently reported as being fixed in
contains a trivial pipelined function that tracks the Oracle version 10.1.0, but I haven’t confirmed Oracle’s
iterations inside the embedded cursor—see Listing 6. claim yet.
Pipelined table functions are ideal for use in
providing SQL access to data that’s normally only
Listing 6. Tracking fetches inside a cursor. available using PL/SQL, such as the interface to
dbms_pipes described in this article. A second use is
CREATE OR REPLACE
PACKAGE PIPEDOBJECTS in assembling rows of data where there are advantages
IS of speed or simplicity in collecting the data piece by
function AllObjects return textTbl pipelined; piece rather than in a single query. And finally, wrapping
function getCtr return number; a pipelined table function in a view opens up new
END; -- Package spec
/ possibilities for the exchange of data between Oracle’s
SQL and PL/SQL contexts. ▲
CREATE OR REPLACE
PACKAGE BODY PIPEDOBJECTS 406MENCHEN.TXT at www.pinnaclepublishing.com
IS
pvCtr integer := 0;
function allObjects return textTbl pipelined Gary Menchen is a senior programmer analyst at Dartmouth College
is
in Hanover, New Hampshire. He’s been writing articles on computer
begin
for rec in (select object_name from all_objects) programming for 20 years on subjects ranging from PL/SQL to Clipper,
loop C, Assembly Language, and—ancient history—dBase. At the moment,
pipe row (rec.object_name);
pvCtr := pvCtr + 1; he’s particularly interested in document generation using PL/SQL.
end loop; Gary.E.Menchen@Dartmouth.Edu.
R
…
EGULAR expressions are a tool for searching and
manipulating textual data. We use regular
Once you have the check constraint in place,
expressions all the time—even if we don’t realize
there’s no need to validate the e-mail address in the
it. If you use a search engine on the Web, you’re already
front-end code.
using regular expressions. Most word processors and
modern languages such as shell, awk, Perl, and Java ALTER TABLE emp MODIFY
have built-in support for regular expressions. Regular (email varchar2(50), check
(regexp_like(email,
expressions are a set of metacharacters, or a pattern, '[:alnum:]*\@[:alnum:]\.[com|org|edu]')));
that represents chunks of text. For example, a pattern like
--valid email address
“[0-9]?[0-9]:[0-9][0-9]:[0-9][0-9] [A|a|P|p][M\m]” can SQL> INSERT INTO emp (person_id,ssn,email)
be used to represent any text like “12:01:01 AM” or VALUES (1,'999999999','email@domain.com');
1 row created.
“1:10:10 pm” or even “5:10:10 pM”. Regular expressions
are very powerful. A simple regular expression can --invalid email address does not contain '@'
SQL> INSERT INTO emp (person_id,ssn,email)
replace hundreds of lines of code. You might use regular VALUES (1,'999999999','emaildomain.com');
expressions to parse a large amount of text data, or to ERROR at line 1:
ORA-02290: check constraint (SYS.SYS_C005315) violated
search textual data using a pattern, or perhaps to verify
the format of a Social Security number. Once you start By moving the logic to the centralized place in the
using it, you’ll become addicted to it. database, you get the obvious maintenance advantage
of not having to duplicate your validation in multiple
The need for regular expressions in the database front ends.
Regular expressions (regexp) can be very useful in The Social Security number (SSN), which is stored as
Web applications. Most Web applications have a a CHAR(9) in the emp table, could also be presented in
database at the back end to store data. Inclusion of “999-99-9999” format by creating a simple view with a
regular expressions in the database eliminates the regular expression function as shown here:
need to duplicate logic in all front-end (or middle-tier)
applications. For example, regular expressions can be CREATE OR REPLACE VIEW emp_v AS
used to validate e-mail addresses using a pattern that SELECT regexp_replace(ssn
,'([0-9]{3})([0-9]{2})([0-9]{3})','\1-\2-\3') ssn, …
might contain characters like “.” and “@” and text like FROM emp;
“com” or “org” at the end.
Consider the scenario where you have a central Once you have the view in place, all of your
employee database that stores personal information. applications can simply query the view instead of
The database is a central back end for your Web retrieving and formatting Social Security numbers on
applications, ERP application, and reporting applications. the front end. Effectively, all of your applications need
You need to validate employee e-mail addresses entered not know how the Social Security number is stored in
from all of your applications. One way to achieve this the database.
is to put the verification logic on all of your front-end
applications separately, meaning you need to put the Regular expressions in Oracle10g
validation logic in the Web applications and the ERP Oracle Database 10g includes support for IEEE/POSIX
application. On the other hand, this could be achieved standard native regular expressions in the SQL. Oracle
by using a simple regular expression in the database. regular expression capability is compatible with that
By using a new regular expression SQL function, you available in programming environments such as Unix,
can add a check constraint on the email column as Perl, and Java. Programmers familiar with regular
shown here: expressions have always used external programs outside
{m,} Matches at least m occurrences. a{3,} matches aaa, aaaa, aaaaa, and so forth.
{m,n} Matches at least m but no more than n occurrences. a{3,5} matches aaa, aaaa, or aaaaa.
[^] Matches any character except for the expressions represented in the list. [^abc] matches any character except a, b, and c.
\n Matches the nth sub-expression enclosed between parentheses \1 matches the first sub-expression enclosed between
preceding the \n. parentheses before \1.
[::] Specifies character classes (for example, [:alpha:]). It matches any [:alpha:] matches any alphabetical character in the
character within the character class. database character set. For US7ASCII, it matches any
character from a to z or A to Z.
[..] Specifies one collation element, and can be a multi-character element. [.ch.] in Spanish specifies one collation element.
[==] Equivalence class operator. [=a=] matches all characters with base letter a, such as
a, á, à, â, ã, ä, and å.
Function:
Return value:
REGEXP_LIKE
Returns the matched portion of the pattern from the string.
Signature:
REGEXP_LIKE
Function:
(source_string REGEXP_REPLACE
,pattern
[,match_parameters])
Signature:
REGEXP_REPLACE
Parameters: (source_string
• source_string specifies source data to be scanned. ,pattern
[,replace_string
• pattern is the regular expression to search within the source_string. [,position
• match_parameters* is an optional parameter that lets you change [,occurrence
the default behavior of REGEXP_LIKE. [,match_parameter ]]]])
'^[^0-9]*[0-9]{3}[^0-9]*[0-9]{3}[^0-9]*
[0-9]{4}[^0-9]*[0-9]+[^0-9]*$' Notice that the query uses i as the last parameter
in the REGEXP_SUBSTR function. It specifies case
The ^ and $ characters at each end act as anchors. insensitivity, meaning it will match Tylenol or tylenol.
The [^0-9]* represents any non-numeric character any
number of times. And [0-9] represents any numeric digit. REGEXP_REPLACE
You can use character class [:digit:] instead of [0-9] if The REGEXP_REPLACE function allows you to search
A real-life example
One common business problem is to
allow searches to be sensitive to
transposed adjacent characters. For
my example, I’ll use our Web-enabled
HRMS application. I’d like to search Figure 2. The problem definition.
-- All of following queries use index pi_rglike SQL > SELECT REGEXP_INSTR('anything','z*') FROM dual;
SELECT email1 FROM person_info
WHERE REGEXP_LIKE(email1,'.*.org'); REGEXP_INSTR('ANYTHING','z*')
-----------------------------
--uses index!! 1
Recently, the DBA manager at a former client asked me to help name you’ve switched to. Really, you haven’t truly logged into
him with a SQL problem. They have a third-party vendor tool that schema. As a result, the USER* views still reflect the
called Ascential, which is an ETL tool that uses the USER* data underprivileged schema, not the one in the current context.
dictionary views. They want Ascential to first connect to a Nevertheless, the DBA manager wanted to be able to “fake it”
database account with minimum privileges and then alter its into using the target schema’s USER* views, such that Ascential
session to a different schema, using: would work as desired. This tip describes one solution I came
up with.
ALTER SESSION SET CURRENT_SCHEMA=B First, I create a table in the privileged schema and grant
SELECT on it to the underprivileged account. In this example,
where schema B is a privileged account. The trouble is, setting the privileged account is PPDEV and the underprivileged
the current schema in this fashion only causes the SQL engine account is PP11553 (“PP” indicating user schemas, instead of
to qualify the unqualified table references with the schema application schemas). Continues
Pinnacle, A Division of Lawrence Ragan Communications, Inc. ▲ 800-493-4867 x.4209 or 312-960-4100 ▲ Fax 312-960-4106
OWNER TABLE_NAME
Next, I create a view in PPDEV’s schema to look just like ------ ----------
the USER* data dictionary view. This view, against the ALL* PPDEV BLICK
For access to current and archive content and source code, log in at www.pinnaclepublishing.com.
Phone: 800-493-4867 x.4209 or 312-960-4100 Copyright © 2004 by Lawrence Ragan Communications, Inc. All rights reserved. No part
Fax: 312-960-4106 of this periodical may be used or reproduced in any fashion whatsoever (except in the
Email: PinPub@Ragan.com case of brief quotations embodied in critical articles and reviews) without the prior
written consent of Lawrence Ragan Communications, Inc. Printed in the United States
of America.
Advertising: RogerS@Ragan.com
Oracle, Oracle 8i, Oracle 9i, PL/SQL, and SQL*Plus are trademarks or registered trademarks of
Editorial: FarionG@Ragan.com Oracle Corporation. Other brand and product names are trademarks or registered trademarks
of their respective holders. Oracle Professional is an independent publication not affiliated
Pinnacle Web Site: www.pinnaclepublishing.com with Oracle Corporation. Oracle Corporation is not responsible in any way for the editorial
policy or other contents of the publication.
Subscription rates This publication is intended as a general guide. It covers a highly technical and complex
subject and should not be used for making decisions concerning specific products or
applications. This publication is sold as is, without warranty of any kind, either express or
United States: One year (12 issues): $199; two years (24 issues): $348 implied, respecting the contents of this publication, including but not limited to implied
Other:* One year: $229; two years: $408 warranties for the publication, performance, quality, merchantability, or fitness for any particular
purpose. Lawrence Ragan Communications, Inc., shall not be liable to the purchaser or any
Single issue rate: other person or entity with respect to any liability, loss, or damage caused or alleged to be
caused directly or indirectly by this publication. Articles published in Oracle Professional
$27.50 ($32.50 outside United States)* reflect the views of their authors; they may or may not reflect the view of Lawrence Ragan
Communications, Inc. Inclusion of advertising inserts does not constitute an endorsement by
* Funds must be in U.S. currency. Lawrence Ragan Communications, Inc., or Oracle Professional.