Skip to main content
Version: 1.17.0

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

The CLI is its own Go module, so @latest resolves its tags (cmd/quark/vX.Y.Z) rather than the library's, and its version numbers are their own series. quark version prints both — the CLI's, and the Quark release built into it:

quark v0.1.0 linux/amd64 (go1.26.6)
built against github.com/jcsvwinston/quark v1.13.0

Releases cut from the first signed tag onward also publish prebuilt archives — Linux, macOS and Windows, amd64 and arm64 — each with an SPDX SBOM beside it, a checksum file, a cosign signature over that file and a build provenance attestation. Take one from the releases page if you would rather not build from source, and check it before you run it: Verifying a release is the two commands.

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; install-rls-policies / verify-rls-policies for PostgreSQL row-level security.
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. When a key is missing the error names the exact keys, the env vars, and quark init, so you are never left guessing what to set.

Starting a project​

quark init --dialect postgresql

init scaffolds the layout (models/, migrations/, seeders/), a .quark.yml, and the small runner your project needs (cmd/<app>/main.go, described below). If the directory is not already inside a Go module it also writes a go.mod so the runner's imports resolve — pass --module your/path to choose the import path, or edit the generated go.mod afterwards. When the directory is already inside a module, init leaves it untouched and derives the import path from it.

It finishes by printing the real next steps, in order:

Next steps:
1. Edit go.mod if you want a different module path.
2. go get github.com/jcsvwinston/quark@latest
3. quark migrate create initial_schema --from-models ./models --dialect postgresql
4. go run ./cmd/<app> migrate up

The runner is what runs migrations and seeders — the standalone quark binary cannot (see below).

With a framework​

quark init --dialect sqlite --with chi # or echo, gin, grpc, nucleus

--with adds the code of one section of Web Frameworks to the layout above: the framework's package as internal/<app>, the notes model and its error-to-status mapping as internal/notes, and the driver module of the chosen dialect blank-imported, so a duplicate key answers 409 (or AlreadyExists). It is not a template written beside the guide: the CLI carries copies of the packages CI builds against each framework and tests over the network, and changes only their package name, their doc comment and their import paths.

--withwritesserve it with
chiinternal/<app>/chi.gogo run ./cmd/<app>-server (:8080)
echointernal/<app>/echo.go (Echo v5)go run ./cmd/<app>-server (:8080)
gininternal/<app>/gin.gogo run ./cmd/<app>-server (:8080)
grpcinternal/<app>/grpc.go and the service in internal/<app>/notespb (.proto, generated code, buf.gen.yaml)go run ./cmd/<app>-server (:50051)
nucleusinternal/<app>/module.go, a Nucleus module, and nucleus.ymlthe mount line in your main.go

cmd/<app>-server/main.go opens the process's one client on the database .quark.yml was written with (QUARK_DATABASE_DEFAULT_DSN points it at another, as it does for the runner), migrates the model, and serves until it is interrupted; ADDR changes the listen address. nucleus.yml is the minimum FromConfigFile needs to boot: the default database (the same one .quark.yml names, in the URL form Nucleus reads — sqlite://myapp.db, postgres://…, mysql://…), host, port and env: development. No file that exists is overwritten, and the runner and the rest of the layout are unchanged.

init also writes the requirements into go.mod: the framework at the version CI tests these packages with, and github.com/jcsvwinston/quark and github.com/jcsvwinston/quark/cmd/quark at the versions the quark binary you ran was built from. A requirement the go.mod already has is kept at its version; a development build of the CLI has no release to pin and leaves the two Quark modules to go mod tidy, which resolves their latest release. The next steps then start with one command instead of the go gets:

2. go mod tidy # fetches what go.mod requires, and the sqlite driver module
3. go run ./cmd/shop-server # serves internal/shop with chi on :8080; QUARK_DATABASE_DEFAULT_DSN picks the database

For Nucleus, main.go is the host application's, so step 3 is the mount line instead:

3. In main: client, err := quark.New("sqlite", dsn) and nucleus.New().FromConfigFile("nucleus.yml").Mount(shop.Module(client))
nucleus.yml is written here and names the .quark.yml database; edit both if you change it.
For the whole application generated around this module (config, policy file, admin panel):
nucleus new shop --with quark

Quark has no dependency on any of these frameworks: the CLI writes their code as text, and the build that checks it runs in Quark's CI, where each target is scaffolded into a project of its own and built, vetted and tested. --with takes one framework per project, and an unknown one fails before anything is written, like an unknown dialect.

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 same static reader lets the binary diff your models against the live database without compiling them in:

quark migrate diff --from-models ./models # the pending operations, human-readable
quark migrate plan --from-models ./models # alias of diff
quark migrate verify --from-models ./models # exit 1 on drift — a CI gate

