CI Hex.pm Docs License

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 and RETURNING.
  • Plain Erlang driver for applications that do not use Ecto.
  • Prepared statements, positional and named binds, PL/SQL calls with OUT parameters, SYS_REFCURSOR, batched inserts and row prefetching.
  • TLS connections, proxy authentication and Oracle character sets.
  • JSON columns and json_extract_path translated to JSON_VALUE.

Requirements

ComponentSupported versions
Oracle Database19c, 26ai (23ai)
Elixir1.18 or later
Erlang/OTP27 or later
ecto_sql3.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"}
  ]
end

Quick start

Define a repository that uses the adapter:

defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Orax
end

and configure it:

config :my_app, MyApp.Repo,
  hostname: "localhost",
  port: 1521,
  database: "FREEPDB1",
  username: "scott",
  password: "tiger",
  pool_size: 10

Then 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

OptionDescription
:hostnameName or IP address of the database server.
:portPort the listener accepts connections on.
:databaseService name, or SID prefixed by a colon (":FREE").
:usernameName of the connecting user.
:passwordPassword of the connecting user.
:parametersKeyword list of connection parameters, see below.
:socket_optionsOptions given to the underlying socket.
:timeoutTimeout 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

OptionDescription
:poolConnection pool module, defaults to DBConnection.ConnectionPool.
:pool_sizeNumber of connections, defaults to 1.
:idle_intervalPing interval in milliseconds for idle connections, defaults to 5000.

Connection parameters

Given as a keyword list under :parameters:

ParameterDescription
:autocommitCommit each statement run outside a transaction, defaults to 1.
:fetchNumber of rows fetched per round trip.
:sduSession data unit size.
:end_of_responseUse the end of response marker when the server offers it (26ai), defaults to true.
:charsetClient character set, defaults to UTF-8.
:proxy_userUser name for proxy authentication.
:descriptionFull connect descriptor, instead of host, port and database.
:newpasswordChange the password of the connecting user.
:roleMode used for an internal logon.
:prelimMode permitted while the database is down.
:sslTLS 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]]
end

and 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: true

Quoted 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\""
end

JSON 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, Jason

Types

Ecto typeOracle type
:id, :integerNUMBER(*,0), INTEGER
:floatNUMBER, FLOAT, BINARY_FLOAT
:decimalNUMBER, FLOAT, BINARY_FLOAT
:stringCHAR, VARCHAR2, CLOB, NCHAR, NVARCHAR2, NCLOB
:binaryRAW, BLOB
:binary_idRAW
:booleanCHAR, VARCHAR2, NUMBER
:mapJSON, CLOB, NCLOB
:dateDATE
:naive_datetimeDATE, TIMESTAMP
:utc_datetimeTIMESTAMP 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 typeOracle type
:number, :integer, :float, :decimalNUMBER, FLOAT, BINARY_FLOAT
:varchar, :char, :clob, :stringCHAR, VARCHAR2, CLOB
:nvarchar, :nchar, :nclobNCHAR, NVARCHAR2, NCLOB
:raw, :blob, :binary, :hexstringRAW, BLOB
:dateDATE
:timestampTIMESTAMP
:timestamptzTIMESTAMP WITH TIME ZONE
:cursorSYS_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_oracle dependency with :orax.
  • Replace Ecto.Adapters.Jamdb.Oracle with Ecto.Adapters.Orax, Jamdb.Oracle with Orax and the Erlang modules jamdb_oracle_* with orax_*.
  • Move config :jamdb_oracle, ... to config :orax, ....
  • The JSON library defaults to Elixir's JSON; set config :orax, :json_library, Jason to keep Jason.
  • Remove :read_timeout from :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.