ArsDigita Archives
 
 
   
 
spacer

Oracle Driver

for AOLserver, part of ArsDigita Free Tools

This page was last updated on 2000-09-11.


Note: If you are running version 1.0.2 of the Oracle driver, it is strongly recommended that you upgrade to at least version 2.1 to prevent unexpected error conditions. (See Version History or the README for more details.)

This product lets you use our favorite Web server, AOLserver, with the Oracle RDBMS. If you want to see whether it works, use the database-backed services at photo.net. This is backed by Oracle 8 and handles several queries/second at all times.

Our driver is free but making it work requires some C and Unix expertise. Basically you have to have all the right Oracle libraries on the computer where you are running AOLserver. In practice, we install the complete Oracle RDBMS on the same machine as AOLserver and then all the right stuff is there. Anyway, then you have to compile the driver and make sure that AOLserver runs with the right environment variables set. If this all sounds too hard, then you can hire us to install it on your machine and support you for one year.

Support for special Oracle features

Transactions

Like all decent database products, Oracle supports transactions (a number of inserts, deletes, and updates that happen as a unit, or not at all). Check out the Transactions chapter of SQL for Web Nerds for details on the concept.

To support transactions in the Oracle driver, we hijack ns_db dml and add three new commands:

  • begin transaction - DML statements after this one are now within a transaction
  • end transaction - commit the changes made during the transaction
  • abort transaction - roll back the transaction. Any DML statements made since the begin transaction will be undone, as if they never happened.
For example:
ns_db dml $db "begin transaction"
ns_db dml $db "insert into users (user_id, email) values (1234, 'markd@arsdigita.com')"
ns_db dml $db "update user_contact_info set aim_name='alex samoyed' where user_id=2342"
ns_db dml $db "delete from user_group_map where group_id=55"
ns_db dml $db "end transaction"
This will affect the contents of three tables once the end transaction statement is executed.

You can combine Tcl catch statements and transaction control:

ns_db dml $db "begin transaction"

if [catch { ns_db dml $db "insert into ..." 
            ns_db dml $db "update ..." 
	    ns_db dml $db "end transaction" } errmsg] {
    ns_db dml $db "abort transaction"
    # perhaps write an error back to the user
    return
} else {
    # tell the user the transaction was successful
}
This will roll back the transaction if an error happened during the processing of the insert and update. If they finished succesfully, commit the transaction.

In the event of problems you should explicitly abort the transaction rather than letting the page exit do the rollback. If you check for anything that was done in the first (successful) part of the transaction, it will give the NEW value. In the following example, a situation where the first insert worked but the second failed. The 'double-click' check of getting the group count, causing the script to try the redirect - only to find there actually is no group number because moving off the current page rolled back the creation of the group.

