Skip to content

justin4957/cquill

cquill

A compile-time safe data access library for Gleam.

Package Version Hex Docs

"Ecto, but scaled down and typed, for Gleam" — Schema-like types, composable queries, and adapter-based persistence without locking into any particular database.

Design Philosophy

  • Compile-time safety over runtime convenience — Invalid queries should fail at compile time
  • Explicit over implicit — No magic; transformations are visible and traceable
  • Gleam-idiomatic — Leverage Result types, pipelines, and the module system naturally
  • Adapter-first — Define persistence boundaries early; real DBs are just one adapter
  • Small, composable modules — Each module has one responsibility

Architecture

cquill follows a layered architecture with clear boundaries:

┌─────────────────────────────────────────┐
│           Your Application              │
├─────────────────────────────────────────┤
│  Schema    │  Changeset  │    Query     │  ← Pure, no I/O
│  (types)   │ (validation)│   (builder)  │
├─────────────────────────────────────────┤
│                  Repo                   │  ← Public API
├─────────────────────────────────────────┤
│  Memory    │  Postgres   │   (Future)   │  ← Adapters
│  Adapter   │  Adapter    │   Adapters   │
└─────────────────────────────────────────┘

Installation

gleam add cquill

Quick Start

Defining Schemas

import cquill/schema
import cquill/schema/field

// Define your schema - this describes the table structure
let user_schema = schema.new("users")
  |> schema.add_field(field.integer("id") |> field.primary_key)
  |> schema.add_field(field.string("email") |> field.not_null)
  |> schema.add_field(field.string("name") |> field.nullable)
  |> schema.add_field(field.boolean("active") |> field.not_null)
  |> schema.add_field(field.integer("age") |> field.nullable)

Building Queries

import cquill/query

// Build queries using composable pipelines
let active_users = query.from(user_schema)
  |> query.where(query.eq_bool("active", True))
  |> query.order_by_desc("created_at")
  |> query.limit(10)

// Queries are just data - inspect them for debugging
let debug_str = query.to_debug_string(active_users)

Executing Queries (Memory Adapter)

The memory adapter is perfect for testing and development:

import cquill/adapter
import cquill/adapter/memory
import gleam/dynamic

pub fn example() {
  // Create an in-memory store with a table and column metadata
  // Column names enable WHERE clause filtering beyond just the primary key
  let store = memory.new_store()
    |> memory.create_table_with_columns("users", "id", [
      "id", "email", "name", "active", "age",
    ])

  // Insert data (column order must match the columns list above)
  let row = [
    dynamic.int(1),
    dynamic.string("[email protected]"),
    dynamic.string("Alice"),
    dynamic.bool(True),
    dynamic.int(30),
  ]
  let assert Ok(store) = memory.insert_row(store, "users", "1", row)

  // Query using the adapter - WHERE clauses filter by any column
  let adp = memory.memory_adapter()
  let compiled = adapter.CompiledQuery(
    sql: "SELECT * FROM users WHERE active = $1",
    params: [adapter.ParamBool(True)],
    expected_columns: 5,
  )

  case adapter.query(adp, store, compiled) {
    Ok(rows) -> // rows is List(List(Dynamic)) - filtered by active = True
    Error(err) -> // handle error
  }
}

Validating Data with Changesets

import cquill/changeset
import gleam/dict
import gleam/dynamic
import gleam/option.{Some}

pub fn validate_user(data: Dict(String, Dynamic)) {
  changeset.new(data)
    |> changeset.validate_required(["email", "name"])
    |> changeset.validate_format("email", "^[^@]+@[^@]+$")
    |> changeset.validate_length("name", min: 2, max: 100)
    |> changeset.validate_number_range("age", min: Some(0), max: Some(150))
    |> changeset.apply()
}

Key Features

  • Schemas as data — Define structure without coupling to persistence
  • Composable queries — Build complex queries from simple, reusable parts
  • Changesets — Validate and transform data before persistence
  • Adapter abstraction — Same API works with Postgres, in-memory, or custom backends
  • Testable by design — Use in-memory adapter for fast, isolated tests

Database Migrations

cquill focuses on runtime data access, not schema evolution. We recommend using dedicated migration tools:

Tool Best For Installation
dbmate Simple SQL migrations brew install dbmate
sqitch Complex dependency chains brew install sqitch
flyway Enterprise environments brew install flyway

Quick Start with dbmate

# Create a migration
dbmate new add_users_table

# Apply migrations
dbmate up

# Regenerate cquill types
gleam run -m cquill_cli generate

See docs/MIGRATIONS.md for the complete migration guide, including:

  • CI/CD integration examples
  • Schema drift detection
  • Makefile templates
  • Best practices

Status

This library is currently in early development. See the GitHub Issues for the development roadmap.

Roadmap

  • Phase 0: Foundation & Research
  • Phase 1: Core Query Execution
  • Phase 2: Code Generation (MVP)
  • Phase 3: Type-Safe Query Builder
  • Phase 4: Transactions & Advanced Features
  • Phase 5: Developer Experience

Development

gleam test           # Run all tests
gleam format         # Format code
gleam docs build     # Build documentation

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

License

Apache-2.0

About

A compile-time safe database library for Gleam

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages