Native Erlang driver and Ecto adapter for Oracle Database.
Orax speaks Oracle's TNS protocol directly from the BEAM: no Oracle Instant Client, no OCI, no ODBC, no NIFs. Add the dependency and connect.
- Ecto 3 adapter: schemas, queries, migrations,
Repo.transaction/2,Ecto.Adapters.SQL.Sandbox, identity primary keys andRETURNING. - Plain Erlang driver for applications that do not use Ecto.
- Prepared statements, positional and named binds, PL/SQL calls with
OUTparameters,SYS_REFCURSOR, batched inserts and row prefetching. - TLS connections, proxy authentication and Oracle character sets.
JSONcolumns andjson_extract_pathtranslated toJSON_VALUE.
Requirements
| Component | Supported versions |
|---|---|
| Oracle Database | 19c, 26ai (23ai) |
| Elixir | 1.18 or later |
| Erlang/OTP | 27 or later |
| ecto_sql | 3.14 or later |
Releases older than 19c are not supported.
Installation
Add orax to your dependencies in mix.exs:
def deps do
[
{:orax, "~> 0.1.0"}
]
endQuick start
Define a repository that uses the adapter:
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Orax
endand configure it:
config :my_app, MyApp.Repo,
hostname: "localhost",
port: 1521,
database: "FREEPDB1",
username: "scott",
password: "tiger",
pool_size: 10Then use it as any other Ecto repository:
MyApp.Repo.all(from u in User, where: u.active, select: u.name)
MyApp.Repo.query!("SELECT sysdate FROM dual")
#=> %{num_rows: 1, rows: [[~N[2026-11-15 10:00:00]]], ...}The implicit :bigserial primary key added by Ecto migrations is created as a
GENERATED BY DEFAULT AS IDENTITY column, so mix ecto.migrate works as
usual.
Configuration
Connection options
| Option | Description |
|---|---|
:hostname | Name or IP address of the database server. |
:port | Port the listener accepts connections on. |
:database | Service name, or SID prefixed by a colon (":FREE"). |
:username | Name of the connecting user. |
:password | Password of the connecting user. |
:parameters | Keyword list of connection parameters, see below. |
:socket_options | Options given to the underlying socket. |
:timeout | Timeout of each call in milliseconds, defaults to 15000. |
When :timeout expires the call is broken off and an error is returned. The
connection stays usable if the server answers the break within another
timeout. Without out-of-band breaks the server only sees the break between
operations, so a statement still running after twice the timeout closes the
connection.
Pool options
| Option | Description |
|---|---|
:pool | Connection pool module, defaults to DBConnection.ConnectionPool. |
:pool_size | Number of connections, defaults to 1. |
:idle_interval | Ping interval in milliseconds for idle connections, defaults to 5000. |
Connection parameters
Given as a keyword list under :parameters:
| Parameter | Description |
|---|---|
:autocommit | Commit each statement run outside a transaction, defaults to 1. |
:fetch | Number of rows fetched per round trip. |
:sdu | Session data unit size. |
:end_of_response | Use the end of response marker when the server offers it (26ai), defaults to true. |
:charset | Client character set, defaults to UTF-8. |
:proxy_user | User name for proxy authentication. |
:description | Full connect descriptor, instead of host, port and database. |
:newpassword | Change the password of the connecting user. |
:role | Mode used for an internal logon. |
:prelim | Mode permitted while the database is down. |
:ssl | TLS options, see below. |
Inside Repo.transaction/2 autocommit is always off until COMMIT or ROLLBACK.
Oracle implicitly commits DDL statements, also inside a transaction.
TLS
Start the :ssl application:
def application do
[extra_applications: [:logger, :ssl]]
endand pass the TLS options under :ssl:
config :my_app, MyApp.Repo,
# ...
parameters: [
ssl: [
verify: :verify_peer,
server_name_indication: ~c"adb.example.oraclecloud.com",
cacerts: :public_key.cacerts_get()
]
]Quoted identifiers
By default identifiers generated by the adapter are not quoted, so Oracle folds them to upper case, as existing Oracle schemas expect. To quote every table, prefix, column, index and constraint name:
config :orax, quote_identifiers: trueQuoted identifiers keep their case, so "users" and "USERS" are different
objects. Use the same setting for migrations and application queries.
With the option off, single identifiers can still be quoted explicitly:
schema "\"USERS\"" do
field :name, :string, source: :"\"NAME\""
endJSON library
:map fields and JSON columns are encoded with Elixir's built-in JSON.
Any module with encode!/1 and decode!/1 can replace it:
config :orax, :json_library, JasonTypes
| Ecto type | Oracle type |
|---|---|
:id, :integer | NUMBER(*,0), INTEGER |
:float | NUMBER, FLOAT, BINARY_FLOAT |
:decimal | NUMBER, FLOAT, BINARY_FLOAT |
:string | CHAR, VARCHAR2, CLOB, NCHAR, NVARCHAR2, NCLOB |
:binary | RAW, BLOB |
:binary_id | RAW |
:boolean | CHAR, VARCHAR2, NUMBER |
:map | JSON, CLOB, NCLOB |
:date | DATE |
:naive_datetime | DATE, TIMESTAMP |
:utc_datetime | TIMESTAMP WITH TIME ZONE |
In migrations, add :name, :string, national: true creates an NVARCHAR2
column, and comment: on tables and columns is supported.
Raw SQL and PL/SQL
Repo.query/4 takes positional (:1, :2) binds. Binds whose type cannot be
inferred from the value are given with the :in option. OUT parameters are
given in place as {:out, type}, or with the :out option, which appends
them after the other binds:
# Binary and UUID binds
Repo.query("INSERT INTO docs (id, body) VALUES (:1, :2)",
[Ecto.UUID.bingenerate(), <<0xE7, 0x99, 0xBE>>], in: [:binary_id, :binary])
# Stored function with an OUT parameter
Repo.query("BEGIN :1 := price_of(:2); END;", [{:out, :number}, "SKU-1"])
#=> {:ok, %{rows: [[50]], ...}}
# RETURNING ... INTO
Repo.query("INSERT INTO events (at) VALUES (:1) RETURNING id INTO :2",
[Date.utc_today()], out: [:integer])
#=> {:ok, %{rows: [[1]], ...}}
# Batched insert
Repo.query("INSERT INTO points VALUES (:1, :2)", [[1, 2], [3, 4]], batch: true)For schema operations the OUT types of RETURNING ... INTO are inferred from
the schema, including identity primary keys and the IDs of nested association
inserts.
OUT / :in type | Oracle type |
|---|---|
:number, :integer, :float, :decimal | NUMBER, FLOAT, BINARY_FLOAT |
:varchar, :char, :clob, :string | CHAR, VARCHAR2, CLOB |
:nvarchar, :nchar, :nclob | NCHAR, NVARCHAR2, NCLOB |
:raw, :blob, :binary, :hexstring | RAW, BLOB |
:date | DATE |
:timestamp | TIMESTAMP |
:timestamptz | TIMESTAMP WITH TIME ZONE |
:cursor | SYS_REFCURSOR |
Using the Erlang driver
Without Ecto, the driver is a process per connection:
{ok, Conn} = orax:start_link([
{host, "localhost"}, {port, 1521},
{user, "scott"}, {password, "tiger"},
{service_name, "FREEPDB1"}
]),
{ok, [{result_set, Columns, _, Rows}]} =
orax:sql_query(Conn, {"SELECT id, name FROM users WHERE id > :1", [10]}),
%% Named binds
orax:sql_query(Conn, {"INSERT INTO t VALUES (:id, :dat)", [#{id => 1, dat => {2026, 1, 1}}]}),
%% PL/SQL with OUT parameters and cursors
orax:sql_query(Conn, {"BEGIN :1 := func(:2); END;", [{out, varchar}, "one hundred"]}),
orax:sql_query(Conn, {"BEGIN OPEN :1 FOR SELECT * FROM t WHERE dat > :2; END;",
[cursor, {2026, 1, 1}]}),
%% Batching and prefetching
orax:sql_query(Conn, {batch, "INSERT INTO t VALUES (:1, :2)", [[1, 2], [3, 4]]}),
orax:sql_query(Conn, {fetch, "SELECT * FROM t WHERE id > :1", [1]}).Character sets
In Erlang, strings are charlists and binaries are RAW values. Text is UTF-8
by default; the :charset parameter also accepts:
:us7ascii, :we8iso8859p1, :ee8iso8859p2, :nee8iso8859p4,
:cl8iso8859p5, :ar8iso8859p6, :el8iso8859p7, :iw8iso8859p8,
:we8iso8859p9, :ne8iso8859p10, :th8tisascii, :vn8mswin1258,
:we8iso8859p15, :blt8iso8859p13, :ee8mswin1250, :cl8mswin1251,
:el8mswin1253, :iw8mswin1255, :tr8mswin1254, :we8mswin1252,
:blt8mswin1257, :ar8mswin1256, :ja16euc, :ja16sjis, :ja16euctilde,
:ja16sjistilde, :ko16mswin949, :zhs16gbk, :zht32euc, :zht16big5,
:zht16mswin950, :zht16hkscs.
Migrating from jamdb_oracle
- Replace the
:jamdb_oracledependency with:orax. - Replace
Ecto.Adapters.Jamdb.OraclewithEcto.Adapters.Orax,Jamdb.OraclewithOraxand the Erlang modulesjamdb_oracle_*withorax_*. - Move
config :jamdb_oracle, ...toconfig :orax, .... - The JSON library defaults to Elixir's
JSON; setconfig :orax, :json_library, Jasonto keep Jason. - Remove
:read_timeoutfrom:parameters.
See the changelog for the other behaviour changes.
Development
Unit tests need no database:
mix test
Integration tests are tagged :oracle and run against an Oracle Database Free
container, as CI does:
docker run -d --name orax-test-db -p 1521:1521 -e ORACLE_PWD=OraxTest1 \
container-registry.oracle.com/database/free:latest-lite
docker cp test/support/create_test_user.sql orax-test-db:/tmp/
docker exec orax-test-db sqlplus -s system/OraxTest1@//localhost:1521/FREEPDB1 \
@/tmp/create_test_user.sql
mix test --include oracle
The connection is read from ORAX_TEST_HOST, ORAX_TEST_PORT,
ORAX_TEST_USER, ORAX_TEST_PASSWORD and ORAX_TEST_DATABASE.
Credits
Orax is a fork of jamdb_oracle by Mykhailo Vstavskyi and Sergiy Kostyushkin, who wrote the driver this project is built on. Many thanks to them.
License
MIT, see LICENSE.