You are on page 1of 16

Oracle

Solutions for High-End


Oracle® DBAs and Developers Professional

Building Your Own


Extensible Indexes,
Part 2
Bryn Llewellyn and Steven Feuerstein
Last month, Bryn Llewellyn and Steven Feuerstein explored the concept behind the
“domain index framework.” In this article, they illustrate the step-by-step procedures
for creating and using domain indexes.

DDL operations
Before you can use an index, you must create it. And then, later on, you might February 2003
decide to drop or alter the index. We need to provide callback methods
Volume 10, Number 2
corresponding to each of those DDL actions so that the domain index is
properly integrated. 1 Building Your Own
Extensible Indexes, Part 2
When creating an index Bryn Llewellyn
ORACLE issues this callback when the user issues a create index statement: and Steven Feuerstein

10 Optimizing Oracle Portal


static function ODCIIndexCreate (
i_index_info in sys.ODCIIndexInfo, Packages for Intranets
i_parameter_string in varchar2, Michael J. Ross and Tom Scott
i_env_not_used in sys.ODCIEnv )
return number
14 Using PLL Libraries with
Form Builder
The i_index_info input argument, via the attributes of the ODCIIndexInfo Tom Reid
TYPE, tells us: the name of the domain index to create, and the schema to create it
in; the name and datatype of the column to be indexed, and the name of the table 16 February 2003 Downloads
it’s in; and the name of the schema the table is in.
Actually, the ODCIIndexInfo TYPE expresses quite a lot more information than

The material in this article is based on material prepared by Bryn Llewellyn


as part of a set of code samples published on the Oracle Technology Network’s Indicates accompanying files are available online at
www.oracleprofessionalnewsletter.com.
PL/SQL home page at http://otn.oracle.com/tech/pl_sql/.
we use here. Just describe it at SQL*Plus to get an idea, and Like_Any_Parsed_Word for each row, and when TRUE, insert
search for the attributes we don’t use in the documentation. the ROWID into the <index-name>$R table.
You’ll soon get a feeling for the richness of the framework
OPEN cur_target_table FOR
and its power to allow you to implement the same features 'select '
(for instance, parallel creation, locally partitioned) as are || 'ROWID ,'
|| i_index_info.indexcols (1).colname
enjoyed by the native B*-tree index. || ' from '
As we illustrated, the create index statement takes a || i_index_info.indexcols (1).tableschema
|| '.'
parameters clause whose argument is a single VARCHAR2. || i_index_info.indexcols (1).tablename;
This is passed to us in the i_parameter_string input argument. LOOP
It’s up to us as designers of this to decide what semantics we FETCH cur_target_table INTO v_rid, v_text;
EXIT WHEN cur_target_table%NOTFOUND;
need to express through this and to invent a suitable syntax.
IF like_any_support.like_any_parsed_word (
As we mentioned in our description of the v_text, v_index_name)
ODCIGetInterfaces method, the third input argument, of -- Uses the cached parsed parameters
-- for this domain index
datatype ODCIEnv, passes information to allow us to THEN
support partitioned indexes. Every one of the following EXECUTE IMMEDIATE
'insert into '
methods has such an argument, and in no case do we use it || v_index_table_name
|| ' values ( :the_rid )'
for the Like_Any example. USING v_rid;
What do you code in the body of this method? END IF;
END LOOP;
You’ll create the data structures to store the persistent
CLOSE cur_target_table;
representation of your domain index data. And you’ll scan
the column to be indexed and compute and store the
Finally, we create the <index-name>$I index on the
required information. This might include creating and
<index-name>$R table:
populating explicit structures for the domain index’s
metatdata. Of course, you could store the index data on the EXECUTE IMMEDIATE
filesystem, but we wouldn’t recommend that! The normal 'CREATE UNIQUE INDEX ' ||
v_index_table_btree_name ||
approach is to create schema objects whose names are ' ON ' ||
v_index_table_name || '( rid )';
systematically derived from the name of the domain index
(for example, a <index-name>$R table and a <index-name>$I
The purpose of this B*-tree index is to enforce
B*-tree index on it) in the schema designated for the index.
the unique constraint and to support the Functional
This implies the use of native dynamic SQL, as do most of
Implementation.
the DDL and DML operations.
For our Like_Any INDEXTYPE we set up the names:
When dropping an index
v_index_name := ORACLE makes a callback to the following function in
i_index_info.IndexSchema || '.' || response to a user’s drop index statement:
i_index_info.IndexName;

v_index_table_name := static function ODCIIndexDrop (


v_index_name || i_index_info in sys.ODCIIndexInfo,
Like_Any_Support.Index_Table_Suffix(); i_env_not_used in sys.ODCIEnv )
return number
v_index_table_btree_name :=
v_index_name ||
Like_Any_Support.Index_Table_Btree_Suffix(); The arguments have the same meaning as for
ODCIIndexCreate. What do you code in the body of this
We’ll do this in all the functions described here. Then we method? You’ll drop the data structures that store the
first parse the create index parameter string: persistent representation of your domain index data,
and if appropriate its metatdata. We just drop the
Like_Any_Support.
Parse_Index_Parameter_String ( <index-name>$R table (which causes the <index-name>$I
i_parameter_string, v_index_name ) index to be dropped), and we free up the package memory
that we used to cache the parsed index parameters:
This has the side-effect of storing the parse information
in package globals. execute immediate
Next we create the <index-name>$R index storage table: 'drop table ' || v_index_table_name;

Like_Any_Support.
EXECUTE IMMEDIATE Purge_Cached_Parsed_Words (
'CREATE TABLE ' || v_index_name );
v_index_table_name ||
'( rid ROWID NOT NULL)';
When altering an index
Now we run a cursor over the table with the indexed ORACLE makes a callback to the following function in
column, selecting it and the ROWID. We evaluate response to a user’s alter index statement:

2 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


static function ODCIIndexAlter ( we need to make sure that every time a row in the table is
i_index_info in sys.ODCIIndexInfo,
io_parameter_string in out varchar2, inserted, deleted, or updated, the domain index (which
i_alter_option in number,
i_env_not_used in sys.ODCIEnv )
consists of, remember, a list of ROWIDs) is also updated.
return number
When inserting a row
This command, with the rename or rebuild options ORACLE issues this callback for each inserted row when the
parameter, i_alter_option, and the parameters clause, captures user inserts into the table with the domain-indexed column:
the user’s meaning for a range of possible index
maintenance operations. For a domain index, these are static function ODCIIndexInsert (
i_index_info in sys.ODCIIndexInfo,
partly obvious specializations of the generic operations i_rowid in rowid,
(most obviously rename) and partly operations that are i_new_field_value in varchar2,
/* or some other datatype */
unique to the given INDEXTYPE. For example, a domain i_env_not_used in sys.ODCIEnv )
return number
index for free text retrieval may warrant a maintenance
operation to add stopwords to the existing list. The
The arguments we’ve already met (i_index_info and
parameters clause allows the implementer to invent a syntax
i_env_not_used) have the meaning that we’ve already
for any rebuild semantics he intends.
explained. i_new_field_value is the new value in the indexed
Again the arguments in common with ODCIIndexCreate
column, and i_rowid is of course the ROWID of the new
have the same meaning as the ones defined there. Note that
row. What do you code in the body of this method? You
io_parameter_string is now an in/out argument. Its out
analyze the new column value and derive from it whatever
meaning is valid when the user has altered the index
information and/or actions you’ve specified in your
parameters. i_alter_option captures whether the user has
algorithm. And you record that information against
mentioned the rebuild or the rename keyword. For Like_Any,
the new ROWID.
we just Raise_Application_Error to say it’s not implemented.
You should provide an overloading of this function for
each for the column datatypes you support with your
When truncating the table
domain index. (In the Like_Any example we support just
ORACLE makes a callback to the following function in
VARCHAR2.) We use exactly the same logic as we did in
response to a user’s truncate table statement:
ODCIIndexCreate:
static function ODCIIndexTruncate (
i_index_info in sys.ODCIIndexInfo, if Like_Any_Support.Like_Any_Parsed_Word (
i_env_not_used in sys.ODCIEnv ) i_new_field_value, v_index_name )
return number then
execute immediate
'insert into ' ||
The arguments have the same meaning as already v_index_table_name ||
'( rid ) values ( :the_rid )' using i_rowid;
explained. Typically—as discussed in our description of end if;
ODCIIndexCreate—the domain index datastructure is one or
several schema-level tables (and/or index organized tables), When deleting a row
usually with appropriate B*-tree indexes where the names of ORACLE issues this callback for each deleted row when the
all these objects are systematically derived from the domain user deletes from the table with the domain-indexed column:
index name. So what do you code in the body of this
static function ODCIIndexDelete (
method? Probably you’ll simply truncate all the tables in the i_index_info in sys.ODCIIndexInfo,
domain index datastructure. We drop the <index-name>$I i_rowid in rowid,
i_old_field_value in varchar2,
index, truncate the <index-name>$R table, and re-create the /* or some other datatype */
<index-name>$I index: i_env_not_used in sys.ODCIEnv )
return number

