Skip to main content
Version: 1.2.2

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
SubcommandWhat it does
initScaffold a project (config + layout) for a chosen dialect.
migratecreate / up / down / status / version for the versioned migrator.
model generateGenerate a model struct from a table (--from-table) or an inline spec (--fields).
inspectschema / table <name> / sql introspection.
syncCheck 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.
seedcreate / run / list seed scripts.
tenantprovision / migrate / list / migrate-all for multi-tenant databases.
validateValidate a model's tags against the live schema.
genGenerate 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
The standalone binary cannot see your migrations

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.