if [catch { ns_db dml $db "begin transaction"
            ns_db dml $db $insert_for_user_groups
            if [info exists insert_for_helper_table] {
                ns_db dml $db $insert_for_helper_table
            } else {
                set $insert_for_helper_table ""
            }
            ns_db dml $db "end transaction" } errmsg] {
                # something went wrong
		# !!! note that this count(*) is picking up the group
		# !!! inserted inside of the transaction
                if { [database_to_tcl_string $db "select count(*) from user_gr\
oups where group_id = $group_id"] > 0 } {
                    # group was already in database
                    ns_returnredirect "group.tcl?group_id=$group_id"

Inserting an "abort transaction" call before the if statement fixed this.

Note that only DML statements (insert, update, delete) are affected by transactions. DDL statements (create table, etc.) are not affected by transactions. You can't roll back a table creation; to leave the database in a consistant state after a failed transaction, you must wrap the transaction in a "catch" that explicitly drops the table upon error.

CLOBs and BLOBs

Oracle lets you have character columns with up to 2 GB of data. But their SQL parser can only handle string literals up to 4000 characters long. So we have some special calls for stuffing long strings into columns of type CLOB, and for stuffing the contents of files into columns of type CLOB or BLOB.

ns_ora clob_dml db query clob_value_1 clob_value_2 ... clob_value_N
The query must insert or update empty CLOBs into CLOB columns and return those columns into bind variables using RETURNING (but see below for an alternate way to use bind variables with the clob_* and blob_* commands). The Oracle function "empty_clob()" returns an empty CLOB. The clob_dml command inserts the Nth clob_value into the Nth bind variable. The bind variable names are not significant. Note that you don't have to escape apostrophes in the clob values. For example,
create table messages (
    message_id	integer primary key,
    subject     varchar(200),
    body        clob
);

ns_ora clob_dml $db "insert into messages (message_id, subject, body)
values (34, 'This is a test', empty_clob())
returning body into :1" "Here's a long body ... to test clob inserts"
This would insert a new row into the table messages with message_id 34, subject "This is a test" and body "Here's a long ... clob inserts". The clob_dml command also works with updates.
create table personal_ads (
    person_id		integer primary key,
    self_desc	        clob,
    bondage_story	clob,
    perfect_mate_desc	clob
);

ns_ora clob_dml $db "update personal_ads 
set self_desc = empty_clob(), 
    bondage_story = empty_clob(),
    perfect_mate_desc = empty_clob()
where person_id = 96
returning self_desc, bondage_story, perfect_mate_desc
into :one, :two, :three" $new_self_desc $new_bondage_story $new_perfect_mate_desc
This updates the row where person_id = 96, setting the three CLOB columns to the values of the tcl variables $new_self_desc, $new_bondage_story, $new_perfect_mate_desc. If there were multiple rows with person_id = 96, each row is updated with the new CLOB values.

Inserting the contents of files into CLOBs or BLOBs is similar to the use of the clob_dml command. (Note that you can use ns_queryget to get the file name of data uploaded via multipart formdata. You will probabily also want to view this lecture by Philip Greenspun on how to do file upload with AOLserver.)

create table music_archive (
    album_id        integer primary key,
    title           varchar(200),
    mp3             blob
);

ns_ora blob_dml_file $db "insert into music_archive (album_id, title, mp3)
values (42, 'Old Skull''s Greatest Hits', empty_blob())
returning mp3 into :1" /net/downloads/getoutofschool.mp3
But here, instead of supplying a string to insert into the column, you're specifying the path to a file. The Oracle driver will read the contents of the file and insert it into the database. Why have this seperate mechanism? AOLserver uses Tcl 7.4 which can't handle embedded null-characters in its variables, so there's no way through the Tcl API to read a binary file without the data getting munged.
ns_ora blob_dml_file $db "update music_archive
set mp3 = empty_blob()
where album_id = 42
returning mp3 into :1" /tmp/barrymanilow.mp3
This updates the row where album_id = 42, setting the BLOB column to the contents of /tmp/barrymanilow.mp3. If there were multiple rows with album_id = 42, each row is updated with the new BLOB value.
ns_ora blob_get_file $db "select mp3 from music_archive
where album_id = 42" /tmp/something.mp3
This writes the result of the query, which should return only one column from one row, into /tmp/something.mp3.
ns_ora write_blob $db "select mp3 from music_archive
where album_id = 42"
This returns the contents of the mp3 column to the connection. Note that you must first write appropriate headers, e.g.,
ReturnHeaders audio/mpeg
ns_ora write_blob $db "select mp3 ..."
ReturnHeaders is defined in the standard ArsDigita utlities.tcl file, available from http://eveander.com/arsdigita/books/panda/utilities.txt.

Calling PL/SQL Functions

You can call PL/SQL functions as normal dml statements. For example,
ns_db dml $db "declare begin bboard_update_index_column('$new_id'); end;"
To execute a PL/SQL function that returns a value, select it from table dual. For example,
ns_db select $db "select get_member_id('Joe', 'Smith') from dual"
But this will not work if the PL/SQL function has side effects (inserts or updates). When the PL/SQL function has side effects, you must use the ns_ora exec_plsql command.
ns_ora exec_plsql $db "begin :1 := get_new_member_id('Cotton', 'Seed'); end;"
The ns_ora exec_plsql command returns the value of the first bind variable in the PL/SQL statement. The name of the bind variable is not significant.

Bind variables

To support using bind variables, we provide some additional ns_ora calls.
  • ns_ora select dbhandle ?-bind set? sql ?arg1 ... argn?
  • ns_ora 0or1row dbhandle ?-bind set? sql ?arg1 ... argn?
  • ns_ora 1row dbhandle ?-bind set? sql ?arg1 ... argn?
  • ns_ora dml dbhandle ?-bind set? sql ?arg1 ... argn?
  • ns_ora array_dml dbhandle ?-bind set? sql ?arg1 ... argn?
  • ns_ora clob_dml_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora blob_dml_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora clob_dml_file_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora blob_dml_file_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora exec_plsql_bind dbhandle sql return_var ?arg1 ... argn?

With the exception of the four lob_dml calls and exec_plsql_bind, these work the same as the corresponding ns_db calls, except that you can use bind variables in your SQL. There are three ways to specify the bind variables:

  • As positional parameters, e.g. ":1". The value for the bind variable is taken from the arguments after the SQL argument in order. ":1" is the first argument after the SQL, ":2" is the second, and so on.
  • As Tcl variable values, e.g. ":min_value". The value for the bind variable is taken from the local Tcl environment, so in this example it would be the value of the variable "min_value".
  • As elements of an ns_set, e.g., ":min_value" (only if the -bind argument is specified). The value for the bind variable is the element named "min_value" in the set with ID set.
Examples:
ns_ora dml $db "update users set last_name = :1
    where user_id = :2" "Gates" $user_id

set values [ns_set create]
ns_set put $values last_name "Gates"
ns_set put $values user_id   $user_id
ns_ora dml $db -bind $values "update users set last_name = :last_name
    where user_id = :user_id"    

set user_email "billg@microsoft.com"
set selection [ns_ora 0or1row $db "select first_names,
    last_name from users where email = :user_email"]
With clob_dml_bind, blob_dml_bind, clob_dml_file_bind and blob_dml_file_bind, an extra argument is required. The list_of_lob_vars argument is a list of bind variables that represent LOB arguments. This is required because there is no way to tell in advance from examining the SQL or the Tcl variables which arguments are a LOB type, yet Oracle requires the driver to specify the LOB type of the arguments before executing the SQL. The solution is for the programmer to pass that information in along with the variables. Examples:
ns_ora clob_dml_bind $db "update manuscripts set text = :1
    where manuscript_id = :2" [list 1] $gettysburg_address $man_id

set gettysburg_address "Four score and seven years ago"
set man_id 1861
ns_ora clob_dml_bind $db "update manuscripts set text = :gettysburg_address
    where manuscript_id = :man_id" [list gettysburg_address]
The exec_plsql_bind call has a similar problem: it needs to be told which bind variable is the return value of the function. The value of the bind variable named in the return_var argument will be returned as the value of the exec_plsql_bind call. If it is a named variable and not a positional variable, then the variable's value will also be set. Examples:
ns_ora exec_plsql_bind $db "begin :1 := translate_to_german('Four score'); end;" 1

set english "Four score and seven years ago"
ns_ora exec_plsql_bind $db "begin :deutsch := translate_to_german(:english); end;" deutsch

Array DML

Array DML works exactly like single-row DML, except that the statement is executed multiple times with distinct data sets. This is useful when inserting many similarly-structured items into the database. To use array DML, set the value of each bind variable to be a list rather than a single value, e.g.:
set last_names [list Gates Ellison Jobs]
set first_names [list Bill Larry Steve]
set user_ids [list 666 816 1984]

ns_ora array_dml $db "
    update users
    set last_name = :last_names, first_name = :first_names
    where user_id = :user_ids
"

# Or, equivalently:

ns_ora array_dml $db "
    update users
    set last_name = :1, first_name = :2
    where user_id = :3
" $last_names $first_names $user_ids

# Or, equivalently:

ns_set values [ns_set create]
ns_set put $values last_names $last_names
ns_set put $values first_names $first_names
ns_set put $values user_ids $user_ids
ns_ora array_dml $db -bind $values "
    update users
    set last_name = :last_names, first_name = :first_names
    where user_id = :user_ids
"
Note that the statement is prepared (i.e., parsed) by Oracle only once, so there is much less overhead and far fewer round trips to the server.

Extra AOLserver Tcl API calls

AOLserver does not provide an ns_db abstraction for some things that it should. Consequently, when talking to Illustra, they have ns_ill calls. We were forced to provide ns_ora calls. Here they are...
  • ns_ora resultid dbhandle -- returns the rowid (Oracle hidden column) of the last object affected by a DML command. This rowid can be used to determine the rowid for a newly-inserted row. Note that this is currently not implemented.

  • ns_ora resultrows dbhandle -- returns the number of rows affected by the last DML command. It can be used to determine how many rows were deleted or updated.

  • ns_ora exec_plsql dbhandle sql -- executes the given PL/SQL statement, returing the value of the first bind variable in the statement.

  • ns_ora clob_dml dbhandle sql clob_value_1 ?clob_value 2 ... clob_value_N?
  • ns_ora blob_dml dbhandle sql blob_value_1 ?blob_value 2 ... blob_value_N?
    Evaluates the given sql statement, inserting the the given values into the columns specified by the bind variables referenced.

  • ns_ora clob_dml_file dbhandle sql path1 ?path2 ... pathN?
  • ns_ora blob_dml_file dbhandle sql path1 ?path2 ... pathN?
    Evaluates the given sql statement, inserting the contents of the given files into the columns as specified by the bind variables referenced.

  • ns_ora clob_get_file dbhandle sql path
  • ns_ora blob_get_file dbhandle sql path
    Evaluates the given sql statement (which should return just one column from one row) and writes the value to the specified file. (any previous file contents will be replaced by the new value) The caller is responsible for deleting the file.

  • ns_ora write_clob dbhandle sql ?nbytes?
  • ns_ora write_blob dbhandle sql ?nbytes?
    Evaluates the given sql statement (which should return just one column from one row) and returns the value to the connection. You can specify the number of bytes to be returned in the nbytes argument. By default the entire BLOB/CLOB is returned.

  • ns_ora select dbhandle sql ?arg1 ... argn?
  • ns_ora 0or1row dbhandle sql ?arg1 ... argn?
  • ns_ora 1row dbhandle sql ?arg1 ... argn?
  • ns_ora dml dbhandle sql ?arg1 ... argn?
  • ns_ora array_dml dbhandle sql ?arg1 ... argn?
  • ns_ora clob_dml_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora blob_dml_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora clob_dml_file_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora blob_dml_file_bind dbhandle sql list_of_lob_vars ?clob_value_1 clob_value_2 ... clob_value_N?
  • ns_ora exec_plsql_bind dbhandle sql return_var ?arg1 ... argn?
    These are described above under the Bind Variables section.

Where's the code?

The code is available for download in the ACS Repository.

Where are more docs?

http://www.aolserver.com/doc/3.0/driver/dr.htm

How do I Report Problems or suggest enhancements?

Email markd@arsdigita.com, or visit the Oracle Driver Project on the photo.net ticket tracker.

Does it work with 7.3?

Depends what you mean by "work". We don't have any production AOLserver/Oracle 7.3 systems live on the Web. By our definition, "work" means "publicly accessible on the Internet and handling at least 10 hits/second". So we can't guarantee that you will download a tar file from us and be happy without editing the C code. In fact, Oracle completely changed their C interface between Release 7 and 8. So you can't just take our latest Oracle 8 driver and win. Anyway, if you're a C wizard you can probably make our driver work in a day. If you're not, hire a C wizard! If you can't hack C and you're too poor to pay a C wizard, then you probably should be running Solid (see http://demo.webho.com) instead of Oracle.

Weird stuff to remember

Make sure to set the AOLserver StackSize parameter to something reasonable, e.g., 500000, if you're using our driver and any recursion in Tcl. The installation instruction page for the ArsDigita Community System covers this. Basically the problem is that AOLserver only allocates a stack by default of 50,000 bytes. Cotton Seed wrote the driver initially to stack-allocate 40,000 bytes to handle error messages. I guess this is how C programmers deal with the fact that they don't have a real computer language like Lisp that can allocate appropriate-sized data structures on the fly. Anyway, that only leaves 10,000 bytes for Tcl recursion, which might not be enough. Here's an example of bumping up the stack size:
[ns/parameters]
User=nsadmin
Home=/home/nsadmin
StackSize=500000
There are also a number of moving pieces in the AOLserver database configuration. The one that seems to give folks the most trouble is the DataSource parameter in the pool configuration. It should look something like this:
[ns/db/pool/poolname]
Driver=ora8
Connections=4
DataSource=
User=alex
Password=woofwoof
Verbose=On
ExtendedTableInfo=On
DataSource must be included, and it should be blank. You may want to take a peek at a sample database configuration section.

Be sure to set your Oracle environment variables. Take a look at our sample AOLserver/Oracle startup script

If you're wanting to run AOLserver on one machine and Oracle on another, then check out this thread on photo.net. We don't use this configuration ourselves so we haven't tested it.

HPUX11 and Oracle 8.1.5

There is a problem with this driver under HPUX11 with Oracle 8.1.5 (8i). The Oracle driver must link against libclntsh.sl, the Oracle shared client library. This library links against libcl and libpthread, which contain "thread local storage". As documented in the man page for shl_load(), libraries containing thread local storage cannot be loaded using shl_load().

The workaround is to generate a version of libclntsh.sl without libcl and libpthread, and cause the Oracle driver to link against that.

Here is a binary version of the 2.0 Oracle driver and libclntsh.sl compiled on HPUX 11 with Oracle 8.1.5 and AOLserver 2.3.3 for HPUX11. Place the files in the bin directory into your AOLserver's bin directory and read the README.txt file for instructions on how to use, and how to generate your own version of these files if necessary.

Version History

  • first version released June 1997

  • 0.6: Released January 13, 1999. Adds lots of comments stuck in by philg over Cotton's vociferous objections. Fixes the bug where driver wasn't allocating enough bytes to hold a very precise average or date computation result, e.g., when getting avg(some_integer_column)

  • 1.0: Released March 12, 1999.
    • Fixed corrupted return data for LONG columns < 1024 bytes. LONG columns over 1024 bytes are not supported.
    • Fixed complaint when inserting an empty string via clob_dml. It now inserts a NULL value in this case
    • Added clob_dml_file and clob_get_file commands
    • Added write_clob command
    • Added blob versions of all of the clob commands
    • Added safety features for Cassandracle
    • Added the debug configuration parameter, so now getting trace output doesn't require recompiling the driver
    • clob_dml SQL and the CLOB contents are now logged to the server.log when verbose=on is set in the pool's configuration
    • Rewrote makefiles so the same makefile works on Solaris, HP-UX 11.00, and Linux

  • 1.0.1: Released April 5, 1999. Fixed clob_get_file so it won't leak open files. (fix donated by Igor Baikalov - thanks!)

  • 1.0.2: Released April 14, 1999. Added display of Oracle's warning messages when a result code of OCI_SUCCESS_WITH_INFO is returned (which can happen in cases such as having NULL values in aggregate functions)

  • 1.0.3: Released June 11, 1999. Turned OCI_SUCCESS_WITH_INFO into a non-error condition. Oracle changed behavior between 8.0.X and 8.1.X, causing the use of aggregate functions with NULLs to generate a warning. Since NULLs in aggregates usually aren't a problem, we recommend upgrading to this version from 1.0.2

  • 2.0: Released January 6, 2000
    • Fixed error where *lob_get_file would leave temporary files in /var/tmp.
    • Optimized *lob_get_file to return directly to the connection rather than spooling from the file system.
    • It's now easier to see what version of the driver you are running. Perform a "strings ora8.so | grep ArsDigita" to get the version number.
    • Fixed ora_open_db to return a better error than "NULL Connection" if it fails to connect to Oracle.
    • Fixed a number of errors found by Jin Choi (jsc@arsdigita.com)
    • Added cleanup if the oracle process dies unexpectedly (which would ultimately lead to the "hostdef extension doesn't exist" error.
    • Added cleanup of the oracle process if a "too many oracle processes" (ORA-00020) or "oracle not available" (ORA-01034) error occurs.
    • Added in reimplementations of some functions that were removed from AOLserver 3.0 but are needed by ACS.
    • The oracle 8.1.X makefile is now the default makefile. If you're still using Oracle 8.0.X, you'll need to use the makefile-80: gmake -f makefile-80

  • 2.0.1: Release January 14, 2000
    • Fixed linking problem on Linux.
      (if you get an "undefined symbol fstat" error when loading the driver, you need this version)

  • 2.1: Release May 1, 2000
    • Plugged some potential buffer overflow situations when reporting errors.
    • Added bind variable support.
    • The error message for certain SQL errors now indicates the location in the SQL of the error. (contributed by H.B. Weinberg)
    • You can now use ns_dberrorcode to get the numeric Oracle error code after an error.
    • We no longer put huge arrays on the stack, so you shouldn't need to turn up your AOLserver stack size so much any more.
    • Fixed some memory-allocation problems in bind variable support (we were not calling Ns_StrDup in all the right places).
    • Expanded bind-variable support.
    • Added array DML.
    • Fixed bug when reading partial LOBs.
    • Added support for building on Win32 platforms.

  • 2.2: Release July 28, 2000
    • Fixed a problem with EPIPE handling in stream_write_lob. (Contributed by Paul Laferriere.)
    • Fixed a problem with handling UTF-8 data in LOBs. (Contributed by Jin Choi.)
    • Added clob_dml_bind, clob_dml_file_bind, blob_dml_bind, blob_dml_file_bind, exec_plsql_bind functions.

  • 2.3: Release 2000-09-11
    • New Makefile based on AOLserver 3 Makefiles.
    • Beginnings of a code reorganization.
    • Eliminated a tiny memory leak in the [bc]lob_dml* commands.

markd@arsdigita.com
spacer