ducky

Native DuckDB driver for Gleam.

Quick Start

import ducky
import gleam/int
import gleam/io

pub fn main() {
  use conn <- ducky.with_connection(":memory:")

  // Create our duck pond
  let assert Ok(_) = ducky.query(conn, "
    CREATE TABLE ducks (name TEXT, quack_volume INT, is_rubber BOOLEAN)
  ")
  let assert Ok(_) = ducky.query(conn, "
    INSERT INTO ducks VALUES
      ('Sir Quacksalot', 95, false),
      ('Duck Norris', 100, false),
      ('Mallard Fillmore', 72, false),
      ('Squeaky', 0, true)
  ")

  // Find the loudest quacker
  let assert Ok(result) = ducky.query(conn, "
    SELECT name, quack_volume FROM ducks
    WHERE is_rubber = false
    ORDER BY quack_volume DESC LIMIT 1
  ")

  case result.rows {
    [ducky.Row([ducky.Text(name), ducky.Integer(volume)])] ->
      io.println(name <> " wins at " <> int.to_string(volume) <> " decibels!")
    _ -> io.println("The pond is empty...")
  }
}
// => Duck Norris wins at 100 decibels!

Types

An opaque connection to a DuckDB database.

pub opaque type Connection

A complete query result with column metadata.

pub type DataFrame {
  DataFrame(columns: List(String), rows: List(Row))
}

Constructors

  • DataFrame(columns: List(String), rows: List(Row))

Errors that can occur during DuckDB operations.

pub type Error {
  ConnectionFailed(reason: String)
  QuerySyntaxError(message: String)
  Timeout(duration_ms: Int)
  TypeMismatch(expected: String, got: String)
  UnsupportedParameterType(type_name: String)
  DatabaseError(message: String)
}

Constructors

  • ConnectionFailed(reason: String)

    Connection to database failed.

  • QuerySyntaxError(message: String)

    SQL query has syntax errors.

  • Timeout(duration_ms: Int)

    Operation timed out.

  • TypeMismatch(expected: String, got: String)

    Type conversion failed.

  • UnsupportedParameterType(type_name: String)

    Unsupported parameter type in query.

  • DatabaseError(message: String)

    Generic error from DuckDB.

A single row from a query result.

pub type Row {
  Row(values: List(Value))
}

Constructors

  • Row(values: List(Value))

A value from a DuckDB result set.

pub type Value {
  Null
  Boolean(Bool)
  TinyInt(Int)
  SmallInt(Int)
  Integer(Int)
  BigInt(Int)
  Float(Float)
  Double(Float)
  Decimal(String)
  Text(String)
  Blob(BitArray)
  Timestamp(Int)
  Date(Int)
  Time(Int)
  Interval(months: Int, days: Int, nanos: Int)
  List(List(Value))
  Array(List(Value))
  Map(dict.Dict(String, Value))
  Struct(dict.Dict(String, Value))
}

Constructors

  • Null
  • Boolean(Bool)
  • TinyInt(Int)
  • SmallInt(Int)
  • Integer(Int)
  • BigInt(Int)
  • Float(Float)
  • Double(Float)
  • Decimal(String)
  • Text(String)
  • Blob(BitArray)
  • Timestamp(Int)
  • Date(Int)
  • Time(Int)
  • Interval(months: Int, days: Int, nanos: Int)
  • List(List(Value))
  • Array(List(Value))
  • Map(dict.Dict(String, Value))
  • Struct(dict.Dict(String, Value))

Values

pub fn close(conn: Connection) -> Result(Nil, Error)

Closes a database connection.

Examples

let assert Ok(conn) = connect(":memory:")
let assert Ok(_) = close(conn)
pub fn connect(path: String) -> Result(Connection, Error)

Opens a connection to a DuckDB database.

Must call close() when done. Use with_connection() instead for automatic cleanup.

Examples

connect(":memory:")
// => Ok(Connection(...))

connect("data.duckdb")
// => Ok(Connection(...))
pub fn field(value: Value, name: String) -> option.Option(Value)

Get a field value from a struct by field name.

Returns None if the value is not a Struct or the field does not exist.

Examples

let person = Struct(dict.from_list([#("name", Text("Alice")), #("age", Integer(30))]))
field(person, "name")
// => Some(Text("Alice"))

field(person, "unknown")
// => None
pub fn get(row: Row, index: Int) -> option.Option(Value)

Get a value from a row by column index.

Examples

let row = Row([Integer(1), Text("Alice")])
get(row, 0)
// => Some(Integer(1))

get(row, 5)
// => None
pub fn path(conn: Connection) -> String

Returns the database path for a connection.

pub fn query(
  conn: Connection,
  sql: String,
) -> Result(DataFrame, Error)

Executes a SQL query and returns structured results.

The query runs on a dirty scheduler to avoid blocking the BEAM. Results are loaded into memory. For large datasets, use LIMIT/OFFSET or filter in SQL to reduce memory usage.

Examples

query(conn, "SELECT id, name FROM users WHERE active = true")
// => Ok(DataFrame(columns: ["id", "name"], rows: [...]))

// For large datasets, paginate:
query(conn, "SELECT * FROM users LIMIT 1000 OFFSET 0")
pub fn query_params(
  conn: Connection,
  sql: String,
  params: List(Value),
) -> Result(DataFrame, Error)

Executes a parameterized SQL query with bound parameters to prevent SQL injection.

Examples

query_params(conn, "SELECT * FROM users WHERE id = ? AND age > ?", [
  Integer(42),
  Integer(18),
])
// => Ok(DataFrame(...))

Security

Always use this function when including user input in queries:

// UNSAFE - SQL injection risk
query(conn, "SELECT * FROM users WHERE name = '" <> user_input <> "'")

// SAFE - parameters are properly escaped
query_params(conn, "SELECT * FROM users WHERE name = ?", [Text(user_input)])
pub fn transaction(
  conn: Connection,
  callback: fn(Connection) -> Result(a, Error),
) -> Result(a, Error)

Executes operations within a transaction.

Commits on success, rolls back on error.

Examples

transaction(conn, fn(conn) {
  use _ <- result.try(query(conn, "UPDATE accounts ..."))
  query(conn, "SELECT * FROM accounts")
})
pub fn with_connection(
  db_path: String,
  callback: fn(Connection) -> Result(a, Error),
) -> Result(a, Error)

Executes operations with automatic connection cleanup.

Connection closes automatically on success or error.

Examples

use conn <- with_connection(":memory:")
query(conn, "SELECT 42")
Search Document