A Gleam DuckDB CLI wrapper

README.md

qwak #

qwak is a wrapper for the DuckDB CLI for Gleam. For now only works in Erlang, but if there's interest a Node version may be added.

Installation #

It's not in Hex yet, so to use it, add the library's git repo URI to your gleam.toml:

[dependencies]
qwak = { git = "https://tangled.org/josm.me/qwak", ref = "main" }

Usage #

import gleam/dynamic/decode
import gleam/erlang/process
import gleam/option.{type Option, None}
import gleam/otp/actor
import gleam/time/calendar
import qwak

pub type Artist {
  Artist(
    name: String,
    genres: List(String),
    location: String,
    started: calendar.Date,
    ended: Option(calendar.Date),
  )
}

pub fn main() {
  let name = process.new_name("qwak")
  let config = qwak.default_config()
  let assert Ok(actor.Started(_, conn)) = qwak.start(name, config)

  let assert Ok(_) =
    qwak.query(
      "create table artists (
        name text not null
      , genres text[] not null
      , location text not null
      , started date not null
      , ended date
      );
    ",
    )
    |> qwak.run(conn)

  let assert Ok(_) =
    qwak.query(
      "insert into artists (name, genres, location, started, ended) values 
        ('Say Sue Me', ['surf rock', 'shoegaze'], 'Busan, South Korea', '2012-06-01', null)
      , ('Cactus For Breakfast', ['surf rock', 'psych rock'], 'Berlin, Germany', '2024-02-01', null)
      , ('The Cranberries', ['dream pop', 'post punk'], 'Limerick, Ireland', '1989-05-01', '2019-04-26')
      , ('Fazerdaze', ['dream pop', 'bedroom pop'], 'New Zealand', '2014-08-01', null)
      , ('Leenalchi', ['pansori pop'], 'Seoul, South Korea', '2019-01-01', null)
      ;",
    )
    |> qwak.run(conn)

  let artist_decoder = {
    use name <- decode.field("name", decode.string)
    use genres <- decode.field("genres", decode.list(decode.string))
    use location <- decode.field("location", decode.string)
    use started <- decode.field("started", qwak.date_decoder())
    use ended <- decode.field("ended", decode.optional(qwak.date_decoder()))

    decode.success(Artist(name:, genres:, location:, started:, ended:))
  }

  let assert Ok(qwak.Rows(rows)) =
    qwak.query("select * from artists where ended is null")
    |> qwak.with_decoder(artist_decoder)
    |> qwak.run(conn)

  assert rows == [
    Artist("Say Sue Me", ["surf rock", "shoegaze"], "Busan, South Korea", calendar.Date(2012, calendar.June, 1), None),
    Artist("Cactus For Breakfast", ["surf rock", "psych rock"], "Berlin, Germany", calendar.Date(2024, calendar.February, 1), None),
    Artist("Fazerdaze", ["dream pop", "bedroom pop"], "New Zealand", calendar.Date(2014, calendar.August, 1), None),
    Artist("Leenalchi", ["pansori pop"], "Seoul, South Korea", calendar.Date(2019, calendar.January, 1), None),
  ]
}

Within an application #

You can create a supervisor qwak actor using qwak.supervised(), and add it to the rest of your supervision tree before anything that needs to use it:

import qwak
import gleam/erlang/process
import my/server

pub fn main() {
  let name = process.new_name("qwak")
  let config = qwak.default_config()
  let duckdb = qwak.supervised(name, config)

  let connection = qwak.connection_from_name(name)

  let ctx = server.Context(connection)
  let server = server.supervised(ctx)

  let assert Ok(supervisor) =
    supervisor.new(supervisor.RestForOne)
    |> supervisor.add(duckdb)
    |> supervisor.add(server)
    |> supervisor.start
}

You can then use it in your request handlers:

fn handle_query(ctx: Context) {
  let Context(connection) = ctx

  let result qwak.query("select * from artists where ended is null")
    |> qwak.with_decoder(artist_decoder)
    |> qwak.run(conn)

  case result {
    Ok(qwak.Rows(rows)) -> display_table(rows)
    Error(e) -> display_error(e)
  }
}

Further documentation can be found at https://hexdocs.pm/qwak.

Development #

gleam run   # Run the project
gleam test  # Run the tests