common_sql

A minimal database driver abstraction layer for Gleam.

common_sql defines a shared interface — types and functions — that is driver-agnostic. Pick a driver package for your database and pass it to common_sql functions. Your application code only ever imports common_sql.

Available drivers

Package Database Hex link
common_sql_sqllite SQLite (via sqlight) hex.pm/packages/sqlight
common_sql_postgresql PostgreSQL (via pog) hex.pm/packages/pog

Usage

gleam add common_sql common_sql_sqllite
# or
gleam add common_sql common_sql_postgresql

SQLite example

import common_sql as sql
import common_sql_sqllite
import gleam/dynamic/decode

type User {
  User(id: Int, name: String)
}

pub fn main() {
  let driver = common_sql_sqllite.driver()

  let user_decoder = {
    use id <- decode.field(0, decode.int)
    use name <- decode.field(1, decode.string)
    decode.success(User(id:, name:))
  }

  // with_connection opens the connection, runs the callback, then closes
  // automatically — even if the callback returns an error.
  use conn <- sql.with_connection(driver, "file:mydb.sqlite3")
  sql.execute(
    driver,
    conn,
    sql.Sql("SELECT id, name FROM users WHERE id = ?"),
    [sql.PInt(1)],
    user_decoder,
  )
}

PostgreSQL example

import common_sql as sql
import common_sql_postgresql
import gleam/dynamic/decode

type User {
  User(id: Int, name: String)
}

pub fn main() {
  let driver = common_sql_postgresql.driver()

  let user_decoder = {
    use id <- decode.field(0, decode.int)
    use name <- decode.field(1, decode.string)
    decode.success(User(id:, name:))
  }

  use conn <- sql.with_connection(driver, "postgres://user:pass@localhost/mydb")
  sql.execute(
    driver,
    conn,
    sql.Portable("SELECT id, name FROM users WHERE id = $1"),
    [sql.PInt(1)],
    user_decoder,
  )
}

If you need the connection object beyond a single block, use connect and close directly:

let assert Ok(conn) = sql.connect(driver, "postgres://localhost/mydb")
// ... multiple queries ...
sql.close(driver, conn)

Parameters

Use the Param type to pass values to queries:

Constructor Gleam type
PInt(Int)Int
PString(String)String
PFloat(Float)Float
PBool(Bool)Bool
PNull SQL NULL

SQL and portability

common_sql.execute accepts a Query value instead of a plain string:

Constructor Description
Sql(String) Driver-native SQL, passed as-is. Use ? for SQLite or $1 for PostgreSQL.
Portable(String) SQL with PostgreSQL-style $1, $2, … placeholders. Drivers that need a different syntax (e.g. SQLite) convert them automatically.

Use Portable when you want a single query string that works with any driver:

// Works with both SQLite and PostgreSQL drivers:
sql.execute(driver, conn, sql.Portable("SELECT id FROM users WHERE id = $1"), [sql.PInt(1)], decode.int)

Limitations of sql.Portable()

For sqlite, $N inside SQL string literals is not distinguished from a real placeholder.

Error handling

All fallible operations return Result(_, DbError):

pub type DbError {
  QueryError(String)       // query failed or row decode failed
  ConnectionError(String)  // could not establish a connection
}

Implementing a new driver

A driver is a Driver(conn) value — a record of three functions generic over the driver's own opaque connection type.

import common_sql.{Driver, DbError, Param, QueryError, ConnectionError}
import gleam/dynamic

pub opaque type Conn {
  Conn(// ... internal handle ...)
}

pub fn driver() -> Driver(Conn) {
  Driver(
    driver_type: "mydb",
    connect: fn(url) {
      // Open a real connection, return Ok(Conn(...)) or Error(ConnectionError(...))
      todo
    },
    execute: fn(conn, query, params) {
      // Run the query, return Ok(List(dynamic.Dynamic)) or Error(QueryError(...))
      // `query` is common_sql.Sql(sql_string) or common_sql.Portable(sql_string).
      // Extract the SQL string and convert placeholders if your driver requires it.
      // Do NOT decode rows here — return raw Dynamic values.
      todo
    },
    close: fn(conn) {
      // Tear down the connection, return Nil.
      // This is always called by with_connection after the callback finishes.
      todo
    },
  )
}

Responsibilities

Concern Where it lives
Connection handling Driver package
Param marshalling Driver package
Row decoding common_sql (execute)
Error normalisation Driver package → DbError

Drivers return List(dynamic.Dynamic) — one Dynamic per row. common_sql.execute applies the caller-supplied decode.Decoder(a) to each row and collects the results.

Design notes

Development

gleam test   # run the test suite
gleam build  # compile the package

Licence

MIT