The structs are read with go/packages, mapped with the runtime's own type mapping, and diffed with quark.Diff against IntrospectSchema; the indexes, foreign keys and checks the tags cannot name are carried over, so the plan never proposes dropping what your models are silent about. (PlanMigration, the library entry point, needs Go values and refuses when given none.)

migrate create can also write the migration body for you. Point it at the package that holds your model structs and name the dialect, and it renders domain DDL through the same type mapping the runtime migrator uses:

quark migrate create domain_schema --from-models ./models --dialect postgresql

Tables come out in dependency order, parents before children. Each carries the NOT NULL, UNIQUE, and size constraints your tags declare, plus FOREIGN KEY clauses and indexes for every rel:"belongs_to" / join: pair, and the Down drops them in reverse order.

--dialect accepts postgresql|mysql|mariadb|sqlite|mssql|oracle and defaults to the configured database.default.driver. The result is a regular migration file — review and edit it like any other.

The standalone binary cannot see your migrations

What happens. migrate up / migrate down and seed run exit non-zero with an explanation when the registry is empty, instead of reporting a misleading "No pending migrations".

Why. Migration files register themselves in an init(), which only runs when their package is compiled into the running binary. A go installed quark never imports your project's migrations/ package, so it genuinely has nothing to apply.

What to do. 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.Main(), which prints errors to stderr and exits non-zero on failure. Don't call a bare commands.Execute() from main: it returns the error instead of printing it, so every failure becomes a silent exit 0.

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

The --fields grammar reaches the rich half of the ORM, not just scalars:

  • A third segment adds a tag token — sku:string:unique, version:int64:version, title:string:not_null.
  • Generic specs produce container types and relations — bio:nullable<string>, tags:array<string>, attrs:json<Attrs>, author:belongs_to<User>. A belongs_to emits both the author_id column and the relation field pair.

The referenced types (Attrs, User above) stay yours to define.

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 compares one Go struct against the live table and reports the mapping in both directions:

  • A Go field whose column is missing in the database always exits non-zero. That is drift you have to fix.
  • A database column with no Go field exits non-zero only under --strict.

It loads your structs with go/packages. --models defaults to paths.models from .quark.yml, falling back to ./..., and the table is matched to a model by pluralized snake_case name unless you name it with --model User.

inspect table and model generate --from-table also exit non-zero when the table doesn't exist, rather than 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​

To reuse the CLI's own command tree (migrate, seed, tenant, …) with your project's migrations compiled in, you need a small runner in your project — and quark init already writes it for you (cmd/<app>/main.go, plus the migrations/seeders doc stubs it imports). The command tree lives in the CLI's module, so the runner needs it as a dependency of its own:

go get github.com/jcsvwinston/quark/cmd/quark@latest

Written out, the whole runner is:

// cmd/quark/main.go (in YOUR project)
package main

import (
_ "github.com/you/yourapp/migrations" // side-effect: registers migrations
_ "github.com/you/yourapp/seeders" // side-effect: registers seeders

"github.com/jcsvwinston/quark/cmd/quark/commands"
)

func main() { commands.Main() }

Both migrations and seeders register themselves from an init(), so blank-importing their packages is all the runner needs. quark migrate create and quark seed create write files that already do this — a scaffolded seeder is:

package seeders

import (
"context"

"github.com/jcsvwinston/quark"
"github.com/jcsvwinston/quark/seed"
)

func init() { seed.Register("demo_users", SeedDemoUsers) }

func SeedDemoUsers(ctx context.Context, client *quark.Client) error {
// ...
return nil
}

commands.Main executes the root command, prints any error to stderr and exits 1 — the same contract as the standalone binary, which is what CI gates need. commands.Execute remains available for mains that deliberately take over error handling; it returns the error and prints nothing, so never call it bare.

Two sibling runner libraries follow the same embed pattern for flows the standalone binary cannot know about (they need your registered models):

  • quarkmigrate — plan/verify/apply schema against the live database (exit 1 = drift, 2 = operational error).
  • quarktenant — install-rls-policies renders and applies the native row-level-security DDL for every registered model, and verify-rls-policies is the boot/CI gate that fails (exit 1) when any table is left unenforced — including a policy that carries the right name while its predicate isolates nothing — see Native row-level security.

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

Three behaviours are worth knowing before you wire this into a deploy job:

  • No dsn_template means the command fails, rather than guessing and migrating the default database under a tenant's name.
  • schema_per_tenant migrations can't be resolved from static config, so they exit with an explicit error. Run those from your own binary.
  • tenant provision under schema_per_tenant stops at the schema plus the registry row. It says so and exits 0 — that is the complete effect. It also refuses an id already registered in quark_tenants, so a retry never crashes on a duplicate CREATE SCHEMA.

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.