glite

A SQLite driver for Gleam on the Erlang VM, built on esqlite, which bundles SQLite 3.50.4.

gleam add glite
import glite
import glite/config
import glite/decode

pub fn main() {
  let assert Ok(conn) = glite.connect(config.new("app.db"))
  let assert Ok(Nil) =
    glite.exec_script(
      conn,
      "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE)",
    )

  let assert Ok(id) =
    glite.query_one(
      conn,
      "INSERT INTO users (name) VALUES (?) RETURNING id",
      [glite.text("Alice")],
      {
        use id <- decode.element(0, decode.int)
        decode.success(id)
      },
    )

  let assert Ok(names) =
    glite.query(conn, "SELECT name FROM users ORDER BY name", [], {
      use name <- decode.element(0, decode.text)
      decode.success(name)
    })

  let assert Ok(Nil) = glite.disconnect(conn)
}

How it works

A connection is one actor that owns the SQLite handle and runs every call on it, one at a time. Share a Connection between as many processes as you like: their calls queue up in the actor.

import glite/tx

glite.transaction(conn, glite.Immediate, fn(t) {
  use count <- result.try(
    tx.query_one(t, "SELECT count(*) FROM queue WHERE state = 'waiting'", [], count_decoder),
  )
  tx.exec(t, "INSERT INTO batches (size) VALUES (?)", [glite.int(count)])
})

Use Immediate for any transaction that writes. Calling the Connection from inside its own transaction body panics with a clear message; use the Tx.

Checked before running

Limits

Every limit is in the config, and reaching one is an error, never a silent truncation.

Setting Default When it's reached
call_timeout_ms 10,000 Timeout; the statement is interrupted
busy_timeout_ms 5,000 Busy; must be shorter than the call timeout
max_rows 10,000 RowLimitExceeded; no rows are returned
max_batch 10,000 BatchLimitExceeded for exec_many
max_queue 1,000 Overloaded; the call isn't sent
interrupt_grace_ms 1,000 How long a timed-out caller waits for SQLite to stop

config.new(path) uses WAL, synchronous=FULL (a committed transaction survives a power cut) and foreign keys on. config.memory() is the same for a private in-memory database.

Supervision

import gleam/erlang/process
import gleam/otp/static_supervisor

let db = process.new_name("db")
let assert Ok(_) =
  static_supervisor.new(static_supervisor.OneForOne)
  |> static_supervisor.add(glite.supervised(config.new("app.db"), db))
  |> static_supervisor.start

let conn = glite.named(db)

If the connection's actor crashes, the supervisor restarts it, which opens the database again, and the same Connection value keeps working. glite.connect instead starts an actor linked to the caller.

Backups and migrations

Values

Parameters are built with glite.int, float, text, blob, bool (stored as 0 or 1), null and nullable. Rows are decoded by column index with glite/decode. A decode error says which row and column failed.

esqlite returns TEXT and BLOB cells the same way, so glite tells them apart by the column's declared type: a column declared with BLOB in its type returns Blob. Anything that isn't valid UTF-8 is always Blob, so a String never holds invalid UTF-8, and decode.blob accepts text as well.

Development

make check     # what CI runs: format, warnings as errors, tests, benchmarks compile
make bench     # benchmarks, in the bench/ project
make release   # tag the version in gleam.toml; CI publishes it to Hex