ExSQL (exsql v0.1.5)
Copy MarkdownA SQLite implementation in pure Elixir.
ExSQL follows SQLite's architecture — tokenizer, parser, executor, storage — reshaped for the BEAM: the engine is a pure functional core over immutable data, with an optional GenServer connection for stateful use.
Quick start
{:ok, conn} = ExSQL.open()
ExSQL.execute(conn, """
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);
INSERT INTO users (name, age) VALUES ('alice', 34), ('bob', 29);
""")
{:ok, result} = ExSQL.query(conn, "SELECT name FROM users WHERE age > 30")
result.rows
#=> [["alice"]]Purely functional use
The engine itself never touches a process; you can thread the database value yourself:
db = ExSQL.Database.new()
{:ok, _, db} = ExSQL.Executor.run(db, "CREATE TABLE t (x)")
{:ok, [result], _db} = ExSQL.Executor.run(db, "SELECT count(*) FROM t")Pipeline
| stage | SQLite (C) | ExSQL |
|---|---|---|
| lexing | tokenize.c | ExSQL.Tokenizer |
| parsing | parse.y (Lemon) | ExSQL.Parser |
| execution | codegen + VDBE (vdbe.c) | ExSQL.Executor (tree-walking) |
| storage | btree.c + pager.c | ExSQL.Table / ExSQL.Database (in-memory) |
Summary
Functions
Stops a connection.
Registers or replaces a connection-local aggregate function.
Registers or replaces a connection-local collation.
Registers or replaces a connection-local scalar function.
Registers or replaces a connection-local incremental aggregate window function.
Registers or replaces a connection-local aggregate window function.
Executes one or more ;-separated statements, returning one
ExSQL.Result per statement.
Like execute/3, but raises ExSQL.Error on failure.
Opens a new in-memory database connection (the equivalent of
sqlite3_open(":memory:")).
Executes a single statement and returns its ExSQL.Result.
Like query/3, but raises ExSQL.Error on failure.
Functions
@spec close(GenServer.server()) :: :ok
Stops a connection.
@spec create_aggregate(GenServer.server(), String.t(), non_neg_integer(), function()) :: :ok | {:error, ExSQL.Error.t()}
Registers or replaces a connection-local aggregate function.
arity is the SQL arity. The callback itself receives one argument: a list
of evaluated, non-NULL argument rows. For example, a one-argument aggregate
receives [[value], ...].
:ok = ExSQL.create_aggregate(conn, "product", 1, fn rows ->
rows |> Enum.map(&hd/1) |> Enum.product()
end)
@spec create_collation(GenServer.server(), String.t(), function()) :: :ok | {:error, ExSQL.Error.t()}
Registers or replaces a connection-local collation.
The callback receives two text values and may return :lt, :eq, :gt, a
negative/zero/positive integer, {:ok, result}, or {:error, message}.
:ok = ExSQL.create_collation(conn, "reverse", fn a, b ->
cond do
a < b -> :gt
a > b -> :lt
true -> :eq
end
end)
ExSQL.query!(conn, "SELECT name FROM users ORDER BY name COLLATE reverse")
@spec create_function(GenServer.server(), String.t(), non_neg_integer(), function()) :: :ok | {:error, ExSQL.Error.t()}
Registers or replaces a connection-local scalar function.
The callback arity must match the SQL arity exactly. It receives evaluated
SQL values as positional arguments and may return a SQL value, {:ok, value},
or {:error, message}.
:ok = ExSQL.create_function(conn, "double", 1, fn x -> x * 2 end)
ExSQL.query!(conn, "SELECT double(21)").rows
#=> [[42]]
@spec create_incremental_window_function( GenServer.server(), String.t(), non_neg_integer(), map() ) :: :ok | {:error, ExSQL.Error.t()}
Registers or replaces a connection-local incremental aggregate window function.
The callback map must contain :init, :step, :inverse, :value, and
:final functions. init.() returns the initial state, step.(state, args)
adds one frame row, inverse.(state, args) removes one frame row,
value.(state) returns the current window value, and final.(state) returns
the aggregate value when the function is used without OVER.
:ok = ExSQL.create_incremental_window_function(conn, "running_sum", 1, %{
init: fn -> 0 end,
step: fn total, [x] -> total + (x || 0) end,
inverse: fn total, [x] -> total - (x || 0) end,
value: fn total -> total end,
final: fn total -> total end
})
@spec create_window_function( GenServer.server(), String.t(), non_neg_integer(), function() ) :: :ok | {:error, ExSQL.Error.t()}
Registers or replaces a connection-local aggregate window function.
The callback contract matches create_aggregate/4: it receives one list of
evaluated, non-NULL argument rows. When used with OVER (...), ExSQL calls
it with the rows in the current window frame.
:ok = ExSQL.create_window_function(conn, "frame_count", 1, fn rows ->
length(rows)
end)
@spec execute(GenServer.server(), String.t(), [ExSQL.Value.t()] | map()) :: {:ok, [ExSQL.Result.t()]} | {:error, ExSQL.Error.t()}
Executes one or more ;-separated statements, returning one
ExSQL.Result per statement.
Bind parameters are bound from params: a list for positional parameters
(?, ?NNN; 1-based) or a map for named ones (:name, @name, $name;
keys may include or omit the sigil, and integer keys bind by index).
ExSQL.query(conn, "SELECT * FROM users WHERE age > ?", [30])
ExSQL.query(conn, "SELECT * FROM users WHERE name = :name", %{name: "alice"})
@spec execute!(GenServer.server(), String.t(), [ExSQL.Value.t()] | map()) :: [ ExSQL.Result.t() ]
Like execute/3, but raises ExSQL.Error on failure.
@spec open(keyword()) :: GenServer.on_start()
Opens a new in-memory database connection (the equivalent of
sqlite3_open(":memory:")).
@spec query(GenServer.server(), String.t(), [ExSQL.Value.t()] | map()) :: {:ok, ExSQL.Result.t()} | {:error, ExSQL.Error.t()}
Executes a single statement and returns its ExSQL.Result.
Errors if sql contains more than one statement — use execute/3 for
scripts. Takes bind parameters like execute/3.
@spec query!(GenServer.server(), String.t(), [ExSQL.Value.t()] | map()) :: ExSQL.Result.t()
Like query/3, but raises ExSQL.Error on failure.