Operational Workflows
Quark ships a quark binary for the operational tasks around your app:
migrations, schema inspection, model generation, seeding, and multi-tenant jobs.
go install github.com/jcsvwinston/quark/cmd/quark@latest
| Subcommand | What it does |
|---|---|
init | Scaffold a project (config + layout) for a chosen dialect. |
migrate | create / up / down / status / version for the versioned migrator. |
model generate | Generate a model struct from a table (--from-table) or an inline spec (--fields). |
inspect | schema / table <name> / sql introspection. |
sync | Check the connection and print how to wire client.Sync(...) — the diff/apply itself needs your compiled model types, so it runs in your app, not the CLI. |
seed | create / run / list seed scripts. |
tenant | provision / migrate / list / migrate-all for multi-tenant databases. |
validate | Validate a model's tags against the live schema. |
gen | Generate typed accessors / binders (see Code Generation). |
The CLI reads its connection from a .quark.yml config (created by quark init)
or from QUARK_DATABASE_DEFAULT_DRIVER / QUARK_DATABASE_DEFAULT_DSN.
Migrations from the CLI
quark migrate create add_users_table # scaffold a new migration file
quark migrate up # apply pending migrations
quark migrate status # show applied vs pending
quark migrate down # roll back the last migration
Migration files register themselves via init(), which only runs when their
package is compiled into the executing binary — a go installed quark
never imports your project's migrations/ package. migrate up/down (and
seed run) therefore exit non-zero with an explanation when the registry
is empty, instead of reporting a bogus "No pending migrations". To run them
for real, build the two-line runner shown in
Embedding the same operations in your own binary:
your main imports the migrations package for its side effects and calls
commands.Execute().
Generating models from an existing database
model generate is database-first or spec-first. From existing tables, it
introspects the database and writes one struct per table:
quark model generate --from-table users,orders --out ./models --package models
Nullable columns become pointers, JSON/JSONB becomes json.RawMessage, and
timestamp columns become time.Time. Treat the output as a starting point —
review it before adding rich types, relations, soft-delete, or optimistic
locking. Or define a model inline, no database needed:
quark model generate Product --fields "id:int64,name:string,price:float64" --out ./models
Flags: --from-table, --fields, --out (default ./models), --package
(default models), --dialect, --tags (default json). The --out directory
is created if missing, and a malformed spec or unwritable output exits non-zero.
Inspecting and validating
quark inspect schema # list tables and columns
quark inspect table users # describe one table
quark validate users --models ./models # compare the User struct against the users table
validate loads your structs with go/packages (--models defaults to
paths.models from .quark.yml, else ./...), matches the table to a model
by pluralized snake_case name (or take --model User explicitly), and reports
the mapping in both directions: a field whose column is missing in the
database always exits non-zero; DB columns unmapped in Go exit non-zero under
--strict. inspect table and model generate --from-table also exit
non-zero when the table doesn't exist, instead of printing an empty report.
inspect schema and inspect table take --format table|json|yaml for
machine-readable output. Schema sync (auto-migration) is a programmatic API —
it needs your compiled model types, so it can't run from a standalone CLI.
quark sync just checks the connection and prints the call to embed:
// DryRun previews the SQL without applying it.
err := client.Sync(ctx, quark.SyncOptions{DryRun: true}, &User{}, &Order{})
Embedding the same operations in your own binary
When you'd rather run these from your own service or CI binary, call the public APIs directly. A migration runner imports your migration package for its side effects and drives the versioned migrator:
// cmd/migrate/main.go
limits := quark.DefaultLimits()
limits.AllowRawQueries = true
client, err := quark.New("pgx", dsn, quark.WithLimits(limits))
if err != nil {
log.Fatal(err)
}
defer client.Close()
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)
}
For a dev/test schema bootstrap, call Migrate directly (it reflects the current
model shape and creates missing tables — different from versioned migrations,
which record exactly which DDL ran):
err := client.Migrate(ctx, &User{}, &Order{}, &Product{})
For database-per-tenant, quark tenant migrate <id> / migrate-all resolve
each tenant's DSN from tenant.dsn_template in .quark.yml — the tenant id
replaces the {tenant} placeholder:
tenant:
strategy: db_per_tenant
dsn_template: postgres://user:pass@localhost/{tenant}?sslmode=disable
Without a template the command fails instead of guessing (it used to migrate
the default database, whatever tenant you named). schema_per_tenant
migrations are not resolvable from static config and exit with an explicit
error — run those from your own binary. In code, the same loop stays explicit —
resolve each tenant's DSN, build a client, run the same migrator:
for _, tenant := range tenants {
client, err := clientForTenant(tenant)
if err != nil {
return err
}
if err := migrate.NewMigrator(client).Up(ctx, 0); err != nil {
_ = client.Close()
return fmt.Errorf("tenant %s: %w", tenant.ID, err)
}
_ = client.Close()
}
(TenantRouter uses tenant IDs as schema names at query time but doesn't
orchestrate schema provisioning — see Multi-Tenant.)
Commit these commands with your app and run them from CI or deployment jobs.