Packages

A native Gleam SQLite driver

Current section

Files

Jump to
glite src glite.gleam
Raw

src/glite.gleam

/// Glite - A native Gleam SQLite driver.
///
/// ## Quick Start
///
/// ```gleam
/// let assert Ok(conn) = glite.connect(config.memory())
/// let assert Ok(_) = glite.exec(conn, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", [])
/// let assert Ok(_) = glite.exec(conn, "INSERT INTO users (name) VALUES (?)", [glite.text("Alice")])
/// let assert Ok(response) = glite.query_with(
/// conn,
/// "SELECT id, name FROM users",
/// [],
/// {
/// use id <- decode.element(0, decode.int)
/// use name <- decode.element(1, decode.text)
/// decode.success(#(id, name))
/// },
/// )
/// glite.disconnect(conn)
/// ```
import gleam/erlang/process.{type Subject}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import glite/config.{type Config}
import glite/connection.{type QueryResult}
import glite/decode.{type RowDecoder}
import glite/error.{type Error}
import glite/internal/connection_actor.{type Message}
import glite/value.{type Value}
/// A query parameter value.
pub type Param =
Value
/// Result from a decoded query.
pub type Response(a) {
Response(rows: List(a), count: Int)
}
/// An opaque connection handle backed by an OTP actor.
pub opaque type Connection {
Connection(subject: Subject(Message), config: Config)
}
// =============================================================================
// Connection lifecycle
// =============================================================================
/// Open a SQLite database and return a connection handle.
/// The connection is managed by an OTP actor process.
pub fn connect(config: Config) -> Result(Connection, Error) {
case connection_actor.start(config) {
Ok(started) -> Ok(Connection(subject: started.data, config: config))
Error(actor.InitTimeout) ->
Error(error.ConnectionError("Connection initialization timed out"))
Error(actor.InitFailed(reason)) -> Error(error.ConnectionError(reason))
Error(actor.InitExited(_)) ->
Error(error.ConnectionError("Connection process exited during init"))
}
}
/// Close the database connection and stop the actor.
pub fn disconnect(conn: Connection) -> Nil {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.Disconnect(reply)
})
}
// =============================================================================
// Query execution
// =============================================================================
/// Execute a statement that does not return rows (CREATE, INSERT, UPDATE, DELETE).
/// Returns the number of rows affected.
pub fn exec(
conn: Connection,
sql: String,
params: List(Param),
) -> Result(Int, Error) {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.Exec(sql, params, reply)
})
}
/// Execute a statement many times with different parameter sets.
/// Prepares once, then binds+steps for each param set efficiently.
/// Returns the total number of rows affected.
pub fn exec_many(
conn: Connection,
sql: String,
params_list: List(List(Param)),
) -> Result(Int, Error) {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.ExecMany(sql, params_list, reply)
})
}
/// Execute a query and return raw results (column names + rows of Values).
pub fn query(
conn: Connection,
sql: String,
params: List(Param),
) -> Result(QueryResult, Error) {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.Query(sql, params, reply)
})
}
/// Execute a query and decode each row using the provided decoder.
pub fn query_with(
conn: Connection,
sql: String,
params: List(Param),
decoder: RowDecoder(a),
) -> Result(Response(a), Error) {
case query(conn, sql, params) {
Ok(result) ->
case decode_rows(result.rows, decoder, []) {
Ok(decoded) -> Ok(Response(rows: decoded, count: list.length(decoded)))
Error(e) -> Error(e)
}
Error(e) -> Error(e)
}
}
/// Execute a query and return the first decoded row, or error if no rows.
pub fn query_one(
conn: Connection,
sql: String,
params: List(Param),
decoder: RowDecoder(a),
) -> Result(a, Error) {
case query_with(conn, sql, params, decoder) {
Ok(response) ->
case response.rows {
[first, ..] -> Ok(first)
[] -> Error(error.DecodeError("Expected at least one row, got none"))
}
Error(e) -> Error(e)
}
}
/// Execute a function within a transaction.
/// Automatically BEGINs, and COMMITs on Ok or ROLLBACKs on Error.
pub fn transaction(
conn: Connection,
f: fn(Connection) -> Result(a, Error),
) -> Result(a, Error) {
case exec_sql(conn, "BEGIN") {
Error(e) -> Error(e)
Ok(_) -> {
case f(conn) {
Ok(val) -> {
case exec_sql(conn, "COMMIT") {
Ok(_) -> Ok(val)
Error(e) -> Error(e)
}
}
Error(e) -> {
let _ = exec_sql(conn, "ROLLBACK")
Error(e)
}
}
}
}
}
/// Get the last insert rowid.
pub fn last_insert_rowid(conn: Connection) -> Int {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.LastInsertRowid(reply)
})
}
// =============================================================================
// Parameter constructors
// =============================================================================
/// Create an integer parameter.
pub fn int(val: Int) -> Param {
value.Integer(val)
}
/// Create a float parameter.
pub fn float(val: Float) -> Param {
value.Real(val)
}
/// Create a text/string parameter.
pub fn text(val: String) -> Param {
value.Text(val)
}
/// Create a blob (binary data) parameter.
pub fn blob(val: BitArray) -> Param {
value.Blob(val)
}
/// Create a boolean parameter (stored as Integer 0/1).
pub fn bool(val: Bool) -> Param {
case val {
True -> value.Integer(1)
False -> value.Integer(0)
}
}
/// Create a NULL parameter.
pub fn null() -> Param {
value.Null
}
/// Create a nullable parameter from an Option value.
pub fn nullable(val: Option(a), to_param: fn(a) -> Param) -> Param {
case val {
Some(v) -> to_param(v)
None -> value.Null
}
}
// =============================================================================
// Helpers
// =============================================================================
fn exec_sql(conn: Connection, sql: String) -> Result(Nil, Error) {
process.call(conn.subject, conn.config.timeout, fn(reply) {
connection_actor.ExecSql(sql, reply)
})
}
fn decode_rows(
rows: List(List(Value)),
decoder: RowDecoder(a),
acc: List(a),
) -> Result(List(a), Error) {
case rows {
[] -> Ok(list.reverse(acc))
[row, ..rest] ->
case decode.run(decoder, row) {
Ok(val) -> decode_rows(rest, decoder, [val, ..acc])
Error(e) -> Error(e)
}
}
}