Operational Workflows
Quark ships a cmd/quark binary with nine subcommands
(go install github.com/jcsvwinston/quark/cmd/quark@latest):
| Subcommand | What it does |
|---|---|
init | Scaffold a project (config + layout) for a chosen dialect. |
gen | Generate typed accessors / binders (see Code Generation). |
model generate | Generate a model struct from an existing table. |
migrate | create / up / down / status / version for the versioned migrator. |
sync | Diff the live schema against your models (--dry-run to preview). |
seed | create / run / list seed scripts. |
inspect | schema / table <name> / sql introspection. |
tenant | provision / migrate / list / migrate-all for multi-tenant DBs. |
validate | Validate a model's tags against the live schema. |
You can also embed the same operations as small Go commands that call the public APIs your application uses — handy when you want them inside your own binary or CI. The patterns below show that approach.
Migration Runner
Create a command that imports your migration package for side effects and runs the versioned migrator:
// cmd/migrate/main.go
package main
import (
"context"
"flag"
"log"
"github.com/jcsvwinston/quark"
"github.com/jcsvwinston/quark/migrate"
_ "github.com/lib/pq"
_ "your/app/migrations"
)
func main() {
var dsn string
var down int
var dryRun bool
flag.StringVar(&dsn, "dsn", "", "database DSN")
flag.IntVar(&down, "down", 0, "number of migrations to roll back")
flag.BoolVar(&dryRun, "dry-run", false, "preview pending migrations")
flag.Parse()
limits := quark.DefaultLimits()
limits.AllowRawQueries = true
client, err := quark.New("postgres", dsn,
quark.WithLimits(limits),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
migrator := migrate.NewMigrator(client)
switch {
case dryRun:
err = migrator.UpDryRun(ctx, 0)
case down > 0:
err = migrator.Down(ctx, down)
default:
err = migrator.Up(ctx, 0)
}
if err != nil {
log.Fatal(err)
}
}
Run it with:
go run ./cmd/migrate -dsn "$DATABASE_URL" -dry-run
go run ./cmd/migrate -dsn "$DATABASE_URL"
go run ./cmd/migrate -dsn "$DATABASE_URL" -down 1
Schema Bootstrap Command
For local development or tests, a bootstrap command can call Migrate directly:
// cmd/schema-bootstrap/main.go
package main
func main() {
ctx := context.Background()
client := mustClient()
if err := client.Migrate(ctx, &User{}, &Order{}, &Product{}); err != nil {
log.Fatal(err)
}
}
This is intentionally different from production migrations. Migrate reflects
the current model shape and creates missing tables; versioned migrations record
exactly which DDL ran and in what order.
Schema Sync Preview
Use Sync dry runs to see additive/rename/drop operations against an existing
table:
err := client.Sync(ctx, quark.SyncOptions{DryRun: true}, &User{})
Because dry runs introspect the live table, the table must already exist. Use a logger on the client to capture the planned SQL:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
client, err := quark.New("postgres", dsn,
quark.WithLogger(logger),
)
Tenant Migration Loop
For database-per-tenant routing, keep tenant migration explicit. Resolve tenant DSNs from your control plane, create a client per tenant, and run the same migrator.
for _, tenant := range tenants {
client, err := clientForTenant(tenant)
if err != nil {
return err
}
migrator := migrate.NewMigrator(client)
if err := migrator.Up(ctx, 0); err != nil {
_ = client.Close()
return fmt.Errorf("tenant %s: %w", tenant.ID, err)
}
_ = client.Close()
}
For schema-per-tenant, run schema creation and DDL with a connection configured
for the target schema, or issue explicit schema-qualified SQL from a versioned
migration. The current TenantRouter uses tenant IDs as schema names during
queries, but it does not orchestrate schema provisioning.
Model Generation and Introspection
The CLI generates Go model structs, either from an existing database (database-first) or from an inline field definition.
From existing tables — introspects the configured database and emits one struct per table:
quark model generate --from-table users,orders --out ./models --package models
Each table is introspected through the information schema and written as a struct
with db: tags and a TableName() method. SQL types are mapped to Go types:
nullable columns become pointers, JSON/JSONB becomes json.RawMessage, and
timestamp/date columns become time.Time. Review the result before use — the
mapping is a starting point, not a substitute for modelling decisions (rich
types, relations, soft-delete, optimistic locking).
From an inline definition — no database needed:
quark model generate Product --fields "id:int64,name:string,price:float64" --out ./models
The --out directory is created if it does not exist, and a generation failure
(malformed --fields, unreachable database, unwritable output) exits non-zero.
Flags: --from-table, --fields, --out (default ./models), --package
(default models), --dialect, --tags (default json).
Quark's internal tests use metadata inspection heavily; application tooling can also inspect Quark's interpretation of a model:
meta := quark.GetModelMeta[User]()
fmt.Println(meta.Table)
for _, field := range meta.Fields {
fmt.Println(field.Column, field.IsPK)
}
Recommended Layout
your-app/
cmd/
migrate/
main.go
schema-bootstrap/
main.go
internal/
db/
client.go
migrations/
202605050001_create_users.go
Use Go commands for operational repeatability, commit them with your app, and run them from CI or deployment jobs.