Packages

A database driver abstraction layer for Gleam

Retired package: Release invalid

Current section

Files

Jump to
common_sql src common_sql.gleam
Raw

src/common_sql.gleam

import gleam/dynamic
import gleam/dynamic/decode
import gleam/list
import gleam/result
import gleam/string
/// A query parameter value to be passed to a driver.
pub type Param {
PInt(Int)
PString(String)
PFloat(Float)
PBool(Bool)
PNull
}
/// Errors that can arise from database operations.
pub type DbError {
QueryError(String)
ConnectionError(String)
}
/// A SQL query to be executed.
///
/// - `Sql(String)` — driver-native SQL passed as-is. Use the placeholder syntax
/// expected by the target driver (e.g. `?` for SQLite, `$1` for PostgreSQL).
/// - `Portable(String)` — SQL written with PostgreSQL-style `$1, $2, …`
/// placeholders. Drivers that require a different syntax (e.g. SQLite) convert
/// the placeholders automatically, so the same string works across all drivers.
pub type Query {
Sql(String)
Portable(String)
}
/// A vtable record that abstracts over a specific database driver.
/// The `conn` type parameter lets each driver use its own opaque connection type.
pub type Driver(conn) {
Driver(
/// Identifies the underlying database engine, e.g. `"sqlite"` or `"postgresql"`.
driver_type: String,
connect: fn(String) -> Result(conn, DbError),
execute: fn(conn, Query, List(Param)) ->
Result(List(dynamic.Dynamic), DbError),
close: fn(conn) -> Nil,
)
}
/// Establish a connection using the given driver and connection URL.
pub fn connect(driver: Driver(conn), url: String) -> Result(conn, DbError) {
driver.connect(url)
}
/// Execute a SQL query, decode each returned row with `decoder`, and collect
/// all rows into a `List(a)`. Decoding errors are surfaced as `QueryError`.
pub fn execute(
driver: Driver(conn),
conn: conn,
query: Query,
params: List(Param),
decoder: decode.Decoder(a),
) -> Result(List(a), DbError) {
use rows <- result.try(driver.execute(conn, query, params))
rows
|> list.map(fn(row) {
decode.run(row, decoder)
|> result.map_error(fn(errors) { QueryError(string.inspect(errors)) })
})
|> result.all
}
/// Close a connection using the given driver.
pub fn close(driver: Driver(conn), conn: conn) -> Nil {
driver.close(conn)
}
/// Open a connection, run `f` with it, then close it — even if `f` returns
/// an error. This is the preferred way to use a connection as it guarantees
/// the connection is always closed.
///
/// ```gleam
/// use conn <- sql.with_connection(driver, "postgres://localhost/mydb")
/// sql.execute(driver, conn, "SELECT id FROM users", [], decode.int)
/// ```
pub fn with_connection(
driver: Driver(conn),
url: String,
f: fn(conn) -> Result(a, DbError),
) -> Result(a, DbError) {
use conn <- result.try(driver.connect(url))
let result = f(conn)
driver.close(conn)
result
}