EXECUTE IMMEDIATE
'DROP INDEX ' || The arguments we’ve already met have the same
v_index_table_btree_name; meaning as already explained. i_old_field_value (not used in
EXECUTE IMMEDIATE the Like_Any example) is the value in the indexed column
'TRUNCATE TABLE ' ||
v_index_table_name;
for the row that’s been deleted. What do you code in the
body of this method? You need to update the index data
EXECUTE IMMEDIATE
'CREATE UNIQUE INDEX ' || structures so that they’ll never return the ROWID of the
v_index_table_btree_name || deleted row in response to a query. Depending on your
' ON ' ||
v_index_table_name || '( rid )'; algorithm, you may be able to do that without knowing
the value of the deleted field. You should provide an
DML operations overloading of this function for each of the column
An index won’t do us much good if its contents aren’t datatypes you support with your domain index (unless you
synchronized with that of the underlying database table. So don’t use i_old_field_value and all the column types you

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 3


support can be cast to the datatype of i_old_field_value in ' ( rid ) values ( :the_rid )' using i_rowid;
exception when e_unique_constraint_violated
your implementation). then null; end;
else
We delete the row for i_rowid from the <index-name> execute immediate
$R table: 'delete from ' || v_index_table_name ||
' where rid = :the_rid' using i_rowid;
end case;
execute immediate
'delete from ' ||
v_index_table_name || Query operations
' where rid = :the_rid' using i_rowid;
The three functions ODCIIndexStart, ODCIIndexFetch,
ODCIIndexClose follow the same paradigm we’re used
When updating a row
to when working with a PL/SQL explicit cursor for loop:
ORACLE issues this callback for each updated row when the
open; fetch in a loop until done; close. Here though we’re
user updates the table with the domain-indexed column:
on the other side of the fence. ORACLE will call our
static function ODCIIndexUpdate ( ODCIIndexStart to allow us to do our setup (which typically
i_index_info in sys.ODCIIndexInfo, will involve opening one or several cursors), and it will
i_rowid in rowid,
i_old_field_value in varchar2, then call ODCIIndexFetch repeatedly so that we can send
/* or some other datatype */ back batches of hit ROWID values until eventually we signal
i_new_field_value in varchar2,
/* or some other datatype */ that we have no more. (Typically we’ll fetch from one or
i_env_not_used in sys.ODCIEnv ) several cursors in our ODCIIndexFetch.) Then it will call
return number
ODCIIndexClose to allow us to do whatever cleanup we need
The arguments have the same meaning as already to do, typically closing our cursors.
explained. What do you code in the body of this method?
You need to update the index data structures to reflect the Set up the ROWIDs cache
change in the indexed field, which is why in general you Listing 1 shows what the Start method looks like.
need both its old and its new values. A common approach Now we see why the interface mechanism was designed
(though we don’t follow it in the Like_Any example) is to as the methods of a TYPE. In general, a given session could
model update as delete of the old row followed by insert have two or more open cursors for a query that uses a
of the new row. particular OPERATOR. Each cursor needs to keep track of
The same considerations for overloading apply where it is in its progress. Thus ORACLE asks us (via the
as before. io_scan_ctx IN/OUT argument) to instantiate an object of the
We evaluate Like_Any_Parsed_Word for the new value of TYPE that implements our methods. We’ll record whatever
the field. (We ignore the old value.) When it’s TRUE, we we need for our state of progress in suitably defined
insert the ROWID into the <index-name>$R table. We trap the attributes of this TYPE.
unique constraint violated exception with a NULL action—it Note: It’s of course essential to code the instantiation of
doesn’t matter, of course, and this is the easiest way to detect the TYPE. If you forget that, you’ll get ORA-06530: Reference
and act on the case that this ROWID was in the table already. to uninitialized composite at runtime when ORACLE calls your
When it’s FALSE, delete the row for this ROWID from ODCIIndexStart and you try to set the attributes of a non-
the <index-name>$R table. (Of course, we don’t need to raise existent io_scan_ctx object.
an exception if the ROWID wasn’t there anyway.) We now see (as promised) why ODCIIndexStart and all
the previous methods discussed are static functions, while
case Like_Any_Support.Like_Any_Parsed_Word ( ODCIIndexFetch and ODCIIndexClose are member functions.
i_new_field_value, v_index_name )
when true then All the previous methods discussed are invoked singly, but
begin ODCIIndexStart, ODCIIndexFetch, and ODCIIndexClose are
execute immediate
'insert into ' || v_index_table_name || invoked in concert with each other. ODCIIndexStart must
instantiate an object of its “self”
Listing 1. The Start method. TYPE to provide a context for the
state of progress information. Then
static function ODCIIndexStart ( ODCIIndexFetch and ODCIIndexClose
io_scan_ctx in out Like_Any_Methods, /* the TYPE of "self" */
i_index_info in sys.ODCIIndexInfo, need to access this state of progress
i_pred_info in sys.ODCIPredInfo,
i_query_info in sys.ODCIQueryInfo,
information via the attributes of this
i_lower_bound in number, /* same datatype as returned... */ instantiated object.
i_upper_bound in number, /* ...by the operator */
i_param_2 in varchar2, What about the other arguments
i_param_3 in number, that we haven’t previously met?
...
/* i_param_2 through i_param_N correspond to the 2nd through Nth arguments i_lower_bound and i_upper_bound
of the operator this TYPE implements. */ (not used in the Like_Any example)
i_env_not_used in sys.ODCIEnv )
return number report on the upper and lower bounds

4 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


of the comparison used in the where clause with our invocations, and the handle in native dynamic SQL, a
OPERATOR, and the flags attribute of i_pred_info says variable of datatype REF CURSOR, is not (yet) allowed as
what the comparison (=, <=, >=, and so forth) was. This the attribute of a schema-level TYPE. Nor is it (yet) allowed
is of course to enable your algorithm to use the user’s as a global PACKAGE variable.
predicate to determine which ROWIDs satisfy the query.
In the Like_Any example, since the predicate can’t be Pass back set of ROWIDs
parameterized, we don’t bother to check the values of Now that we’ve done the setup, let’s see what the fetch
i_lower_bound, i_upper_bound, and i_pred_info. We will make looks like.
use of these values when we implement our Keyword
INDEXTYPE, which we’ll describe in a later article. member function ODCIIndexFetch (
self in out Like_Any_Methods
Other information expressed in the i_pred_info argument -- needed in general 'cos the function will change
-- the TYPE's attributes
includes the name of the OPERATOR used to express the ,
where predicate. (In general, two or more OPERATORs can i_nrows in number,
o_rid_list out sys.ODCIRidList,
be defined for the same INDEXTYPE.) i_env_not_used in sys.ODCIEnv )
i_query_info reports on whether the FIRST_ROWS or return number

