Sibyl
Sibyl is an OCI-based interface between Rust applications and Oracle databases.
Example
Blocking Mode
use sibyl as oracle; // pun intended :)
Nonblocking Mode
use sibyl as oracle;
async
Note that:
- The nonblocking mode example is almost a verbatim copy of the blocking mode example (see above) with
awaits added.- The example below uses and depends on Tokio
- For the moment, Sibyl can use only Tokio as an async runtime.
Notes on Building
Sibyl needs an installed Oracle client in order to link either OCI.DLL on Windows or libclntsh.so on Linux. The cargo build needs to know where that library is. You can supply that information via environment variable OCI_LIB_DIR on Windows or LIBRARY_PATH on Linux. On Linux LIBRARY_PATH would include the path to the lib directory with libclntsh.so. For example, you might build Sibyl's example as:
LIBRARY_PATH=/usr/lib/oracle/19.13/client64/lib
On Windows the process is similar if the target environment is gnu. The OCI_LIB_DIR would point to the directory with oci.dll:
set OCI_LIB_DIR=%ORACLE_HOME%\bin
cargo build --examples --features=blocking
However, for msvc environment the OCI_LIB_DIR must point to the directory with oci.lib. For example, you might build that example as:
set OCI_LIB_DIR=%ORACLE_HOME%\oci\lib\msvc
cargo build --examples --features=blocking
Note that Sibyl has 2 features -
blockingandnonblocking. They are exclusive and one must be explictly selected. Thus, when Sibyl is used as a dependency it might be included as:
[]
= { = "0.5", = "blocking" }
Usage
Environment
The OCI environment handle must be created before any other OCI function can be called. While there can be many environments - for example, one can create an environment per connection - usually one is enought. Sibyl initializes it to be the most compatible with Rust requirements - thread-safe using UTF8 character encoding. That single environment handle can be created in main and then passed around:
Note however that some functions will need a direct reference to this handle, so instead of passing it around some applications might prefer to create it statically:
use Environment;
use lazy_static;
lazy_static!
Then later one would be able to create, for example, a current timestamp as:
use TimestampTZ;
let current_timestamp = from_systimestamp?;
Connections
Use Environment::connect method to connect to a database:
Where dbname can be any name that is acceptable to Oracle clients - from local TNS name to EZConnect identifier to a connect descriptor.
SQL Statement Execution
All SQL or PL/SQL statements must be prepared before they can be executed:
let stmt = session.prepare?;
A prepared statement can be executed either with the query or execute or execute_into methods:
queryis used forSELECTstatements. In fact, Sibyl will complain if you try toqueryany other statement.executeis used for all other, non-SELECT, DML and DDL that do not have OUT parameters.execute_intois used with DML that have OUT parameters.
query and execute take a tuple of IN arguments, which can be specified as positional arguments or as name-value tuples. For example, to execute the above SELECT we can call query using a positional argument as:
let rows = stmt.query?;
or bind a value to :id by name as:
let rows = stmt.query?;
In most cases which binding style to use is a matter of convenience and/or personal preferences. However, in some cases named arguments would be preferable and less ambiguous. For example, statement might change during development and thus force the change in argument positions. Also SQL and PL/SQL statements have different interpretation of a parameter position. SQL statements create positions for every parameter but allow a single argument to be used for the primary parameter and all its duplicares. PL/SQL on the other hand creates positions for unique parameter names and this might make positioning arguments correctly a bit awkward when there is more than one "duplicate" name in a statement.
Note one caveat - until min_specialization is stabilized Sibyl has no way to distinguish whether a 2-item tuple is used to pass a named argument or 2 positional arguments. At the moment you'll have to use a 3-item tuple with a unit type as the last item when you are passing 2 positional arguments. The unit type is treated as "nothing", so effectively only first 2 arguments are used. For example:
let stmt = session.prepare?;
let rows = stmt.query?;
execute_into allows execution of statements with OUT (or INOUT) parameters. For example:
let stmt = session.prepare?;
let mut department_id: u32 = 0;
let num_inserted = stmt.execute?;
execute and execute_into return the number of rows affected by the statement. query returns what is colloquially called a "streaming iterator" which is typically iterated using while. For example (continuing the SELECT example from above):
let mut employees = new;
let stmt = session.prepare?;
let rows = stmt.query?;
while let Some = rows.next?
There are a few notable points of interest in the last example:
- Sibyl uses 0-based column indexing in a projection.
- Column value is returned as an
Option. However, if a column is declared asNOT NULL, likeEMPLOYEE_IDandLAST_NAME, the result will always beSomeand therefore can be safely unwrapped. LAST_NAMEandFIRST_NAMEare retrieved as&str. This is fast as they are borrowed directly from the respective column buffers. However, those values will only be valid during the lifetime of the row. If the value needs to continue to exist beyond the lifetime of a row, it should be retrieved as aString.
Note that while Sibyl expects 0-based indexes to reference projection columns, it also accepts column names. Thus, the row processing loop of the previous example can be written as:
while let Some = rows.next?
Oracle Data Types
Sibyl provides API to access several Oracle native data types.
Number
use Number;
let oracle = env?;
let pi = pi;
let two = from_int;
let two_pi = pi.mul?;
let h = from_string?;
let hbar = h.div?;
assert_eq!;
Date
use Date;
let mar28_1996 = from_string?;
let next_monday = mar28_1996.next_week_day?;
assert_eq!;
Timestamp
There are 3 types of timestamps:
Timestampwhich is equivalent to Oracle's TIMESTAMP,TimestampTZ- for TIMESTAMP WITH TIME ZONE, andTimestampLTZ- for TIMESTAMP WITH LOCAL TIME ZONE
use TimestampTZ;
let ts = from_string?;
assert_eq!;
Note that if you are getting
ORA-01805when timestamp with time zone is used, then most likely your local client and the server it is connected to are using different versions of the time zone file. This stackoverflow answer should help you in setting up your local client with the correct time zone file.
Interval
There are 2 types of intervals:
IntervalYMwhich is eqivalent to Oracle's INTERVAL YEAR TO MONTH,IntervalDS- INTERVAL DAY TO SECOND
use ;
let launch = with_date_and_time?;
let landing = with_date_and_time?;
let duration : IntervalDS = landing.subtract?;
assert_eq!;
RowID
Oracle ROWID can be selected and retrieved explicitly into an instance of the RowID. However, one interesting case is SELECT FOR UPDATE queries where Oracle returns ROWIDs implicitly. Those can be retrieved using Row::get_rowid method.
let stmt = session.prepare?;
let rows = stmt.query?;
let row = rows.next?.unwrap;
let rowid = row.rowid?;
let manager_id: u32 = row.get?.unwrap;
assert_eq!;
let stmt = session.prepare?;
let num_updated = stmt.execute?;
assert_eq!;
Cursors
Cursors can be returned explicitly:
let stmt = session.prepare?;
let mut cursor = new?;
stmt.execute_into?;
let rows = cursor.rows?;
// ...
Or, beginning with Oracle 12.1, implicitly:
let stmt = session.prepare?;
stmt.execute?;
if let Some = stmt.next_result?
CLOBs, BLOBs, BFILEs
Let's assume a table was created:
(
id NUMBER GENERATED ALWAYS AS IDENTITY,
bin BLOB
);
We can then create and write data into that LOB as:
// ... create OCI environment, connect to the database, etc.
let file = new?;
file.set_file_name?;
let file_len = file.len.await?;
file.open_file.await?;
let mut data = Vecnew;
let num_read = file.read.await?;
file.close_file.await?;
// ... or do not close now as it will be closed
// automatically when `file` goes out of scope
// Insert new BLOB and lock its row
let stmt = session.prepare.await?;
let mut lob = BLOBnew?;
stmt.execute_into.await?;
lob.open.await?;
let num_bytes_written = lob.write.await?;
lob.close.await?;
session.commit.await?;
And then later it could be read as:
let id: usize = 1234; // assume it was retrieved from somewhere...
let stmt = session.prepare.await?;
let rows = stmt.query.await?;
if let Some = rows.next.await?
Where read_blob could be this:
async
Testing
Some of Sibyl's tests connect to the database and expect certain objects to exist in it and certain privileges granted:
- At least the HR demo schema should be installed.
- While there is no need to install other demo schemas at least
MEDIA_DIRshould be created (see$ORACLE_HOME/demo/schema/mk_dir.sql) and point to the directory with demo files that can be found inproduct_mediain the db-sample-schemas.zip. - Some of the LOB tests need text files with the the expected content. Those can be found in
etc/mediaand copied intoMEDIA_DIR. - A test user should be created. That user needs acccess to the HR schema and to the
MEDIA_DIRdirectory. Seeetc/create_sandbox.sqlfor an example of how it can be accomplished. - Tests that connect to the database use environment variables - DBNAME, DBUSER and DBPASS - to identify the database, user and password respectively. These variables should be set before executing
cargo test.
Supported Clients
The minimal supported client is 12.2 as Sibyl uses some API functions that are not available in earlier clients. While suporting those is definitely feasible, it was not a priority.
Sibyl tests are routinely executed on x64 Linux with Instant Clients 12.2, 18.5, 19.13 and 21.4 that connect to the 19.3 database. Sibyl is also tested on x64 Windows the with Instant Client 19.12.
Known Issues with Some Clients
SessionPool::session_max_use_count and SessionPool::set_session_max_use_count will fail on 12.2 client with ORA-24315: illegal attribute type.
Client 21.4 (at least with 19.3 database) is strangely picky about names of parameter placeholders for LOB columns. For example, if a table was created with the following LOB column:
(
id NUMBER GENERATED ALWAYS AS IDENTITY,
txt CLOB
);
and if an SQL parameter name is the same as the LOB column name (as in this example):
let stmt = session.prepare?;
Then 21.4 client will fail executing this SQL with ORA-03120: two-task conversion routine: integer overflow. Renaming the parameter placeholder resolves this:
let stmt = session.prepare?;
21.4 also does not "like" some specific parameter names like :NAME which makes it fail with the same ORA-03120.
Note that 12.2 through 19.13 clients (as far as Sibyl's tests showed) do not exhibit this issue.
21.4 client (at least when it is connected to the 19.3 database) cannot read CLOBs piece-wize - something bad happens in OCILobRead2 as it reads the last piece and the process gets killed. 21.4 client has no issues executing piece-wise reads from BFILEs and BLOBs.
Limitations
At this time Sibyl provides only the most commonly needed means to interface with the Oracle database. Some of the missing features are:
- Array interface for multi-row operations
- User defined data types
- PL/SQL collections and tables
- Objects
- JSON data
- LDAP and proxy authentications
- Global transactions
- High Availability
- Continuous query and publish-subscribe notifications
- Advanced queuing
- Shards
- Direct path load
Some of these features might be added in the upcoming releases if the need arises or if they are explicitly requested. Some, however, will never be implemented. The latter category includes those that are incompatible with nonblocking execution.