SQL migration, query, and codegen toolkit — your migrations are the single source of truth.
sequa is a single Go binary (sequa) plus an embeddable library (pkg/sequa).
It applies SQL migrations, gives you an interactive SQL client, and generates
type-safe Go from the schema your migrations define. The central idea:
migrations dictate codegen. The schema is assembled by parsing your
up-migrations — not by introspecting a live database — so generated models and
query methods never drift from the schema you actually ship.
sequa is Postgres-first. Today every verb targets PostgreSQL; the migration
and query layers are built on engine-agnostic foundations so more engines can be
added without reworking the design.
| Verb | Built on | Engine support today | Direction |
|---|---|---|---|
migrate |
golang-migrate | PostgreSQL (postgres://, postgresql://) |
Engine-agnostic by design; only the Postgres driver is wired up today |
query |
xo/usql | PostgreSQL | usql supports many engines; only its Postgres driver is registered today |
generate |
pg_query_go (libpg_query) |
PostgreSQL only | MySQL + SQLite planned (M5) |
verify |
pg_catalog introspection |
PostgreSQL only | Drift check: live schema vs migrations |
Codegen is Postgres-only by nature — it uses PostgreSQL's own parser. migrate
and query are Postgres-only in the current build even though their
foundations are engine-neutral.
go install github.com/inovacc/sequa/cmd/sequa@latest
cgo note: the
generateverb importspg_query_go, which is cgo — building the codegen path requires a C compiler (gcc). Themigrateandquerypaths are pure Go.
sequa init # creates ./migrations
sequa init --dir db/migrations
sequa migrate create add_users # timestamped up/down pair
sequa migrate create add_users -s # sequential numbering instead
sequa migrate up --dsn "$DATABASE_URL" # apply all pending migrations
sequa migrate down --dsn "$DATABASE_URL" # roll back the most recent migration
sequa migrate status # applied + pending, with timestamps
sequa migrate version # current schema version (+ dirty flag)
create writes a <version>_<name>.up.sql / .down.sql pair. The other
subcommands resolve the DSN from --dsn or $DATABASE_URL, and the migrations
directory from --dir or autodetection (see Configuration).
Codegen is static and offline — no database connection needed. Place a
sequa.yaml at your project root:
version: "1"
sql:
- engine: postgresql # only postgresql in this milestone
schema: internal/store/migrations # the migrations dir (schema source)
queries: internal/store/queries.sql # a .sql file OR a dir of *.sql (optional)
gen:
go:
package: db # generated package name
out: internal/store/db # output directoryAnnotate each query with sqlc-style headers in the queries file:
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListByEmail :many
SELECT id, email FROM users WHERE email = $1;
-- name: CreateUser :one
INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;| Verb | Returns |
|---|---|
:one |
(Row, error) via QueryRowContext |
:many |
([]Row, error) via QueryContext |
:exec |
error via ExecContext |
:execrows |
(int64, error) via ExecContext + RowsAffected() |
:execresult |
(sql.Result, error) via ExecContext |
Result lists may be plain columns, *, or the count/min/max/sum/avg
aggregates, typed per Postgres's promotion rules (e.g. count → non-null
int64, sum of an integer → nullable int64, avg → nullable numeric).
Multi-table JOINs are not supported yet — see ISS-2 in
docs/ISSUES.md.
Then run:
sequa generate # uses ./sequa.yaml
sequa generate --config path/to/sequa.yaml
It writes <out>/models.go (one struct per table) always, and <out>/queries.go
when queries is set. See docs/GENERATE_AND_QUERY.md
for the full type-mapping table and supported query shapes.
An interactive SQL client and REPL powered by usql (psql-style result tables and
backslash meta-commands like \dt, \d):
sequa query "postgres://user:pass@localhost/db?sslmode=disable" # REPL
sequa query --dsn "$DATABASE_URL" # REPL (env/flag DSN)
sequa query --dsn "$DATABASE_URL" -c "SELECT * FROM tasks;" # one-shot table
The DSN may be a positional argument, --dsn, or $DATABASE_URL. With -c
(--command) it runs one statement and exits; otherwise it opens the REPL.
Check that the live database schema matches what your migrations define — a drift check and migration smoke test:
sequa verify --dsn "$DATABASE_URL" # against an existing database
sequa verify --ephemeral --dsn "$DATABASE_URL" # spin up a throwaway DB, apply migrations, verify
It parses the up-migrations into a catalog, introspects the live schema via
pg_catalog, and reports missing/extra tables and columns plus type and
nullability mismatches. With --ephemeral it creates a throwaway database
(requires CREATEDB), applies the migrations to it, and verifies against that —
a zero-setup migration smoke test.
Two introspection commands that describe the CLI itself — handy for docs and for feeding AI assistants a current command reference:
sequa cmdtree # compact ASCII tree of every command
sequa cmdtree --full # per-command usage, description, and flags
sequa cmdtree --command migrate # details for a single command
sequa cmdtree --json # machine-readable tree
sequa aicontext # Markdown reference of all commands + flags
sequa aicontext --compact # one line per command
sequa aicontext --json # structured JSON
Because the output is generated from the live command tree, it never goes stale.
Apply your own embedded migrations on application startup:
import (
"context"
"embed"
"github.com/inovacc/sequa/pkg/sequa"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func main() {
m, err := sequa.New(dsn, migrationsFS, "migrations")
if err != nil {
// handle err
}
ctx := context.Background()
if err := m.Up(ctx); err != nil {
// handle err
}
}New(dsn string, fsys fs.FS, subdir string) returns a *Migrator with:
Up(ctx) error— apply all pending migrationsDown(ctx) error— roll back the most recent migrationVersion(ctx) (uint, bool, error)— current schema version and dirty flag
The filesystem's subdir holds <version>_<name>.up.sql / .down.sql pairs.
Persistent flags (available on every command):
| Flag | Purpose |
|---|---|
--dsn |
Database DSN. Falls back to $DATABASE_URL when empty. |
--dir |
Migrations directory. Autodetected when empty. |
-v, --verbose |
Verbose (debug) logging. |
- DSN resolves as
--dsn→$DATABASE_URL. Forquery, a positional DSN argument takes precedence. - Directory autodetection scans, in order:
migrations/,db/migrations/,sql/migrations/,database/migrations/— picking the first that exists and contains at least one*.up.sqlfile. - Logs are written to stderr as JSON; stdout is reserved for query and data output.
The project uses a Taskfile:
task build # go build ./...
task test # go test -short ./... (unit tests only)
task test:cover # short tests + total coverage
task test:full # go test ./... (needs SEQUA_TEST_DATABASE_URL)
task lint # golangci-lint run ./...
task test:docker # full suite incl. integration, in Docker vs real Postgres
-short gating. Tests that need a real database call t.Skip() under
testing.Short() and when SEQUA_TEST_DATABASE_URL is unset. So task test
(which passes -short) runs only unit tests; the integration tests run via
task test:full (with SEQUA_TEST_DATABASE_URL pointing at a Postgres) or via
task test:docker.
Docker integration path. task test:docker brings up postgres:16-alpine,
waits for health, and runs the whole suite from a cgo-capable golang:1.26 image
(gcc is required for pg_query_go). Package test binaries run with -p 1 so the
integration tests, which share one Postgres, don't race on schema creation.
| Milestone | Scope | State |
|---|---|---|
| M1 | Spine + migrate — config autodetect, DB connection, golang-migrate runner (up/down/status/version), library API |
Done |
| M2 | query — embedded usql REPL and one-shot command |
Done |
| M3 | generate (Postgres) — models + typed query methods from the migration-defined schema |
Done |
| M4 | verify — introspection + drift diff (live schema vs migrations) |
Done |
| M5 | Engines 2 & 3 — MySQL + SQLite codegen behind an Engine abstraction |
Planned |
Full documentation is indexed in docs/README.md — design, the generate/query guide, roadmap, backlog, known issues, and feature specs. Contributing: CONTRIBUTING.md.
BSD 3-Clause. See LICENSE.