ALL_ROWS optimizer hint was used and on which one or


several so-called ancillary OPERATORs were invoked in the ORACLE asks us for i_nrows rows each time it calls this,
select list or order by clause. and we deliver them as elements of the sys.ODCIRidList
We’ll cover the topic of ancillary OPERATORs in a later VARRAY. We may or may not deliver as many rows as
article when we describe the Keyword INDEXTYPE. Briefly, requested. When eventually we’ve delivered all our results,
the feature is motivated by the need to materialize a value we set the last element of o_rid_list to null to tell ORACLE
for closeness of match to the query predicates for each we’re done.
returned row, especially to allow the closest matches to be Recall that as a member function, ODCIIndexFetch
returned first. Consider this query: has access to anything we decide is appropriate and that
we therefore implement as an attribute of our interface
select title from books where contains ( methods TYPE. For our Like_Any implementation, we need
body, 'oracle', 42 ) > 0
order by score(42) desc; only the a_cursor attribute. When we look in a later article at
the Keyword INDEXTYPE, we’ll see that here it’s appropriate
The order by predicate score(42)—with a single label to materialize all the query results when ODCIIndexStart is
argument—is an ancillary OPERATOR for the Oracle- called. Of course we then need to store them so that we can
supplied ConText INDEXTYPE. The contains primary hand them back in batches when ODCIIndexFetch is called.
OPERATOR is parameterized to take the text query expression So we define a nested table of ROWIDs as a data attribute of
argument and the label argument after the column argument. the methods TYPE. Then in ODCIIndexFetch we just pass
The label is chosen arbitrarily by the user and ties the back batches of these pre-calculated results.
ancillary OPERATOR to its primary OPERATOR—necessary The implementation of ODCIIndexFetch for our Like_Any
to avoid ambiguity in the case that the same primary INDEXTYPE loops while calling Dbms_Sql.Fetch_Rows. As
OPERATOR appears two or more times in the where long as we’re still getting rows, we call Dbms_Sql.Column_
clause on different columns. Don’t worry if this seems Value_Rowid, extend the o_rid_list VARRAY of datatype
mysterious. All will be revealed by our implementation of sys.ODCIRidList, and set the new element to the fetched
the Keyword INDEXTYPE. ROWID. When we’ve returned the requested number of
For our Like_Any INDEXTYPE, we instantiate rows, or when we’ve fetched all the rows, we exit:
io_scan_ctx as an object of the “self ” class. Then we invoke
Dbms_Sql.Open_Cursor and store the returned handle as the o_rid_list := SYS.odciridlist ();

a_cursor attribute of io_scan_ctx. Next we parse, set up the WHILE (v_ridcount < i_nrows AND NOT v_done)
LOOP
define binding, and execute a_cursor for 'select rid from< v_rowcount := DBMS_SQL.fetch_rows (a_cursor);
index-name>$R'. o_rid_list.EXTEND ();
v_ridcount := v_ridcount + 1;
CASE v_rowcount
io_scan_ctx := like_any_methods (DBMS_SQL.open_cursor); WHEN 0
THEN
DBMS_SQL.parse (io_scan_ctx.a_cursor,
v_done := TRUE;
'select rid from ' || v_index_table,
DBMS_SQL.native o_rid_list (v_ridcount) := NULL;
); EXIT;
ELSE
DBMS_SQL.define_column_rowid ( DBMS_SQL.column_value_rowid (a_cursor, 1, v_rid);
io_scan_ctx.a_cursor, 1, v_rid); o_rid_list (v_ridcount) := v_rid;
END CASE;
v_junk := DBMS_SQL.EXECUTE (io_scan_ctx.a_cursor); END LOOP;

Note: We can’t use native dynamic SQL here because Close the ROWIDs cache
we need to hold the cursor context across successive ORACLE calls the following program when all the results
www.oracleprofessionalnewsletter.com Oracle Professional February 2003 5
have been delivered. We can then perform whatever cleanup ODCIConst.CleanupCall to enable us to clean up whatever
is necessary. caching data structures we’ve used. Moreover, on the first
call the io_scan_ctx in/out argument is NULL, enabling us to
member function ODCIIndexClose ( detect that and to initialize our caching data structures as
i_env_not_used in sys.ODCIEnv )
return number purpose-designed attributes of the TYPE that implements
our interface. ORACLE looks after this context (that is, the
Again, as a member function, ODCIIndexClose has access reference to that object) for us between the calls.
to anything we decide is appropriate. We’ll see later that for We don’t take advantage of this mechanism in our
the Keyword INDEXTYPE, we free up our results buffer. For Like_Any example. (But we will in the Keyword
the Like_Any implementation we just close our cursor: INDEXTYPE.) On the first call, we instantiate io_scan_ctx as
an object of the “self ” class, with an arbitrary value for its
Dbms_Sql.Close_Cursor ( a_cursor );
a_cursor attribute. (This won’t be used per se. But it illustrates
the paradigm and is necessary for the control flow.) Then
The Functional Implementation
we invoke local procedure Evaluate_Satisfies_Like_Any to
ORACLE issues this callback when it chooses a query
calculate the return value.
execution plan driven by some other index(es), so that the
On the second and subsequent regular calls, we invoke
row with our domain-indexed field is accessed by ROWID. It
local procedure Evaluate_Satisfies_Like_Any to calculate the
then needs to know if the predicate expressed by the domain
return value.
index’s OPERATOR is satisfied by just this row. In this case,
And on the final cleanup call, we do nothing.
ORACLE will evaluate the comparison, so it just needs a
The local procedure Evaluate_Satisfies_Like_Any selects
value corresponding to the OPERATOR’s return datatype.
the requested ROWID value from the <index-name>$R table.
Listing 2 shows what it looks like.
On finding the row, it sets the return value to 1. On failing to
If we didn’t care about efficiency, we could implement
find the row, it sets the return value to 0:
this by performing explicit analysis of the supplied field
value on demand, reusing some of the code we’d written PROCEDURE evaluate_satisfies_like_any
for index population and maintenance. (This gets a bit IS
v_rid ROWID;
complicated. In rare cases, ORACLE might call our BEGIN
BEGIN
Functional_Implementation when the value doesn’t arise EXECUTE IMMEDIATE 'select rid from '
from a column. Then i_column is NOT NULL and i_index_ctx || v_index_table
|| ' where rid = :v'
of course has no information about the non-existent index. INTO v_rid
We won’t pursue that here.) In the common case, USING i_index_ctx.rid;
v_retval := 1;
Functional_Implementation is called when the value arises EXCEPTION
WHEN NO_DATA_FOUND
from a column. In that case i_column is NULL and i_index_ctx THEN
supplies the information that enables us to access the index. v_retval := 0;
END;
For many INDEXTYPEs, we’ll be able to compute the return END evaluate_satisfies_like_any;
value of Functional_Implementation more efficiently by
accessing the index data than by explicit analysis of the Listing 3 (on page 7) shows the executable section of
supplied field value. Functional_Implementation.
Note: When ORACLE calls the functional The procedure Like_Any_Support.Check_Index_Exists
implementation, it does not switch user to the domain index queries DBA_INDEXES to ensure that the given Like_Any
owner. So if, as we do for the Like_Any INDEXTYPE, you index exists and, if it doesn’t, raises an exception.
implement it as an invoker’s rights program (that is, compiled
with AUTHID CURRENT_USER), then you’ll have to make Supporting logic for Like_Any
sure that the querying user has SELECT privilege on the The Like_Any INDEXTYPE depends on just two very
index storage table(s). straightforward algorithms. We’ve implemented them in the
In general, we might expect our
Functional_Implementation to be called Listing 2. The Functional Implementation.
several times during the execution
of a given query, and we might well static function Functional_Implementation (
i_column in varchar2, /* operator_parameter_list */
be able to increase the efficiency i_param_2 in varchar2, /* " " " */
of the process by caching some data. i_param_3 in number, /* " " " */
... /* " " " */
Thus ORACLE calls us repeatedly /* i_param_2 through i_param_N correspond to the 2nd through Nth arguments
with i_cleanup_flag set to of the operator this implements.
The semantics of its return value exactly match those of that operator. */
ODCIConst.RegularCall. And then one i_index_ctx in sys.ODCIIndexCtx,
io_scan_ctx in out Like_Any_Methods,
last time (when the result we return is i_cleanup_flag in number )
immaterial) with i_cleanup_flag set to return number

