Skip to content

Getting Started

Ahmad Faiz Kamaludin edited this page Oct 18, 2026 · 2 revisions

This page takes you from go get to running your first queries.

On this page: Install · Choose a mode · Build only · Build and run · Options · Your first queries · Next steps

Install

go get github.com/akfaiz/quill
import "github.com/akfaiz/quill/db"

quill has no third-party runtime dependencies. You bring your own database/sql driver (pgx, go-sql-driver/mysql, a SQLite driver, ...). It requires Go 1.25 or newer.

Choose a mode

There are two entry points. Both return a *db.DB.

Function Needs a connection Use it to
Build only db.Use(dialect, opts...) No Produce (sql, args) for your own driver, tests or logging
Build and run db.New(conn, dialect, opts...) Yes Build and execute

From a *db.DB you start a statement:

Start with Returns Purpose
Select(cols...).From("t") *db.SelectBuilder Read rows
Insert("t") *db.InsertBuilder Insert rows
Update("t") *db.UpdateBuilder Update rows
Delete("t") *db.DeleteBuilder Delete rows

Each builder exposes only the methods that are valid for its statement. An insert has no Where, and only a select has GroupBy.

Build only

Set the dialect once and reuse it:

pg := db.Use(db.Postgres)

sql, args, err := pg.Select().From("users").Where("id", 7).ToSQL()
// SELECT * FROM "users" WHERE "id" = $1      args: [7]

sql, args, err = pg.Update("users").Set("name", "Anna").Where("id", 7).ToSQL()
// UPDATE "users" SET "name" = $1 WHERE "id" = $2      args: [Anna 7]

Running a query on a build-only DB returns db.ErrNotBound. It never panics.

Build and run

db.New accepts anything with QueryContext and ExecContext (the db.Conn interface): *sql.DB, *sql.Tx and *sql.Conn all qualify.

import (
    "database/sql"

    "github.com/akfaiz/quill/db"
    _ "github.com/jackc/pgx/v5/stdlib" // any database/sql driver
)

sqlDB, err := sql.Open("pgx", dsn) // load dsn from config or a secret store, never hard-code it
if err != nil {
    return err
}

conn := db.New(sqlDB, db.Postgres)

Dialect constants: db.Postgres, db.MySQL, db.MariaDB, db.SQLite. The dialect decides quoting ("x" or `x`), placeholders ($1 or ?) and dialect-specific clauses. See Dialect Differences.

Tip: call the variable conn, not db. A variable named db hides the package inside its scope.

Options

Both constructors accept the same options.

Option Effect
db.WithPrefix("app_") Prefixes table names, aliases and qualified columns. From("users as u") becomes "app_users" AS "app_u"
db.WithHook(fn) Calls fn(ctx, db.QueryEvent) after every statement (logging, metrics, tracing)
db.WithLogArgs() Includes bind values in QueryEvent.Args. Off by default because arguments can be personal data
conn := db.New(sqlDB, db.Postgres,
    db.WithPrefix("app_"),
    db.WithHook(func(ctx context.Context, e db.QueryEvent) {
        log.Printf("%s args=%d took=%s err=%v", e.SQL, e.NumArgs, e.Duration, e.Err)
    }),
)

More in Hooks and Options.

Your first queries

ctx := context.Background()

type User struct {
    ID     int64
    Name   string
    Email  *string // pointer: NULL becomes nil
    Active bool
}

// Read many
var users []User
err := conn.Select().From("users").Where("active", true).OrderBy("id").Get(ctx, &users)

// Read one by id (db.ErrNotFound when missing)
var u User
err = conn.Select().From("users").Find(ctx, 7, &u)

// Insert and get the new id
id, err := conn.Insert("users").Values(map[string]any{"name": "Ann"}).ExecGetID(ctx)

// Update, returns rows affected
n, err := conn.Update("users").Set("name", "Anna").Where("id", id).Exec(ctx)

// Delete
n, err = conn.Delete("users").Where("id", id).Exec(ctx)

Every method that touches the database takes a context.Context first and honours cancellation.

Skip the destination variable

Give the struct a TableName() and use the Typed API:

type User struct {
    ID    int64   `db:"id,omitempty"`
    Name  string  `db:"name"`
    Email *string `db:"email"`
}

func (User) TableName() string { return "users" }

users, err := db.From[User](conn).Where("active", true).OrderBy("id").Get(ctx) // []User

Run several statements atomically

err := conn.Transaction(ctx, func(tx *db.DB) error {
    if _, err := tx.Update("accounts").Decrement("balance", 50).Where("id", 1).Exec(ctx); err != nil {
        return err // rolls back
    }
    _, err := tx.Update("accounts").Increment("balance", 50).Where("id", 2).Exec(ctx)
    return err // nil commits
})

Next steps

Clone this wiki locally