Using Oracle db26ai from Rust with the sibyl crate
Part 1: Environment setup and queries

WARNING:
This article shares many parts and its structure with the one about Using Oracle from Rust with
oracle, because I wanted both to be self-contained. You can read them independently, one after the other or use them to compare what they can offer to you.If you have already set up your database instance using the other article, you are good to go. Jump straight to the "First 'contact' with the Oracle DB instance" section.
In this second article about using Oracle AI db26ai from Rust, I will use the sibyl crate. According to the history of
its Git repo, it was created in 2019 by Alexander Demenchuk. While it offers fewer options for parsing the contents of
the rows and doesn't implement the Iterator trait to iterate through them, it does provide both synchronous and
asynchronous interfaces and supports several runtimes. I will use the non-blocking implementation with the tokio
async run-time for the examples, but I will await for each call and get no real concurrency. The purpose is just to
share how to use the asynchronous implementation.
I will use the same setup as in the previous article, making no assumptions about your development environment, so I will share how to set it up using some "real" data from the database we can use for the examples.
Let's get cracking!
Install the driver and SQLcl
An Oracle DB instance
The whole purpose of this article is to interact with an Oracle database, to query, create, update, and modify it, so we will need to have access to an instance. I recommend using a container because we will add a schema and some data. If you spin up an instance of the Oracle AI Database 26ai Free Container, it will start in a clean state, and you can discard the data after using all the code in this article. I have already explained how to run Oracle AI db26ai in a container in this other article, in case you haven't done so yet.
Required dynamic libraries
In this article, I will use the sibyl crate, which is based on Oracle Call Interface (OCI). That means that the OCI dynamic libraries must be installed and available on your system. In case you don't have them yet, they are available in a package that can be downloaded in two versions: basic and basic light. They both contain all the drivers, but the latter only includes the English error messages and support for some character sets. I'll use the former, but feel free to do otherwise if you don't need the extra data.
Double-click on the dmg file that you have downloaded from the link above, or use the following command with the
right version identifier.
hdiutil mount instantclient-basic-macos.arm64-<version id>.dmgThen, install its contents from the command line.
cd /Volumes/instantclient-basic-macos.arm64-<version id>/
sh ./install_ic.sh
However, the script will fail in Tahoe (macOS 26) and show a pop-up because macOS cannot assess the safety of this code.
Open the System settings, and select Privacy & Security from the left pane. Scroll down and allow this script to
run. Execute the script again, and it will create the directory ~/Downloads/instantclient_23_3 and copy its contents
there. I moved its contents to ~/bin/oracle because I don't want the libraries I use to be in my downloads directory,
and I added that directory to the dynamic linker path so they can be used by the crate.
export DYLD_LIBRARY_PATH=~/bin/oracleYou also need to export that path to the Rust compiler so it can find the OCI libraries.
export RUSTFLAGS="-L/Users/<your_user>/bin/oracle"Oracle DB command-line interface
It is also convenient to have a tool to talk to our Oracle DB instance. SQL*Plus has been the traditional go-to Oracle
client for a very long time, and you can download it from the same place and install it using the same process that I
used for the dynamic libraries. But I'd recommend you use SQLcl instead, which is newer, easier to use, and offers
additional features, like code completion, which may be useful here. I will install it using Homebrew
(brew install sqlcl) to simplify the process, but you can refer to the official documentation and to Jeff's video for
an intro to the tool.
Prepare the database
Now that I have our container up and running and the client and libraries installed, I'd like to connect to the database and add some data. Oracle provides sample schemas, but they aren't installed in the container by default. We will download them from this GitHub repo, uncompress the zip file and connect to the database from there.
unzip db-sample-schemas-23.3.zip
cd db-sample-schemas-23.3/customer_orders
sql sys@localhost:1521 as sysdbaSet up for vector search
Once I have connected to the database with the correct password, I can use the opportunity to alter the CDB's vector pool size, as I will need it for the second part of this article.
ALTER SYSTEM SET vector_memory_size=512M scope=spfile;
This change has been introduced to the server parameters file and requires restarting the database so it gets read and
applied. In this case, I had called my container db-free, and I restart it from the command line.
docker container restart db-freeLet's get some data
In order to run the script that creates the database, I will use SQLcl from the command line, connecting again to the database as SYS, but this time targeting the pluggable database rather than the container one.
sql sys@localhost:1521/FREEPDB1 as sysdba @co_install.sqlWhen prompted, I will use "co" as the password for the "CO" user. Obviously, this is NOT a secure choice, but this is just a test in a container that will be discarded afterwards. The tablespace for CO will be USERS (default value), and I have safely assumed that the schema can be overwritten because this is a new container.
The script will create the tables, add the data, and exit from SQLcl. I want to connect again, this time with the CO user.
sql co/co@localhost:1521/FREEPDB1And make a query to verify that everything went well.
SELECT product_id, product_name, unit_price FROM products WHERE product_name LIKE 'Girl''s Coat%';Which should reply with a single product.
PRODUCT_ID PRODUCT_NAME UNIT_PRICE
_____________ _____________________ _____________
15 Girl's Coat (Blue) 13.09
Feel free to familiarize yourself with the schema by using desc <table_name> or info <table_name> before moving on
to writing some Rust code.
Enhance user CO
I will finish the database setup by configuring the user CO to work with vector search, because I will be using these changes in the second part of this article. I need to make these changes as SYSDBA, so we can start a new session as the SYS user and with the SYSDBA privilege.
sql sys@localhost:1521/FREEPDB1 as sysdbaThis is a good opportunity to verify that the change I made before has been applied.
SHOW PARAMETER vector_memory_size;Then, I issue the following commands that add some capabilities to the CO user.
GRANT "DB_DEVELOPER_ROLE" TO "CO";
ALTER USER "CO" DEFAULT ROLE ALL;
ALTER USER "CO" QUOTA UNLIMITED ON USERS;
EXIT;That's it! Let's start coding!
First "contact" with the Oracle DB instance
I am going to start by creating a new workspace in my oracle-from-rust project. I will add the sibyl crate that I will use
to interact with Oracle AI db26ai. Also, I need to add tokio with the macros and rt-multi-thread features, and
anyhow to simplify error handling.
cargo new sibyl-test
cd sibyl-test
cargo add -F tokio,nonblocking sibyl
cargo add -F macros,rt-multi-thread tokio
cargo add anyhowConnection
Before any real interaction with the Oracle database instance, I need to connect to it. Sessions are created using the
instance of Environment that is produced by the top level function env() that is meant to parse the Oracle Cloud
Infrastructure environments.
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
println!("sibyl-test");
let sibyl_env = sibyl::env()?;
let session = sibyl_env
.connect("localhost:1521/FREEPDB1", "co", "co")
.await?;
Ok(())
}WARNING:
The connection data is hard-coded in this example. DO NOT do this in real software! Instead, get those values from the environment.
Ping
And I would like to send the simplest command before closing the session: a ping. It shouldn't fail, because the connection has just been established.
session.ping().await?;
println!("Connected to the database.");
If we run this version with cargo run, we should get the "Connected to database" message in the output. But if you get
any "Library not loaded" error mentioning libclntsh.dylib double-check your DYLD_LIBRARY_PATH before you
move on. If the problem is caused by rustc and the linker ("ld: library 'clntsh' not found"), verify RUSTFLAGS.
Simplest statement
Although the goal is to use statements to interact with the database, we can use a statement that uses the DUAL table to return a result without interacting with any actual table.
let statement = session.prepare("SELECT 'Hello Oracle!' FROM dual").await?;
if let Some(row) = statement.query_single(()).await? {
let greeting: String = row.get(0)?;
println!("{greeting}");
} else {
eprintln!("Greeting query failed.");
}Querying data from Rust
Prepared queries
In contrast with the oracle crate, sibyl requires that all SQL must be prepared first and then executed, i.e., there
is no way to execute SQL directly using Session methods. However, I don't think that is a limitation in the context
of an application, and I'd consider it a small burden when writing a proof of concept.
Multi-row query
SQL is prepared using the prepare() method of the Session instance. If successful, it returns a Statement that
can be executed right away or later. For a select statement, we use the query() method, which returns an instance of
Rows, if successful. Rows is a result set for accessing each of the rows, but it doesn't implement Iterator; it
only provides the next() method. So we have to control the number of rows to display.
Row implements only the get() method to retrieve the column values by position and requires that the type
of the variables that will receive the values be specified.
const MAX_ROWS: usize = 20;
let sql_customer_list = "SELECT * FROM co.customers";
println!("Getting {MAX_ROWS} customers:");
let stmt = session.prepare(sql_customer_list).await?;
let rows = stmt.query(()).await?;
let mut i: usize = 0;
while i < MAX_ROWS
&& let Some(row) = rows.next().await?
{
let id: u32 = row.get(0)?;
let email: String = row.get(1)?;
let fullname: String = row.get(2)?;
println!("{id}: {fullname} - {email}");
i += 1;
}Run the code again with this new snippet, and you should get a listing with MAX_ROWS customers. Hooray!
Single row query
There is also a method for queries that result in a single row. And we have already used it to obtain a greeting from the database.
The query used to retrieve data for a product with a given PRODUCT_ID can be simplified to the form shown below. If successful, the query returns a single row.
let sql_product_with_id = "SELECT product_id, product_name, unit_price \
FROM products \
WHERE product_id = :1";
let stmt = session.prepare(sql_product_with_id).await?;
if let Some(row) = stmt.query_single(15).await? {
let id: i32 = row.get(0)?;
let name: String = row.get(1)?;
let price: Option<f32> = row.get(2)?;
println!("\nProduct 15:");
println!("{id}: {name} - {}", price.unwrap_or(0.0));
} else {
eprintln!("Product query failed.");
}
Notice that the type for the price value is an Option<f32>, because items in this column might be NULL.
Reusing queries
In an application, database interactions are used repeatedly. All the SQL used by sibyl is prepared in advance, and
the produced Statement, which is immutable, can be reused as needed. This makes the code more consistent and saves
some time and effort.
Reusing the prepared statement
We can start by writing a function that, given a statement, executes the query and displays the results.
async fn query_mr(stmt: &Statement<'_>, n_rows: usize) -> Result<(), sibyl::Error> {
let rows = stmt.query(()).await?;
let mut i: usize = 0;
while i < n_rows
&& let Some(row) = rows.next().await?
{
let id: u32 = row.get(0)?;
let email: String = row.get(1)?;
let fullname: String = row.get(2)?;
println!("{id}: {fullname} - {email}");
i += 1;
}
Ok(())
}And we can use this function several times with the same statement.
println!("\nAGAIN Getting {MAX_ROWS} customers:");
let stmt_mr = session.prepare(sql_customer_list).await?;
let rows = stmt_mr.query(()).await?;
let mut i: usize = 0;
while i < MAX_ROWS
&& let Some(row) = rows.next().await?
{
let id: u32 = row.get(0)?;
let email: String = row.get(1)?;
let fullname: String = row.get(2)?;
println!("{id}: {fullname} - {email}");
i += 1;
}
println!("\nAND AGAIN Getting {MAX_ROWS} customers:");
query_mr(&stmt_mr, MAX_ROWS).await?;
println!("\n...AND AGAIN Getting {MAX_ROWS} customers:");
query_mr(&stmt_mr, MAX_ROWS).await?;
Running this code will return MAX_ROWS customers three times, reusing the same statement. Wonderful!
Using bind-by-name variables
Some of those interactions with the data that we repeat over and over are slightly different from one another. When I want to fetch the product with a given PRODUCT_ID, as I did in the section about the single row query, the SQL statement was the same, but I could replace the binding variable with a different value.
This is what I have to do when we want to repeat a similar interaction with different parameters. I will reuse the statement and pass the values to execute the query. But we can do better and give each binding variable a meaningful name. That way, we can provide pairs (name, value) in any order, making our query code easier to read and understand.
For example, I might want to retrieve information about the status of any orders processed by a given STORE_ID for a given CUSTOMER_ID. I could achieve this with the following snippet.
let sql_refunded_store_customer = "SELECT order_id, order_tms \
FROM co.orders \
WHERE order_status = 'REFUNDED' AND store_id = :store AND customer_id = :customer";
println!("\nCustomer orders refunded in store 1 for customer 99");
let stmt = session.prepare(sql_refunded_store_customer).await?;
let rows = stmt.query((("customer", 99), ("store", &1))).await?;
let mut i: usize = 0;
while i < MAX_ROWS
&& let Some(row) = rows.next().await?
{
let order_id: u32 = row.get(0)?;
let timestamp: Timestamp = row.get(1)?;
println!("{order_id} - {timestamp:?}");
i += 1;
}You may precede the binding variable names with a colon if you wish to do so. Also, notice that I have used sibyl's
Timestamp that I am printing using the Debug trait.
Run it and get a single row that matches the criteria, although there could have been more. Query long and prosper! 🖖
Summary
In this first article on using Oracle AI db26ai from Rust with the sibyl crate, I have started by explaining how to
set up an environment from scratch to test your code with an instance of the database. I have spun up the Oracle AI
db26ai with development data, making the examples reproducible in your development environment.
I have also explored using actual Rust code, several ways to perform queries using the crate, extract the data from each row, and reuse your work even with different parameters. You can find the code of this article and other related ones in this repo.
In the second part of this article, I will cover using DML, DDL, privileged connections, and even vector search. Don't miss that part!
Stay curious. Hack your code. See you next time!