6 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


Like_Any_Support package. In fact,
Listing 3. The executable section of Functional_Implementation.
this was the first code we wrote. This
allowed us to test and debug our BEGIN
essential algorithms via a stand-alone like_any_support.check_index_exists (i_index_ctx);
CASE
harness with no dependency on the WHEN (io_scan_ctx IS NULL)
domain index framework. The AND (i_cleanup_flag = odciconst.regularcall)
THEN
Like_Any_Support package is compiled io_scan_ctx := like_any_methods (99); -- not used
with definer’s rights (that is, with evaluate_satisfies_like_any; -- action the same on first call...
WHEN (io_scan_ctx IS NOT NULL)
authid definer) because: (1) it doesn’t AND (i_cleanup_flag = odciconst.regularcall)
need to access objects in the schemas THEN
evaluate_satisfies_like_any; -- ...as on subsequent calls
of the users who own the index table WHEN (io_scan_ctx IS NOT NULL)
and the index itself; and (2) it does AND (i_cleanup_flag = odciconst.cleanupcall)
THEN
need to access DBA_INDEXES. We v_retval := 0; -- no action required for cleanup
give the Like_Any_Sys user select on END CASE;
END;
sys.dba_indexes privilege at cartridge
install time.

Parsing parameter input for CREATE INDEX The local procedure Get_Next_Word advances along
This procedure accepts the end user’s parameter string the compacted parameter string and detects and stores the
(from the create index statement) and parses it into individual next word:
words, relying on the trivial syntax that one or several
spaces separate words. The words are cached in a package- FUNCTION get_next_word
RETURN BOOLEAN
global two-dimensional PL/SQL table whose first index IS
v_more_words BOOLEAN := TRUE;
(index-by-varchar2) is the domain index name and whose BEGIN
second index (index-by-pls_integer) runs over the set of words g_space_pos := INSTR (g_parms, c_one_space, g_start_pos);
g_word_idx := g_word_idx + 1;
for that domain index: CASE g_space_pos > 0
WHEN TRUE
IDX varchar2(61) THEN
-- to define the type of variable to hold g_words_for_index (i_index_name) (g_word_idx) :=
-- an index name SUBSTR (g_parms, g_start_pos,
; (g_space_pos - g_start_pos));
type Words_Tab_t is table of varchar2(64) g_start_pos := g_space_pos + 1;
index by pls_integer WHEN FALSE
-- the set of words parsed out of PARAMETERS THEN
-- for a given index g_words_for_index (i_index_name) (g_word_idx) :=
; SUBSTR (g_parms, g_start_pos,
type Words_For_Index_Tab_t is table of Words_Tab_t (1 + g_length - g_start_pos));
index by IDX%type v_more_words := FALSE;
-- multi-dim array: for all indexes, the PARAMETER words END CASE;
; RETURN v_more_words;
END get_next_word;

The appropriate code is in place in the function The executable section of Parse_Index_Parameter_String
Like_Any_Parsed_Word (see below) to populate this cache for can now be quite terse:
a given domain index the first time it’s mentioned in a
particular session. BEGIN
The local procedure Compact_Spaces transforms all g_parms :=
LTRIM (
occurrences of two or more adjacent spaces in the parameter RTRIM (i_parms, c_one_space),
c_one_space);
string to just one. (The g_ naming convention denotes
variables that are visible from the outer scope.) IF g_parms IS NULL
THEN
RAISE e_no_words;
PROCEDURE compact_spaces END IF;
IS
prev_length g_length%TYPE; compact_spaces ();
BEGIN
prev_length := -1; WHILE get_next_word ()
LOOP
LOOP NULL;
g_parms := END LOOP;
REPLACE ( END;
g_parms, c_two_spaces, c_one_space);
g_length := LENGTH (g_parms);
EXIT WHEN g_length = prev_length; Validate that a word is “like any”
prev_length := g_length;
END LOOP;
This takes the value of a field from the domain-indexed
END compact_spaces; column and the name of the domain index and returns

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 7


TRUE if the field has at least one of the parsed words as a user with only connect role and SELECT privilege on the
substring. By the way, we make very effective use of the indexed table.
index by varchar2 table feature, new in Oracle9i Database We then create a second index of INDEXTYPE Like_Any
Release 2. and interweave queries against both and the first. It also
invokes Like_Any_Support’s debugging procedure to show
BEGIN the contents of the parsed index parameters cache.
IF NOT g_words_for_index.EXISTS (i_index_name)
THEN
v_parms := get_parms_from_user_indexes (i_index_name);
parse_index_parameter_string (v_parms, i_index_name);
Summary
END IF; By following the conventions for the signatures (names and
FOR j IN
parameter lists) of all the methods used by ORACLE to
g_words_for_index (i_index_name).FIRST () build and maintain an index, we’re able to integrate our own
...
g_words_for_index (i_index_name).LAST () application-specific indexing algorithms into the low-level
LOOP functioning of the database.
v_found :=
INSTR (i_text, We also found that we could take advantage of the
g_words_for_index (i_index_name) (j), 1) object-relational model and new Oracle9i features to produce
> 0;
EXIT WHEN v_found; an elegant and efficient implementation:
END LOOP; • The object TYPE makes it possible to instantiate an
-- FALSE if we do the loop to completion object type instance that contains a persistent cursor
RETURN v_found; object, and do so for many different queries,
END;
simultaneously within a session.
The procedure Get_Parms_From_User_Indexes queries • String-indexed associative arrays allow us to easily
DBA_INDEXES to get the parameter string that was used create, maintain, and access lists of keywords used
at index creation time. In the implementation of a more in the “like any” algorithm, and for multiple sets
elaborate INDEXTYPE, we might need to implement special of keywords.
structures to store index parameterization data that can’t be
accommodated in the SYS data dictionary. Then we’d invent Next month, we’ll implement the Keyword INDEXTYPE
a naming convention and corresponding inner syntax for the to support more interesting queries: retrieving rows where
PARAMETERS clause. (Don’t worry about this now. We’ll the indexed text field contains either all or any (by choice at
see this technique in action in the Keyword INDEXTYPE.) query time) of a list of keywords, again given at query time.
The Like_Any_Support package also exposes a couple of other Each hit has a score—the number of matching words in the
housekeeping and debugging subprograms whose purpose indexed field. We’ll show how to reference this score in the
is obvious from the code. SELECT list and in the ORDER BY clause. ▲

Testing BRYN.ZIP at www.oracleprofessionalnewsletter.com


