gdo
gdo is a typed database access library for Gleam.
It provides a small, functional API for:
- opening SQLite connections
- executing prepared statements
- reading rows through typed decoders
- running explicit transactions
- handling failures through structured errors
Features
- idiomatic Gleam API
Result-based error handling- positional and named parameters
- prepared statement workflow
- explicit transaction control
- typed row decoding helpers
- structured connection, query, transaction, and decode errors
Installation
gleam add gdo
Quick Start
import gdo
import gdo/connection
import gdo/decode
import gdo/error.{type Error}
import gdo/result
import gdo/value.{Int, Positional, String}
import gleam/option.{None, Some}
pub fn main() -> Result(Nil, Error) {
use db <- result.try(gdo.open_sqlite("file:app.sqlite"))
use _ <- result.try(
connection.exec(
db,
"create table if not exists users (id integer primary key, name text not null)",
[],
),
)
use insert_result <- result.try(
connection.exec(db, "insert into users (id, name) values (?, ?)", [
Positional(Int(1)),
Positional(String("Ana")),
]),
)
let last_id = result.last_insert_id(insert_result)
let _ = last_id
use maybe_row <- result.try(
connection.query_one(db, "select id, name from users where id = ?", [
Positional(Int(1)),
]),
)
case maybe_row {
Some(current_row) -> {
let decoder =
decode.map2(
decode.column_at(0, using: decode.int()),
decode.column_at(1, using: decode.string()),
with: fn(id, name) { #(id, name) },
)
let assert Ok(#(id, name)) = decode.decode(current_row, using: decoder)
let _ = #(id, name)
connection.close(db)
}
None -> connection.close(db)
}
}
Connection Workflow
Use gdo.open_sqlite for the common path, or connection.open with
connection.sqlite_config when you want to build the configuration explicitly.
import gdo
import gdo/connection
pub fn open_database() {
let assert Ok(db) =
gdo.sqlite_config("file:app.sqlite")
|> connection.open
assert connection.in_transaction(db) == False
}
For one-shot calls there are root helpers:
gdo.exec_sqlitegdo.query_one_sqlitegdo.query_all_sqlite
These helpers open a SQLite connection for the operation and return the typed result directly.
Statement Workflow
Prepare statements when you want to reuse SQL, validate placeholder style once, or keep execution and reading separate.
import gdo
import gdo/connection
import gdo/statement
import gdo/value.{Named, String}
pub fn find_user_by_email() {
let assert Ok(db) = gdo.open_sqlite("file:app.sqlite")
let assert Ok(stmt) =
connection.prepare(db, "select id, email from users where email = :email")
let assert Ok(Some(current_row)) =
statement.query_one(stmt, [Named("email", String("ana@example.com"))])
let _ = current_row
}
gdo supports:
- positional placeholders:
? - named placeholders:
:name
Mixing placeholder styles in the same statement is rejected during preparation.
Transaction Workflow
Transactions are explicit and keep the connection immutable from the caller’s point of view.
import gdo
import gdo/connection
import gdo/value.{Int, Positional, String}
pub fn create_user() {
let assert Ok(db) = gdo.open_sqlite("file:app.sqlite")
let assert Ok(db) = connection.begin(db)
let assert Ok(_) =
connection.exec(db, "insert into users (id, name) values (?, ?)", [
Positional(Int(1)),
Positional(String("Ana")),
])
let assert Ok(db) = connection.commit(db)
let _ = db
}
Use connection.rollback when the unit of work should be discarded.
Row Decoding
Rows can be inspected directly with row.get and row.get_at, or decoded into
application values with gdo/decode.
import gdo/decode
pub fn user_decoder() {
decode.map2(
decode.column_at(0, using: decode.int()),
decode.column_at(1, using: decode.string()),
with: fn(id, name) { User(id:, name:) },
)
}
pub type User {
User(id: Int, name: String)
}
Available decoders include:
decode.intdecode.floatdecode.booldecode.stringdecode.bytesdecode.nullabledecode.mapdecode.map2decode.map3
Error Model
gdo keeps failures inside the public Error type:
ConnectionErrorQueryErrorTransactionErrorDecodeErrorUnsupportedFeatureInvalidConfiguration
Helpers in gdo/error expose the common fields:
error.messageerror.codeerror.sqlstate
Typical handling looks like this:
import gdo
import gdo/error
pub fn run_query() {
case gdo.open_sqlite("file:app.sqlite") {
Ok(_) -> Nil
Error(err) -> {
let _message = error.message(err)
let _code = error.code(err)
let _sqlstate = error.sqlstate(err)
Nil
}
}
}
SQLite Notes
Current SQLite support includes:
- real connection open and close
- prepared statement execution
- reads through
query_oneandquery_all - transaction operations
last_insert_id- driver error mapping into
gdo/error
Current SQLite limitation:
- the JavaScript SQLite path is currently Deno-specific through
sqlight - result rows are most reliable through positional access and
decode.column_at. The current SQLite path does not yet expose real column names from backend metadata, so rows returned from queries use synthetic column names internally.
Contributing
Contributions are welcome. Read CONTRIBUTING.md, CODE_OF_CONDUCT.md, and SECURITY.md before opening a pull request.
License
This project is licensed under the Apache License 2.0. See LICENSE.
Development
From the gdo directory:
gleam test
gleam format --check src test