A type-safe, security-first ORM for Go — generics on the surface, six dialects underneath.
Docs · Quick Start · Examples · CLI · Changelog
Quark is in v0.8 — late-alpha. v0.8 is the Phase 4 cut: observability, stampede-protected caché, and resilience. OTel metrics (counter + duration/rows histograms) join the existing tracing spans; WithSpanRedaction keeps bind values off spans by default; WithSlowQueryThreshold emits structured slow-query WARNs through Client.logger; the cache backing is now wrapped with stampedeStore — singleflight + ±jitter + XFetch (ADR-0011) — so a hot key never produces a database stampede on miss; cache invalidation is per-row (<table>:<pk>) on top of the historical table tag, and the Redis tag-TTL bug is fixed (NX + GT pair takes the MAX); and WithDeadlockRetry re-runs the transaction closure on PG 40P01 / MySQL 1213 / MSSQL 1205 / Oracle ORA-00060 with exponential backoff + jitter. Every new feature is opt-in; the v0.7 surface keeps working unchanged. v0.7 added per-column timezones: time.Time columns can declare a timezone (quark:"tz=Europe/Madrid") or inherit a Client-wide default (quark.WithDefaultTZ), with a UTC-always wire contract — closing the last deferred type from Phase 1. v0.6 was the Phase 3 cut: schema-as-code migrations — neutral schema introspection across the four CI dialects + SQLite, pure-Go schema diff, Client.PlanMigration / ApplyPlan round-trip, transactional + resumable execution, quarkmigrate plan/verify/apply CLI workflow, orchestrated Backfill, per-Client model registry, a distributed migration lock, and Array[T]. v0.5 was a Phase 0 cleanup release that made the cross-engine integration matrix (PostgreSQL, MySQL, MariaDB, MSSQL via testcontainers; Oracle excluded pending an image issue) blocking in CI. v0.4 landed the composable query builder (typed expression AST, subqueries, CTEs, window functions, set operators, pessimistic locking, structured Join builder, nested-Preload dotted paths, IN(...) chunking, HavingAggregate); v0.3 landed the rich-type / dirty-tracking layer (Nullable[T], JSON[T], RegisterTypeMapper, optimistic locking, soft-delete scopes). It is not yet v1.0 production-ready — see docs/ANALISIS_MADUREZ.md for the honest gap analysis and the path to a real v1.0.
Breaking changes are documented in docs/MIGRATION_vX.Y.Z.md per version (none for v0.8, v0.7, v0.6 or v0.5; MIGRATION_v0.4.0.md covers the Join builder rename from v0.3.x). Release notes per version live under docs/RELEASE_NOTES_*.md.
After running production services on GORM, three patterns kept causing incidents: every db.Find(&result) forced an interface{} cast the compiler couldn't verify; column names in WHERE clauses were plain strings with no guard against typos or injection in dynamic queries; N+1 queries appeared silently whenever a Preload was forgotten, only surfacing in slow-query logs hours later; and multi-tenant isolation meant copy-pasting WHERE tenant_id = ? everywhere, relying on discipline instead of enforcement. Quark is the ORM I wished existed: generics end the casts, SQLGuard validates every identifier at the API boundary, eager loading is explicit, and multi-tenancy is first-class — not an afterthought.
go get github.com/jcsvwinston/quarkpackage main
import (
"context"
"log"
"github.com/jcsvwinston/quark"
_ "modernc.org/sqlite"
)
type User struct {
ID int64 `db:"id" pk:"true"`
Name string `db:"name" quark:"not_null"`
Email string `db:"email" quark:"unique"`
Age int `db:"age"`
}
func main() {
client, err := quark.New("sqlite", "file:app.db?cache=shared")
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
// Create the table
client.Migrate(ctx, &User{})
// Insert
u := User{Name: "Alice", Email: "alice@example.com", Age: 30}
quark.For[User](ctx, client).Create(&u)
// u.ID is now set
// Query
users, _ := quark.For[User](ctx, client).
Where("age", ">=", 18).
OrderBy("name", "ASC").
Limit(20).
List()
// Update (partial — only non-zero fields) → returns (rowsAffected int64, err error)
u.Name = "Alice Smith"
rows, err := quark.For[User](ctx, client).Update(&u)
_, _ = rows, err
// Delete
_, _ = quark.For[User](ctx, client).HardDelete(&u)
_ = users
}Switch to PostgreSQL — change one line, zero query code changes:
client, _ = quark.New("postgres", "postgres://user:pass@localhost/db")See the per-dialect runnable examples under examples/ (one folder per supported engine).
Recording coming soon. To preview Quark locally right now:
git clone https://github.com/jcsvwinston/quark go run ./examples/sqlite
Most Go ORMs make you choose between safety and ergonomics. Quark doesn't.
| Quark | GORM | sqlx | ent | |
|---|---|---|---|---|
Native Generics (no interface{}) |
✅ | partial¹ | ❌ | ✅ |
| SQL Injection Guard | identifier + value | value only² | manual | value only² |
| 6 Dialects, zero config switch | ✅ | ✅ | ❌ | partial |
| Native Multi-Tenant (DB/Schema/RLS) | ✅ | manual/plugin | manual | manual/interceptor |
| Immutable Query Builder | ✅ | mutable³ | N/A | ✅ |
| Integrated L2 Cache | ✅ | plugin | ❌ | ❌ |
stdlib *sql.DB — no magic pool |
✅ | ✅ | ✅ | ❌ |
| OpenTelemetry built-in | ✅ | plugin | ❌ | plugin |
| Batch Ops (Delete/Upsert/Update) | ✅ | partial⁴ | ❌ | partial |
¹ GORM v2 core API uses
interface{}; generic wrappers exist but are not part of the primary API.
² GORM and ent use parameterized queries that protect values against injection. Quark additionally validates identifiers (column/table names) at the API layer. See docs/comparison.md for a detailed breakdown with code examples.
³ GORM queries can mutate shared state when chained;Session(&gorm.Session{NewDB: true})mitigates this but is opt-in.
⁴ GORM supportsCreateInBatches; batch DELETE and batch UPDATE require custom loops.
For a cell-by-cell justification with code examples, see docs/comparison.md.
- 100% Type-Safe — Go Generics end
interface{}casts and silent runtime errors forever - SQLGuard — Every identifier (column, table, operator) is validated before touching the wire
- Immutable Builder — Clone-on-write query builder, safe for concurrent goroutines
- 6 Dialects — PostgreSQL · MySQL · MariaDB · SQLite · MSSQL · Oracle, all with idiomatic SQL generation
- Native Multi-Tenancy — Database-per-tenant, schema-per-tenant, and Row-Level Security out of the box
- L2 Cache — Pluggable cache backend (in-memory, Redis) wired directly into the query lifecycle
- OpenTelemetry — Distributed tracing and metrics without changing your query code
- Batch Operations — Chunked
DeleteBatch, dialect-optimalUpsertBatch, atomicUpdateBatch - Eager Loading — Single-query
Preload()eliminates N+1 queries - Auto-Migrations & Sync —
Migrate()creates tables;Sync()evolves them, including column renames - Hooks & Middleware — Full lifecycle hooks (
BeforeCreate,AfterDelete…) and stackable middleware - Versioned Migrations — Code-first migration files with Up/Down and dry-run support
- Composite PKs — First-class support for multi-column primary keys across all dialects
- Streaming —
Iter(),Cursor(), andPaginate()prevent OOM on large datasets - CLI —
quark model generate,quark migrate up,quark inspect schemaand more
Quark refuses to build a query with an unknown column, operator, or identifier:
// Compile-time + runtime guard: "drop_table" is not a valid operator
quark.For[User](ctx, client).Where("name", "drop_table", "x").List()
// → ErrInvalidQuery: operator "drop_table" not allowed
// Raw subqueries require explicit opt-in
quark.For[User](ctx, client).WhereSubquery("id", "IN", rawSQL).List()
// → ErrInvalidQuery: WhereSubquery requires AllowRawQueries to be enabledEnable raw queries only where you deliberately need them:
lims := quark.DefaultLimits()
lims.AllowRawQueries = true
client, _ = quark.New("postgres", "postgres://user:pass@localhost/db", quark.WithLimits(lims))// Create
err := quark.For[User](ctx, client).Create(&user)
// Find by PK
user, err := quark.For[User](ctx, client).Find(1)
// Update (partial — zero-value fields are skipped)
user.Name = "Bob"
rows, err := quark.For[User](ctx, client).Update(&user)
// UpdateMap (force any value, including zero)
rows, err := quark.For[User](ctx, client).
Where("id", "=", user.ID).
UpdateMap(map[string]any{"active": false, "score": 0})
// Upsert (INSERT … ON CONFLICT) — all 6 dialects
err = quark.For[User](ctx, client).Upsert(&user, []string{"email"}, []string{"name", "age"})
// Soft delete (sets deleted_at) or hard delete
rows, err = quark.For[User](ctx, client).Delete(&user)
rows, err = quark.For[User](ctx, client).HardDelete(&user)
rows, err = quark.For[User](ctx, client).Where("active", "=", false).DeleteBy()// DeleteBatch — chunked IN clauses, respects dialect limits
affected, err := quark.For[User](ctx, client).DeleteBatch([]int64{1, 2, 3, 100})
// UpsertBatch — dialect-optimal (multi-row ON CONFLICT / bulk MERGE / individual MERGE)
err = quark.For[User](ctx, client).UpsertBatch(users, []string{"email"}, []string{"name", "age"})
// UpdateBatch — N partial updates in a single transaction
affected, err = quark.For[User](ctx, client).UpdateBatch(users)users, err := quark.For[User](ctx, client).
Select("id", "name", "email").
Where("active", "=", true).
Where("age", ">", 18).
WhereIn("role", []any{"admin", "editor"}).
WhereBetween("created_at", start, end).
WhereNot("banned", "=", true).
Or(func(q *quark.Query[User]) *quark.Query[User] {
return q.Where("tier", "=", "vip")
}).
OrderBy("created_at", "DESC").
Limit(50).Offset(100).
List()
count, err := quark.For[User](ctx, client).Where("active", "=", true).Count()
total, err := quark.For[Order](ctx, client).Sum("amount")err := client.Tx(ctx, func(tx *quark.Tx) error {
if err := quark.ForTx[User](ctx, tx).Create(&u); err != nil {
return err // triggers ROLLBACK
}
tx.Savepoint("checkpoint")
// nested savepoint — partial rollback possible
return nil // triggers COMMIT
})cfg := quark.DefaultTenantConfig()
cfg.Strategy = quark.DatabasePerTenant // or SchemaPerTenant, RowLevelSecurity
cfg.BaseClient = adminClient
router := quark.NewTenantRouter(cfg,
func(ctx context.Context) string {
return ctx.Value("tenant_id").(string)
}, nil)
// Queries are automatically routed & isolated — no code changes
users, _ := quark.For[User](tenantCtx, router).List()import "github.com/jcsvwinston/quark/cache/memory"
store := memory.New()
client, _ := quark.New("postgres", "postgres://user:pass@localhost/db",
quark.WithCacheStore(store),
)
// Cache for 5 minutes, tagged "users"
users, _ := quark.For[User](ctx, client).
Cache(5*time.Minute, "users").
List()
// Invalidate the tag (e.g. after a write)
store.InvalidateTags(ctx, "users")Redis backend: github.com/jcsvwinston/quark/cache/redis
import quarkotel "github.com/jcsvwinston/quark/otel"
client, _ := quark.New("postgres", "postgres://user:pass@localhost/db",
quark.WithMiddleware(quarkotel.New()),
)
// Every query now emits spans and metrics to your configured OTEL exporter// Create table if not exists
client.Migrate(ctx, &User{}, &Order{})
// Evolve schema: add columns, rename with quark:"rename:old_col", drop with safe=false
client.Sync(ctx, &User{})// migrations/20240101_create_users.go
migrate.Register(&migrate.Migration{
ID: "20240101_create_users",
Up: func(ctx context.Context, client *quark.Client) error {
return client.Exec(ctx, `CREATE TABLE users (...)`)
},
Down: func(ctx context.Context, client *quark.Client) error {
return client.Exec(ctx, `DROP TABLE users`)
},
})quark migrate up
quark migrate down --steps 1
quark migrate statusInstall:
go install github.com/jcsvwinston/quark/cmd/quark@latest| Command | Description |
|---|---|
quark init |
Scaffold a new project with .quark.yml config |
quark model generate --from-table users |
Generate Go structs from live database tables |
quark migrate create add_index |
Create a new versioned migration file |
quark migrate up |
Apply pending migrations |
quark migrate down --steps 1 |
Revert the last migration |
quark migrate status |
Show applied / pending migrations |
quark inspect schema |
Print full database schema |
quark inspect table users |
Inspect a specific table |
quark validate --table users |
Validate column ↔ struct mapping |
quark seed run |
Execute registered seeders |
quark tenant provision acme |
Provision a new tenant |
quark tenant migrate-all |
Run migrations across all tenants |
github.com/jcsvwinston/quark
├── *.go Core ORM (client, query builder, CRUD, dialect)
├── cache/
│ ├── memory/ In-memory L2 cache
│ └── redis/ Redis L2 cache
├── migrate/ Versioned migration engine
├── otel/ OpenTelemetry middleware
├── internal/ Private implementation (guard, schema, introspection)
├── cmd/
│ └── quark/ CLI tool
├── examples/ Runnable examples per dialect
└── docs/ Architecture, API reference, multi-tenancy guide
client, err := quark.New("postgres", "postgres://user:pass@localhost/db",
quark.WithLimits(quark.Limits{
MaxResults: 10_000, // hard cap on List() results
MaxWhereConditions: 20, // prevent runaway WHERE chains
MaxJoins: 5,
QueryTimeout: 30 * time.Second,
AllowRawQueries: false, // explicit opt-in for raw SQL
SafeMigrations: true, // block DROP COLUMN by default
}),
quark.WithCacheStore(store), // L2 cache backend
quark.WithMiddleware(myMiddleware), // stackable middleware
quark.WithQueryObserver(myObserver), // query logging / metrics
)Pull requests are welcome. For major changes, please open an issue first.
git clone https://github.com/jcsvwinston/quark
cd quark
go test ./... # all unit + integration tests (SQLite runs offline)External engine tests require env vars:
QUARK_TEST_POSTGRES_DSN=postgres://...
QUARK_TEST_MYSQL_DSN=user:pass@tcp(...)/db?parseTime=true
QUARK_TEST_MSSQL_DSN=sqlserver://...
QUARK_TEST_ORACLE_DSN=oracle://...
Apache 2.0 — see LICENSE