The file Run_Stand_Alone_Test.sql tests our basic algorithms
(as promised earlier) independently of the domain index Bryn Llewellyn is PL/SQL Product Manager, Database and Application
Server Technologies Development Group, at Oracle Corporation
framework.
Headquarters. Bryn has worked in the software field for 25 years. He
The file End_To_End_Test.sql is invoked as the last
joined Oracle UK in 1990 at the European Development Center in the
line of Master_Script.sql after connecting as an ordinary Oracle Designer team. He transferred to the Oracle Text team and from
“resource, connect” user (significantly not the Like_Any_Sys there moved into Consulting as EMEA’s Text specialist. He relocated to
user). It issues the full range of SQL commands to ensure Redwood Shores in 1996 to join the Text Technical Marketing Group. His
that each of the methods in the Like_Any_Methods TYPE brief has recently been extended to cover Product Manager
(including the functional implementation) is exercised responsibility for PL/SQL. Bryn.Llewellyn@oracle.com.
and to test for the intended behavior. It checks that the
Steven Feuerstein is considered one of the world’s leading experts on
expected exception is raised on attempting a Satisfies_
the Oracle PL/SQL language. He’s the author or co-author of nine books
Like_Any query while no index of INDEXTYPE Like_Any
on PL/SQL, including the now-classic Oracle PL/SQL Programming and
exists on that column.
Oracle PL/SQL Best Practices (all from O’Reilly & Associates). Steven is a
We want to stress test the invoker’s rights regime and Senior Technology Advisor with Quest Software, has been developing
that the privileges given to the Like_Any_Sys user are software since 1980, and worked for Oracle Corporation from 1987 to
sufficient. And we want to test that the ordinary users of 1992. Steven is president of the Board of Directors of the Crossroads
our INDEXTYPE don’t need any surprising privileges. So Fund, which makes grants to Chicagoland organizations working for
the index is created in a separate schema from the table with social, racial, and economic justice (www.CrossroadsFund.org).
the indexed column. And the queries are issued by a third steven.feuerstein@quest.com.

8 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


AD:
GE
LL P -A
A
FU UG
IO

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 9


Oracle
Professional

Optimizing Oracle Portal


Packages for Intranets
Michael J. Ross and Tom Scott
Here Michael Ross and Tom Scott discuss an approach for Enterprise portals, however, offer a real opportunity
structuring Oracle Portal code that minimizes maintenance to address these challenges and improve corporate
tasks involved with large intranet portlet libraries. information publishing. Having evolved beyond the simple
aggregation of a Web user’s customized data feeds, portals

I
NFORMATION portals based upon the Oracle Portal now have the capability to serve as focal points for online
product hold tremendous promise for data publication. business data. Portals allow an organization to present
In developing portals, a common methodology for information—its own and that garnered from other
implementing the provider and portlet packages involves sources—in pages readable with the ubiquitous Web
creating separate PL/SQL packages for each individual browser, which has quickly become the most common
portlet. While that approach is ideal for publishing computer interface. For the typical company, this approach
individual portlets to the developer community, it creates can enhance the accessibility of all its Web-based
severe maintenance problems for organizations using Portal information, particularly for its dynamic data, in addition
for intranet development involving dozens, and perhaps to the traditional static data. Specifically, a company can
hundreds, of custom portlets. We’ll discuss a way to simplify more easily unify the interfaces to that information,
the structure of portlet libraries in Portal packages. standardize its format, and better control its security.
Oracle Portal is Oracle’s architecture for general portlet
The power of portals and portal development. It was formerly known as WebDB,
Throughout the history of corporate enterprise, particularly and included with the Oracle8i Database. It has since been
in our age of technology, the lifeblood of any organization enhanced and renamed, and is now bundled with Oracle9i
is truly information. The dissemination of it—to the right Application Server (Oracle9iAS). Oracle Portal gives users
people in the right form—is crucial to providing world-class the ability to create and administer information portals, and
service to valued clients, keeping employees informed about in turn, integrate internal as well as public information,
the company in a timely manner, and presenting compelling customize its look and feel, and more effectively deploy
product and service information to prospective customers. Web-based database applications. Every Portal Web page
Yet the underlying technology hasn’t always maximized consists of one or more portlets, each of which accepts and
this potential. delivers data using any browser-capable technology,
In the case of the Internet, the rapid adoption of the including of course the most common one, HTML.
Web provided a worldwide infrastructure for delivering an In order to jumpstart Portal development, Oracle offers
organization’s message. Initially, a manageable number of the Portal Development Kit (PDK), a framework integrated
HTML pages appeared sufficient for publishing limited with Oracle Portal, intended for the creation of portlets and
amounts of static information. But the inevitable growth services on any platform running an Oracle-based portal.
of Web pages on corporate sites—both public and later The PDK is designed with autonomous portlets in mind,
private—resulted in innumerable versioning and making it easier for corporate subscribers to pick and
presentation inconsistencies. These difficulties were choose which portlets to install. Thus, Oracle developers can
proportional to the sophistication and breadth of these create new types of portlets with a common distribution
Internet and intranet sites, and hence to the extent to mechanism, as set forth in the structure of the sample
which each individual firm took advantage of this new provider and portlet packages in the PDK. This article
medium. Recent extensions to HTML (such as CSS, XSL, presents a design for these packages that allows developers
and Dynamic HTML) have helped somewhat, but the to incorporate new portlets without incurring the
technologies aimed at dynamic publishing have invariably maintenance overhead implicit in a separate PL/SQL
resulted in even more moving parts, and a resultant increase package for each one.
in risk, configuration issues, and performance problems.
Furthermore, most of these technologies are file-centric, and The proliferation of portlets
the capabilities of the database, for data management and When considering the adoption of any new technology such
dynamic generation, have been under-utilized. as Oracle Portal, an organization should determine the likely
10 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com
uses for which it can and realistically will be used. Oracle provider’s package body file, named provider_before.pkb in
Portal clearly can be leveraged by firms that derive a our example here.
majority of their business from their public Web sites. But
this is a minority of companies with any significant IT
Listing 1. Redundant code to call portlet functions.
infrastructure, or, for that matter, companies that make an
appreciable use of Oracle products. In fact, not many firms IF ( p_portlet_id = PORTLET_1 ) THEN
RETURN portlet_1_before.get_portlet_info(
worldwide generate their revenue primarily from e- p_provider_id => p_provider_id,
commerce. Moreover, with the dot-com meltdown, that p_language => p_language
);
segment of the corporate world is now even smaller. Thus, ELSIF ( p_portlet_id = PORTLET_2 ) THEN
for typical organizations to justify the costs of using Oracle RETURN portlet_2_before.get_portlet_info(
p_provider_id => p_provider_id,
Portal, they must be able to demonstrate its value aside from p_language => p_language
);
its use for their public sites. (These costs include the product ELSIF ( p_portlet_id = PORTLET_3 ) THEN
licensing expenses, hardware and network infrastructure RETURN portlet_3_before.get_portlet_info(
p_provider_id => p_provider_id,
costs, and the development dollars needed for creating and p_language => p_language
);
maintaining Portal-based sites.) In other words, the bulk of END IF;
the usage for this product will derive from its utility in
creating corporate intranet sites. In this code fragment, retrieving the needed information
As noted earlier, portals may prove to be the ideal about a particular portlet involves executing a separate—yet
tool for aggregating, standardizing, and controlling the almost identical—(ELS)IF clause, and then calling a separate
publication of online corporate data, especially internal-only and nearly identical function in its own portlet package. This
information. If Oracle Portal could be used consistently to duplication of code will result in a higher risk of introducing
gain these advantages, then the use of Oracle Portal can in new bugs into the framework, a greater number of execution
most cases be financially justified. In fact, such a portal paths during unit testing, and the likely repetition of effort
methodology could serve as more than just an architectural when modifications are needed in the future.
framework for Web content. With the proper use of Oracle Using the traditional PDK approach, each of the
databases, most if not all of that content could be centrally three portlets would need its own package, containing
controlled and dynamically created. Such an approach subprograms that register, de-register, and show the
would bypass the costs, complexity, and structuring issues particular portlet, return its properties, and determine
associated with relying upon a large number of directories whether it’s runnable. An example of this is the PDK
and HTML files—as well as Perl and Unix shell scripts, if sample portlet found in PDK\PLSQL\sample\param_
such are utilized. More important, the
content is dynamically generated; Listing 2. Portlet function get_portlet_info() duplicated for every portlet.
hence, the Web pages can easily
FUNCTION get_portlet_info(
display the new content as soon as it is p_provider_id IN INTEGER,
refreshed in the Oracle database. p_language IN VARCHAR2
)
From a source code perspective, RETURN wwpro_api_provider.portlet_record
IS
one advantage of Oracle Portal portlet wwpro_api_provider.portlet_record;
software is that the code can be BEGIN
portlet.id := provider_before.PORTLET_1;
organized in PL/SQL packages. It’s portlet.provider_id := p_provider_id;
portlet.name := 'Portlet_1';
critical to note that the default structure portlet.title := 'Portlet One';
of the current version of the PDK is to portlet.description := 'First portlet';
portlet.image_url := NULL;
create a unique package for every portlet.thumbnail_image_url := NULL;
portlet.help_url := NULL;
portlet. An example of this is the PDK portlet.timeout := NULL;
sample provider found in PDK\ portlet.timeout_msg := NULL;
portlet.implementation_style := NULL;
PLSQL\sample\sample_provider.pkb portlet.implementation_owner := NULL;
portlet.implementation_name := NULL;
(within the PDK installation archive portlet.content_type := wwpro_api_provider.CONTENT_TYPE_HTML;
file). Throughout this provider code, portlet.api_version := wwpro_api_provider.API_VERSION_1;
portlet.has_show_edit := FALSE;
every call to a portlet subprogram is portlet.has_show_edit_defaults := FALSE;
duplicated for each individual portlet. portlet.has_show_preview := FALSE;
portlet.call_is_runnable := NULL;
This is illustrated in the PL/SQL portlet.call_get_portlet := NULL;
portlet.accept_content_type := NULL;
fragment shown in Listing 1, which portlet.has_show_link_mode := NULL;
uses the same approach as the sample portlet.language := wwnls_api.AMERICAN;
portlet.preference_store_path := NULL;
PDK code. (To maximize readability, all portlet.created_on := SYSDATE;
portlet.created_by := wwctx_api.get_user;
of the code for this article was written portlet.last_updated_on := SYSDATE;
from scratch.) This code would be portlet.last_updated_by := wwctx_api.get_user;
RETURN portlet;
located in subprograms defined in the END get_portlet_info;

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 11


