- Published on
Adding Rust to a System That Already Uses Oracle
- Authors

- Name
- Mehdi Akiki
Article · Through the layers
The insert succeeds. The Rust program reads the row back and prints the right value. Another process cannot find it.
In this example, the missing step is a commit. The new program has checked its own uncommitted work and assumed that other processes can see it too. This is an easy trap when replacing a small Oracle job: the query code looks familiar, but the old job's transaction handling lived somewhere else.
For the first transaction check, write one row, commit it, and read it through a second connection. Then change the row, roll that change back, and ask the second connection what it sees. There is no need to port the whole job to answer whether its commit and rollback behave as intended.
A Rust dependency does not install the Oracle Client
This example pins the oracle crate at 0.6.3. It uses ODPI-C underneath. The crate documentation lists the native requirements: a C compiler for the build and Oracle Client libraries at runtime. A Cargo build succeeding on the CI image does not put those libraries into the image that eventually runs the tool.
The shared helper also sets a database round-trip timeout. That choice raises this example's minimum Oracle Client version to 18.1, even though the crate can connect with older clients when that feature is not used. Record the actual client release and its supported server combination, rather than combining unrelated minimum-version statements from two pages.
Use an approved Oracle Net alias or connection description. In a deployment requiring TLS, that means the appropriate TCPS configuration, wallet or trust material, and server identity checks. The Rust example receives the connection description; it does not silently build a secure Oracle Net configuration around an arbitrary host. The ODPI-C installation guide is the right place to resolve library loading before investigating application SQL.
If the process cannot load OCI, adding database grants will not help. If OCI loads but login fails, inspect the service name and account. Keep those failures separate. They happen before the transaction example can tell you anything.
Make the transaction visible to another connection
The fixture creates a uniquely named scratch table in a disposable schema. Its identifier is generated by trusted Rust code; it is not a request parameter. This is the transaction part from oracle_profile.rs:
fn exercise(conn: &oracle::Connection, table: &str) -> AnyResult<()> {
let id = 7i32;
let note: Option<String> = None;
conn.execute(
&format!("INSERT INTO {table} (id, note) VALUES (:1, :2)"),
&[&id, ¬e],
)?;
conn.commit()?;
let observer = oracle_connection()?;
let row = observer.query_row(
&format!("SELECT id, note FROM {table} WHERE id = :1"),
&[&id],
)?;
let actual_id: i32 = row.get(0)?;
let actual_note: Option<String> = row.get(1)?;
assert_eq!((actual_id, actual_note), (id, None));
conn.execute(
&format!("UPDATE {table} SET note = :1 WHERE id = :2"),
&[&"changed", &id],
)?;
conn.rollback()?;
let writer_row = conn.query_row(&format!("SELECT note FROM {table} WHERE id = :1"), &[&id])?;
assert_eq!(writer_row.get::<_, Option<String>>(0)?, None);
let row = observer.query_row(&format!("SELECT note FROM {table} WHERE id = :1"), &[&id])?;
assert_eq!(row.get::<_, Option<String>>(0)?, None);
println!("committed insert visible; rolled-back note unchanged");
Ok(())
}
The values are bound using :1 and :2. Only the generated table identifier enters formatted SQL. A real service should usually use a fixed, reviewed table or call the existing package that owns the business rule.
After the insert commits, the observer should read identifier 7 with a null note. The writer then changes the note and rolls back. It rereads its own row and must find a null note again. This catches a missing rollback: another connection would still see the old committed value while that update remained uncommitted. The observer also rereads the note to check what other processes can see.
The second connection matters. The writer can see its own changes before a commit. Reading through that same connection would not prove that another process can see them. Oracle's commit reference documents the visibility change and the transaction boundary.
A null note is decoded as Option<String>. Requiring a plain String would turn an allowed null into a conversion failure. Replacing it with an empty string would make a product decision the query layer has not been asked to make. Oracle also has a specific empty-character-value rule to check before promising that those states remain distinct.
Keep DDL outside the transaction you are investigating
The scratch table setup and cleanup are separate from the insert/update exercise. That is deliberate. Oracle DDL has implicit commit behavior. Putting a table-creation statement between the update and the rollback would change what the rollback can undo.
Do not turn this example into an application migration runner by copying its scratch-table creation into a request handler. Use the project's migration process. Let the application account work with the objects it needs instead of granting it broad DDL rights because the demonstration needed a temporary test object.
The fixture attempts to drop its table when the exercise returns. A killed process or lost session can leave cleanup unfinished, so it belongs in a disposable schema with an owner able to remove leftovers. Both connections read the expected note before cleanup. Dropping the table is just housekeeping.
There are other transaction details to examine in the real job. Does a stored procedure commit internally? Does an exception handler roll back all work or only part of it? Is the caller relying on a savepoint? A Rust wrapper cannot safely add its own retry or commit policy without understanding those choices.
Find who commits the old job
Before replacing the old job, list its inputs, output rows, commit point, and failure response. Check whether a run can be repeated without writing the same business event twice. Binding an identifier keeps that value out of the SQL text. It does not prevent a second attempt from recording the same payment or order again.
The synchronous Oracle calls also need a place in the service architecture. A command-line job can call them directly. An async web service should place blocking database work behind a bounded worker arrangement rather than letting it occupy every runtime worker. Do not create an unlimited new blocking task for every incoming request and call that a capacity plan.
The helper's call timeout applies to individual database round trips, as the connection reference explains. It is not a total duration limit for a long job containing many successful calls. Give the job its own supervised lifetime too.
Use a disposable schema for this example and supply the expected server version in ORACLE_EXPECT_VERSION. The helper rejects a different version. Record the Oracle Client build too: two images with the same Rust lockfile can load different native libraries. Neither the helper's version check nor the scratch table checks your existing packages, grants, or triggers.
When the first port is ready, compare it with the old job using a successful write and a deliberately rolled-back change. Have the other process read both results. That catches the missing commit while there is still only one row and one transaction to explain.