pturso

Package Version Hex Docs

Turso client for Gleam, implemented as an external Rust process (source in this repo, a binary called erso) communicating via Erlang ports.

Since this uses ports, it works with erlang-linux-builds.

The API is similar to that of sqlight which means it also plays nicely with parrot.

Currently this only works for the Erlang target. Turso technically does run in wasm, so we could support JS theoretically but I haven’t had the need yet!

This project is architected in a “very async” way. No benchmarks yet though, so no clue if that pays off or not.

Communication between BEAM and erso uses the bincode/wincode format, which is quick and Rust-native.

gleam add pturso

Quick Start

import gleam/dynamic/decode
import pturso

pub fn main() {
  // Start the erso binary (auto-downloads if needed)
  let assert Ok(port) = pturso.start()

  // Connect to a database (local files only for now; no Turso Cloud support yet)
  let conn = pturso.connect(port, to: "my_app.db", log_with: fn(_) { Nil })

  // Create a table
  let assert Ok(Nil) = pturso.exec("
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY,
      name TEXT NOT NULL,
      email TEXT
    )
  ", on: conn)

  // Insert data with parameters
  let assert Ok(_) = pturso.query(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    on: conn,
    with: [pturso.String("Alice"), pturso.String("alice@example.com")],
    expecting: decode.success(Nil),
  )

  // Query data
  let user_decoder = {
    use id <- decode.field(0, decode.int)
    use name <- decode.field(1, decode.string)
    use email <- decode.field(2, decode.optional(decode.string))
    decode.success(#(id, name, email))
  }

  let assert Ok(users) = pturso.query(
    "SELECT id, name, email FROM users",
    on: conn,
    with: [],
    expecting: user_decoder,
  )

  echo users
  // [#(1, "Alice", Some("alice@example.com"))]

  pturso.stop(port)
}

Starting the Client

pturso requires a native binary (erso) to perform database queries. The source code for that binary is in this repo in the ./rust directory.

In Gleam, this binary can be managed in a few ways.

Automatic (easiest)

// Auto-detects the best method:
// 1. Uses ERSO env var if set
// 2. Uses cargo install if cargo is available
// 3. Downloads pre-built binary from GitHub releases
let assert Ok(port) = pturso.start()

Build from source via crates.io

Requires Cargo.

// Uses ~/.cargo/bin/erso, runs `cargo install erso` if not present
let assert Ok(port) = pturso.start_from_crates_io()

Pre-built from GitHub Releases

Downloads artifacts built via the GH Actions on this repo.

// Downloads pre-built binary to ~/.cache/pturso/erso
let assert Ok(port) = pturso.start_from_github_release()

Custom Binary Path

For if you want to manage the erso binary yourself.

// Use your own binary
let assert Ok(port) = pturso.start_with_binary("/path/to/erso")

Connecting to Databases

// Local SQLite file
let conn = pturso.connect(port, to: "local.db", log_with: fn(_) { Nil })

// In-memory database
let conn = pturso.connect(port, to: ":memory:", log_with: fn(_) { Nil })

// With query logging
let conn = pturso.connect(port, to: "app.db", log_with: fn(entry) {
  io.println("SQL: " <> entry.sql <> " (" <> int.to_string(entry.duration_ms) <> "ms)")
})

Actual database connections are created lazily. connect doesn’t interact with the filesystem, and so it never fails. You’ll have to run an actual query before the DB is created/read.

Parameter Types

// Supported parameter types
pturso.Null
pturso.Int(42)
pturso.Float(3.14)
pturso.String("hello")
pturso.Blob(<<1, 2, 3>>)

Running Multiple Statements

Use exec to run multiple SQL statements (e.g., migrations):

let assert Ok(Nil) = pturso.exec("
  CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT);
  CREATE TABLE comments (id INTEGER PRIMARY KEY, post_id INTEGER, body TEXT);
  CREATE INDEX idx_comments_post ON comments(post_id);
", on: conn)

Error Handling

Unfortunately we just use Strings for errors right now.

The only reason is I haven’t needed specific error handling for different error codes yet.

case pturso.query("SELECT * FROM nonexistent", on: conn, with: [], expecting: decoder) {
  Ok(rows) -> // handle rows
  Error(pturso.DatabaseError(message)) -> io.println("DB error: " <> message)
  Error(pturso.DecodeError(errors)) -> io.println("Decode error")
}

Development

Most of the code is Erlang files in src/*.erl. I did it this way since Gleam doesn’t seem to have good stdlib ports support yet.

gleam test             # Run Gleam/Erlang tests
cd rust && cargo test  # Run Rust tests

Further documentation can be found at https://hexdocs.pm/pturso.

Search Document