portlet.pkb. Continuing with our code in Listing 1, an
Listing 3. Portlet index-by table and portlet IDs.
almost identical get_portlet_info() function must
consequently exist for every portlet. Focusing on the first TYPE portlet_type IS RECORD(
portlet_id INTEGER,
portlet (for instance), this function would be in the portlet’s portlet_package VARCHAR2(30),
package body file portlet_1_before.pkb. The resulting portlet_title VARCHAR2(30),
portlet_name VARCHAR2(30),
get_portlet_info() function is illustrated in Listing 2 (on page portlet_description VARCHAR2(60),
11). Note that it uses a package constant, PORTLET_1, for the has_show_edit BOOLEAN := FALSE,
has_show_edit_defaults BOOLEAN := FALSE,
portlet ID, which is defined as the value 1 in the provider’s has_show_preview BOOLEAN := FALSE
);
package specification.
Clearly, the addition of a new portlet to the provider TYPE portlet_table_type IS TABLE OF portlet_type
INDEX BY BINARY_INTEGER;
would entail the creation of another package just for that one
portlets portlet_table_type;
portlet. Moreover, all provider subprograms that refer to the
portlets would require the insertion of another ELSIF clause PORTLET_1 CONSTANT INTEGER := 1;
PORTLET_2 CONSTANT INTEGER := 2;
in every IF statement that references subprograms in the PORTLET_3 CONSTANT INTEGER := 3;
portlet’s package.
As a result of this structure, any organization that
Listing 4. Initialization of portlet index-by table.
creates a significant intranet or Internet site that relies
heavily upon custom portlets, and uses the current PDK as a portlets(PORTLET_1).portlet_id := PORTLET_1;
portlets(PORTLET_1).portlet_name := 'Portlet_1';
starting point for its portal development, will end up with portlets(PORTLET_1).portlet_title := 'Portlet One';
dozens if not hundreds of packages to maintain. It would be portlets(PORTLET_1).portlet_description := 'First portlet';

a shame to replace the proliferation of HTML files with the portlets(PORTLET_2).portlet_id := PORTLET_2;
portlets(PORTLET_2).portlet_name := 'Portlet_2';
equally costly proliferation of Portal packages. But this is portlets(PORTLET_2).portlet_title := 'Portlet Two';
precisely what happens with the default PDK approach. portlets(PORTLET_2).portlet_description := 'Second portlet';

portlets(PORTLET_3).portlet_id := PORTLET_3;
Restructuring the provider and portlet code portlets(PORTLET_3).portlet_name := 'Portlet_3';
portlets(PORTLET_3).portlet_title := 'Portlet Three';
To avoid the creation of excessive packages, we extract the portlets(PORTLET_3).portlet_description := 'Third portlet';
redundant code, thus allowing almost
all of the portlet subprograms to be free Listing 5. Generalized portlet function get_portlet_info().
of references to individual portlets.
FUNCTION get_portlet_info(
Instead, they can handle all of the p_provider_id IN INTEGER,
provider’s portlets generically. This p_portlet_id IN INTEGER,
p_language IN VARCHAR2
approach involves defining and using )
RETURN wwpro_api_provider.portlet_record
an index-by table to store all of the IS
portlet-specific information, thereby portlet wwpro_api_provider.portlet_record;
BEGIN
allowing the processing of multiple portlet.id := p_portlet_id;
portlet.provider_id := p_provider_id;
portlets in a single package. As before, portlet.name :=
the portlet IDs are defined as package provider_after.portlets(p_portlet_id).portlet_name;
portlet.title :=
constants. The code in Listing 3 shows provider_after.portlets(p_portlet_id).portlet_title;
portlet.description :=
the definition of such a portlet index- provider_after.portlets(p_portlet_id).portlet_description;
by table and the portlet IDs. This portlet.image_url := NULL;
portlet.thumbnail_image_url := NULL;
would be done in the provider’s portlet.help_url := NULL;
portlet.timeout := NULL;
package specification file, named, for portlet.timeout_msg := NULL;
instance, provider_after.pks. portlet.implementation_style := NULL;
portlet.implementation_owner := NULL;
The portlet index-by table is portlet.implementation_name := NULL;
portlet.content_type := wwpro_api_provider.CONTENT_TYPE_HTML;
initialized in the global section of the portlet.api_version := wwpro_api_provider.API_VERSION_1;
provider’s package body (in portlet.has_show_edit :=
provider_after.portlets(p_portlet_id).has_show_edit;
provider_after.pkb), as seen in the portlet.has_show_edit_defaults :=
provider_after.portlets(p_portlet_id).has_show_edit_defaults;
block of code shown in Listing 4. portlet.has_show_preview :=
The redesigned get_portlet_info() provider_after.portlets(p_portlet_id).has_show_preview;
portlet.call_is_runnable := NULL;
function is illustrated in Listing 5. It portlet.call_get_portlet := NULL;
portlet.accept_content_type := NULL;
would be in the package body file portlet.has_show_link_mode := NULL;
containing all the portlets, named, portlet.language := p_language;
portlet.preference_store_path := NULL;
in this example, portlets_after.pkb. portlet.created_on := SYSDATE;
portlet.created_by := wwctx_api.get_user;
Its function signature has one portlet.last_updated_on := SYSDATE;
modification: The portlet ID is now portlet.last_updated_by := wwctx_api.get_user;
RETURN portlet;
passed to the function, in addition to END get_portlet_info;

12 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


the provider ID and language. For the purpose of simplicity, portlet() procedure.
this implementation assumes that the portlet index-by table 5. Add a new ELSIF clause to the provider’s get_
has been initialized such that every portlet’s row number portlet_list() procedure.
in the index-by table is equal to its portlet ID, as done in 6. Add a new ELSIF clause to the provider’s is_
Listing 4. If such is not the case in your implementation, portlet_runnable() procedure.
you’ll need to write and use a lookup function to return the 7. Add a new ELSIF clause to the provider’s register_
row number of a portlet from the portlet index-by table portlet() procedure.
(given the portlet ID). Another improvement is that the 8. Add a new ELSIF clause to the provider’s show_
portlet record’s language field is no longer hard-coded to portlet() procedure.
wwnls_api.AMERICAN, as is done in the PDK; instead, the
parameter p_language is used. This necessitates more effort and changes than this
By using a portlet index-by table to contain and alternative approach, which involves only these steps:
centralize all of the portlet-specific information, the 1. Define a new global constant to represent the portlet ID.
procedure get_portlet_info() has been made into a generic 2. Add a new row of portlet-specific information to the
subprogram that works for all the portlets. This can easily be portlet index-by table.
done for the other portlet subprograms, such as, deregister(), 3. Create a new show_portlet() procedure.
is_runnable(), and register(). The only exception is the 4. Add a new ELSIF clause to the portlet’s show()
procedure show(), which is used to display the particular procedure.
portlet on the browser page. More than likely, each portlet
will need its own show() procedure, to generate and output The end result of this design is to minimize the work
the needed markup (portlet text with HTML or XML/XSL needed to develop and maintain the Portal libraries being
tags) to display the specific portlet. But even the call to the developed. Specifically, this design requires less new code,
show() procedure can be made more generic, as you can see less unit testing, and consequently less risk.
in Listing 6.
The advantages of this overall code restructuring are Re-factoring existing code
perhaps best demonstrated by the process of adding a new The guidelines presented here for generalizing the portlet
portlet to a provider. Consider the individual steps that must subprograms should prove useful if an organization has
be taken when using the default design: yet to begin developing its Portal software, or if the Portal
1. Define a new global constant to represent the portlet ID. packages it has created already are small enough to justify
2. Write a new PL/SQL package for the portlet, containing re-factoring them along these lines. But an existing portal
five or more new subprograms. project whose size prohibits any such rewrite may still be
3. Add a new ELSIF clause to the provider’s deregister_ improved, assuming of course that the design mirrors that
portlet() procedure. of the PDK.
4. Add a new ELSIF clause to the provider’s get_ In that case, the common code in all of the portlet
packages can simply be moved into a
central portlet package, thereby
Listing 6. Generalized portlet function show().
eliminating the redundant instances of
PROCEDURE show( the identical portlet subprograms.
p_portlet_record IN wwpro_api_provider.portlet_runtime_record However, it may be tempting to
)
IS continue this consolidation too far,
BEGIN
IF ( NOT is_runnable(
by moving the common portlet
p_provider_id => p_portlet_record.provider_id, subprograms into the provider
p_reference_path => p_portlet_record.reference_path
) ) THEN package. This isn’t recommended,
RAISE wwpro_api_provider.PORTLET_SECURITY_EXCEPTION; because it violates the spirit of
ELSIF ( p_portlet_record.exec_mode != wwpro_api_provider.MODE_SHOW ) THEN
RAISE wwpro_api_provider.PORTLET_EXECUTION_EXCEPTION; separating the portlets from their
ELSE provider. In general, the provider
IF ( p_portlet_record.portlet_id = provider_after.PORTLET_1 ) THEN
show_portlet_1( and portlet subprograms are best
p_portlet_record => p_portlet_record
); kept physically separated, to reflect
ELSIF ( p_portlet_record.portlet_id = provider_after.PORTLET_2 ) THEN their logical separation. Developers
show_portlet_2(
p_portlet_record => p_portlet_record tasked to modify such Portal software
); could be confused by the presence of
ELSIF ( p_portlet_record.portlet_id = provider_after.PORTLET_3 ) THEN
show_portlet_3( portlet subprograms in provider
p_portlet_record => p_portlet_record
);
packages, and have difficulty locating
END IF; them initially.
END IF;
END show; Continues on page 16

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 13


Oracle
Professional

Using PLL Libraries with


Form Builder
Tom Reid
PLL libraries are an extremely useful and powerful way to of the library to attach. The whereabouts of libraries are
enhance Oracle Forms Builder capabilities. They’re essentially system-dependent, but when searching for attached libraries
a collection of custom-built utilities that can be bundled in at runtime Forms Builder first searches the current directory
with your Forms applications as and when needed to support and then the directories listed in the PATH environment
functionality that wouldn’t otherwise be available. Another variable. If you don’t already know the library name, use the
great thing about PLL libraries is that it’s possible to access Browse button to search for it; otherwise, enter its name and
their underlying source code. This means that if the click on the Attach button. Note that you shouldn’t specify a
functionality provided doesn’t quite match your needs, you library path when attaching libraries. Path names for library
can amend the code so that it does. In this article, Tom Reid attachments are stored internally and, as a result, aren’t
shows an example of how to go about doing this and also portable. Instead, you should specify the name of the library
how to create your own PLL libraries that your Forms and rely on the Forms Builder standard search path to locate
applications can share. your library at runtime. Once the library is attached you can
expand it and see what utilities it includes, the parameters

I
T’S quite possible that you’ll never need to use PLL required when calling them, and so on. From here on they
libraries. But, as your Forms development experience can be used pretty much as you would a regular built-in
continues and you start to develop more and more Forms function or procedure, as shown in the following
complex applications, it’s very likely that at some point the snippet that calls a PLL library procedure:
built-in Forms functions and procedures just won’t solve
the particular problem you happen to be working on. Or if BEGIN

they do, you might find that it involves greater complexity
/* pause for 5 seconds */
than you’d really like to implement. As a simple example, win_api_utility.sleep(5000);
consider a scenario where you want to suspend the running

of your Forms application for, say, five seconds before END;
continuing. At first sight you might turn to the PAUSE
built-in procedure, but that depends on the user pressing Can I edit PLL libraries?
a key in order to get things moving again. Another option You’ll find that the attached libraries are read-only and
would be to use a timer or an external program call, but therefore you won’t be able to edit them directly from within
that involves extra programming and complexity. Wouldn’t the Forms Builder application. However, there’s a way
it be great if someone had already written such a utility around this. Forms Builder has a file convert utility that
that you could simply link in with your application and call enables you to convert .PLL (library) files to their .PLD (text)
as required? That’s where PLL libraries come to the rescue. equivalents and vice versa. You’ll find this utility under the
In this particular example, the one we’re interested in is File | Administration | Convert… menu. Click on this and a
called D2KWUTIL.PLL. This library contains a whole host dialog box appears with three text fields to fill in. In the Type
of useful little Windows utilities, including one called text field, choose PL/SQL Library from the dropdown list. In
Win_Api_Utility.Sleep. This procedure takes as an argument the File text field, either type the name of or browse for the
the number of milliseconds for which the application is to PLL library file you wish to convert. In the Direction text
sleep—exactly what’s required. field, choose Binary To Text from the dropdown list. When
you now hit the Convert button, you should see that a .PLD
How do I use PLL libraries? file appears in the relevant directory. This file contains the
Before you can use the procedures and functions contained source code for the library and can be opened by a regular
in a PLL library, you have to attach it to your Forms module. text editor such as Notepad. If desired, you can now change
First, open a Forms module in the Object Navigator window, the functionality of the existing modules within the library
and then click on the Attached Libraries node and select the or even add new ones or delete old ones. I’d urge you,
Navigator | Create menu item. Next, you specify the name however, not to edit or change the original .PLL library files

14 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com


that came with your system unless absolutely necessary. It’s After attaching the PLL library as described previously, I
better to make a copy of the library and make changes to could simply call the write_block function as shown in
that copy instead. Listing 1.
If you find that you have a lot of PL/SQL code in
functions and procedures that are shared among many of
Listing 1. Calling the write_block function.
your Forms applications, consider creating your own PLL
library to make sharing the code easier. To do this, simply DECLARE
create a text file with a .PLD extension. Paste your functions ret_val number := 0;

and procedures code into this file and convert it to a .PLL BEGIN
library using the File | Administration | Convert… menu. ret_val :=
f50Write.WRITE_BLOCK(:system.current_block,
Now you can attach your newly created PLL library to your 'output.csv','w',TRUE,',','ALL',FALSE);
If ret_val < 0
Forms applications and make use of the procedures and Then
functions contained within it. Message('Write block failed,
please contact support');
End if;
A real-life example END;
To pull all the threads of this topic together, I’m going to take
you through the steps of a real-life example that I worked on I then incorporated this code as part of a menu
not long after I first started using Forms as a development icon, which made it accessible by every Forms module,
environment. The basic requirement was to allow the users meaning that it could be used to output any database
of the system the ability to dump the contents of multi- block in the entire application to a file. I think that’s pretty
record database blocks into a text file. When I looked into powerful stuff.
this I discovered that there were many ways to achieve this. Last but not least, you should be aware of a couple of
I could have used an ordinary SQL*Plus script, PL/SQL extra points. First, a useful side effect of being able to specify
with the TEXT_IO package, or created an external program the separator character is that if you make this a comma, you
such as embedded C. However, I happened across PLL instantly have a comma-separated file, which is great if you
libraries, and when I looked into them further I came across want to subsequently read the file into a spreadsheet like
a function in the F50UTIL library (I was using Forms version Excel. Second, the write_block function works for both
5 at the time) called f50write.write_block. This did exactly single-record and multi-record blocks, but only for database
what I required—namely, output a block of data to a file. blocks. In my own particular circumstance I needed it to
The function write_block is defined in the library thus: work for both data and control blocks; therefore, I had to
modify the code and use this as a model for my own block-
function write_block to-file function. I did this by first converting the F50Write
(
block_name in varchar2, PLL library to its PLD equivalent using the process I
output_file in varchar2 := 'output.lis',
output_mode in varchar2 := 'W', described previously. Next, I copied and pasted the
column_align in boolean := TRUE, write_block function into my own function and renamed it
sep_char in varchar2 := ' ',
rec_option in varchar2 := 'ALL', block_to_file. I then changed my new block_to_file function
displayed_only in boolean := FALSE
) return number;
as required and incorporated it into my Form module by
creating a new Program Unit and defining my block_to_file
where: function in the normal way.

• block_name is the name of the block to write. PLL library code compatibility
• output_file is the name of the output file. The default file The code shown in this article should work unchanged
name is ‘output.lis’. under all versions of Oracle 7 and 8 using Developer 2000/
• output_mode determines how the file is written to. The Forms 5. My understanding is that Forms6i was the last
valid values are ‘w’ for write and ‘a’ for append. version of Forms with which PLL libraries were fully
• column_align is a Boolean indicating whether the output integrated. Forms6i developers need to amend the code
should be column-aligned. snippets shown here to change the references F50UTIL and
• sep_char is the character(s) to use as column separators. f50write.write_block to F60UTIL and f60write.write_block,
The default value is a space character. respectively. Certain PLL libraries such as D2KWUTIL,
• rec_option specifies the records to write from the block. which are client/server-based, are unsupported in
Valid values are ‘ALL’, ‘VIEWED’, and ‘VISIBLE’. Developer9i, which is a totally Web-centric environment
• displayed_only is a Boolean indicating whether to write and may not work as expected, if at all.
only the displayed items or all of the items whether
they’re visible or not. The default value is FALSE, Summary
indicating that all items should be written. In this article I’ve shown you how you can enhance the

www.oracleprofessionalnewsletter.com Oracle Professional February 2003 15


capabilities of Form Builder by using PLL libraries. I also as it will take your Forms development skills to a new level
demonstrated how you can edit existing PLL libraries to of expertise. ▲
add, delete, or modify the functions and procedures they
contain or indeed create your own libraries if desired. It’s Tom Reid lives and works in Edinburgh, Scotland. He develops Oracle
well worth it to become proficient in using PLL libraries, systems for a large investment bank. oracle_tips@hotmail.com.

Oracle Portal Packages... easier to maintain and extend in the future. The benefits of
this approach are most evident when a new portlet needs to
Continued from page 13 be added to the portal. ▲
Conclusion
Michael J. Ross is a database software developer in San Diego. He’s also
Before development teams begin writing Oracle Portal code
the communications director and Webmaster of the San Diego Oracle
for their companies’ private and public Web sites, it’s critical
Users Group. www.ross.ws.
that they select in advance—or at least evolve toward—a
methodology that will avoid the unnecessary creation of Tom Scott is the owner of Scott Consulting, Inc., a San Diego-based
portlet packages. The design outlined in this article will consulting firm that specializes in Oracle design and implementation for
avoid individual packages for each portlet and minimize a variety of industries. He’s also the president of the San Diego Oracle
code overhead. In turn, the resulting Portal software will be Users Group. tom-s@pacbell.net.

February 2003 Downloads


• BRYN.ZIP—Source code to accompany Bryn Llewellyn Extensible Indexes, Part 2.”
and Steven Feuerstein’s article, “Building Your Own

For access to all current and archive content and source code, log in at
www.oracleprofessionalnewsletter.com with your unique subscriber user name and password. User name cement
For access to this issue’s Downloads only, click on the “Source Code” button, select the file(s)
you want from this issue, and enter the User name and Password at right when prompted. Password denim

Editor: Garry Chan (gchan@procaseconsulting.com) Oracle Professional (ISSN 1525-1756)


CEO & Publisher: Mark Ragan is published monthly (12 times per year) by:
Group Publisher: Connie Austin Pinnacle
Executive Editor: Farion Grove A division of Lawrence Ragan Communications, Inc.
Production Editor: Andrew McMillan 316 N. Michigan Ave., Suite 400
Chicago, IL 60601
Questions?
POSTMASTER: Send address changes to Lawrence Ragan Communications, Inc., 316
Customer Service: N. Michigan Ave., Suite 400, Chicago, IL 60601.

Phone: 800-493-4867 x.4209 or 312-960-4100


Copyright © 2003 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
Editorial: FarionG@Ragan.com of America.

Advertising: HowardF@Ragan.com Oracle, Oracle 8i, Oracle 9i, PL/SQL, and SQL*Plus are trademarks or registered trademarks of
Oracle Corporation. Other brand and product names are trademarks or registered trademarks
Pinnacle Web Site: www.pinnaclepublishing.com of their respective holders. Oracle Professional is an independent publication not affiliated
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
United States: One year (12 issues): $229; two years (24 issues): $389 applications. This publication is sold as is, without warranty of any kind, either express or
Canada:* One year: $249; two years: $423 implied, respecting the contents of this publication, including but not limited to implied
warranties for the publication, performance, quality, merchantability, or fitness for any particular
Other:* One year: $254; two years: $432 purpose. Lawrence Ragan Communications, Inc., shall not be liable to the purchaser or any
other person or entity with respect to any liability, loss, or damage caused or alleged to be
Single issue rate: caused directly or indirectly by this publication. Articles published in Oracle Professional
$27.50 ($30 in Canada; $32.50 outside North America)* 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.

16 Oracle Professional February 2003 www.oracleprofessionalnewsletter.com

You might also like