Help us shape the future of Go! Take the 2026 Go Developer Survey

quark

package module
v1.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

README

Quark ORM

Quark

A type-safe, security-first ORM for Go — generics on the surface, six dialects underneath.

Go Reference CI Go Version Release

Docs · Quick Start · CLI · Changelog


📌 Status

Quark is v1.17.0 on the stable v1.x line (v1.2.0 closed the scaling deferrals; v1.1.0 was the hardening minor; v1.0.0 the first stable release under SemVer). Phases 0–6 are complete: the core query builder, CRUD, schema-as-code migrations across all six dialects (Oracle now in blocking CI), multi-tenancy, caché, hooks/events/audit log, observability, opt-in code generation, read replicas with failover, and pluggable sharding. v1.x keeps API compatibility; breaking changes go to v2.x with a docs/MIGRATION_v2.0.0.md. Symbols on their way out are listed in docs/deprecations/, each with its replacement, the release it is removed in and the date that window opens; so is behaviour that changes at the major, such as UpdateBatch checking versions by default in 2.0. v1.0 was gated on the qualitative checklist in docs/V1_GATE.md (cross-engine coverage, structural gaps closed or consciously waived), not on a performance target — ADR-0017 retired the ≥3× codegen performance gate, so code generation is a type-safety feature, not a speedup. Known limitations consciously deferred at each minor are listed in the release notes (docs/RELEASE_NOTES_v1.17.0.md for the current line).

The v1.10.0 minor makes the missing-driver error name the module to import. The previous v1.9.0 minor takes the database drivers out of the library: each engine ships as its own module, so a program links only the one it uses (24 MB → 6 MB, 171 modules → 129). Add one blank import — drivers/postgres, drivers/mysql, drivers/sqlite, drivers/mssql or drivers/oracle — and nothing else changes. The previous v1.8.0 minor closes the core and CLI findings of an end-to-end audit, and most of them share a shape: a query or a command that was accepted and then quietly did something other than what it said. With a join in the query, filtering on a column whose name exists in both tables — the primary key included — could not be expressed at all, because the identifier guard rejected the qualified orders.id while the engine rejected the bare id as ambiguous; Where, OrderBy, GroupBy, Select and Col now accept the table.column form when the query has joins. WithStrictColumns() checks column names against the registered model: the identifier guard is lexical, so Where("agee", ">", 1) passed it as well-formed, and on SQLite an unknown identifier degrades to a string literal — the query returned every row of the table and reported nothing. WithoutAssociations() asks Update and Create to write this entity's row only, so a change another writer made to a preloaded child between your read and your update is not overwritten from your in-memory snapshot. NewWithDB mounts Quark on a *sql.DB the host application already owns, instead of a second DSN and a second pool. On the CLI, quark init outside a Go module no longer scaffolds a project whose first command fails, generated seeders register themselves, and the values of --table and --model reach introspection SQL through SQLGuard.

The v1.7.x line is about telling one database failure from another. IsUniqueViolation and IsDeadlock answer the question a write handler actually has — was this a duplicate I can explain to the user, or something I should not have swallowed — matching on the code the driver reports rather than on message text, so they are unaffected by the server's language or by wording changes between driver releases. The classification behind them was also blind where the installation guide points: under lib/pq none of its three internal uses recognised anything, so WithDeadlockRetry never retried and a downed read replica was never detected as one, neither of them reporting an error. The v1.7.1 patch closes a cache leak under RowLevelSecurityNative: the tenant was missing from the L2 cache key, so two tenants issuing the same query shared an entry and a .Cache() fill for one was served to the other — the engine's row-level security protected the database, and nothing protected the cache.

The v1.3.3 patch closes the native row-level-security correctness backlog: Create and Update now scope their implicit transaction to the operation like every other entry point — a write from a long-lived context (batch job, CLI, worker) no longer keeps a pooled connection idle in transaction until that context ends, later DDL on the touched table no longer blocks behind its locks, and a row you just wrote is visible to reads on the same context; the executor returns its pooled connection on every path even when the driver panics; and driver or transaction errors on the QueryRow path reach Scan intact instead of being masked as ErrNoRows. The test matrix gains a -race lane.

The v1.3.2 patch hardens the native row-level-security executor and the engine-gated test surface: connection acquisition under RowLevelSecurityNative now honours the caller's context (a saturated pool aborts with the request's deadline instead of hanging), a failed deferred commit is logged at error level and counted (Client.DeferredCommitFailures), the write semantics introduced in v1.3.1 are documented where readers see them, and every engine-dependent test now runs in a CI lane — Redis-backed cache locks included — or is explicitly marked manual.

The v1.3.1 patch closes the fifth audit round: mixed set-operator chains (Union + Intersect in one query) now return ErrUnsupportedFeature instead of emitting SQL whose meaning silently differs per engine; the native row-level-security executor no longer loses INSERT … RETURNING writes (the implicit transaction's lifecycle was tied to the request context, and its deferred commit raced the automatic rollback) and its integration test runs in the Postgres CI lane along with every other engine-gated test that previously ran nowhere (deadlock retry, LISTEN/NOTIFY, RLS install); the published roadmap no longer hardcodes a version; and the Oracle set-operator notes state Quark's own gating instead of a false claim about the engine.

The v1.3.0 minor adds the INTERSECT ALL / EXCEPT ALL multiset set-operators (IntersectAll / ExceptAll) and corrects their per-dialect support: PostgreSQL and MariaDB run them, while SQL Server, Oracle, SQLite and MySQL return ErrUnsupportedFeature instead of emitting SQL the engine would reject.

v1.2 (scaling follow-ups): closes two v1.1 deferrals — cross-instance cache-stampede coordination (opt-in WithCacheCrossInstance with a memory or Redis CacheLocker, ADR-0020) and richer sharding ergonomics: shard keys derived from the entity via ShardKeyer/WithShardKeyOf (ADR-0021) and explicit scatter-gather cross-shard reads (ScatterGather/ScatterCount/ScatterMerge, ADR-0022). Also pins toolchain go1.26.5 + pgx v5.9.2 (dependency security updates), fixes an over-eager Update zero-value warning, and memoizes the scan plan on the read path. No breaking changes.

The v1.2.2 patch executes the v1.2.1 audit backlog across the whole surface: the four P0s (SQL injection in quark tenant provision, Count()/Paginate ignoring UNION/INTERSECT/EXCEPT, a MySQL/MariaDB panic on Upsert with empty conflictCols, and Offset-without-Limit silently dropped or rendered invalid), a CLI that refuses to lie (migrate up/seed run with an empty registry exit non-zero with the embed recipe, validate actually loads your Go structs, tenant migrate targets the tenant's database, phantom flags removed), UpsertBatch chunking to per-dialect bind-parameter ceilings (~65k PG/MySQL/MariaDB, ~32k SQLite, ~2100 MSSQL), SQL Server Upsert back-filling the generated PK via OUTPUT INSERTED (Oracle's MERGE has no RETURNING — documented), and INTERSECT/EXCEPT enabled on MariaDB 10.3+.

The v1.2.1 patch closes the 2026-07 audit findings: the CLI works out of the box (quark init wrote a driver name sql.Open did not recognise), failing commands exit non-zero, quark sync no longer advertises flags it ignored, inspect --format json|yaml and quark version exist, dialect Quote() is self-escaping, and the raw-query guard no longer rejects -- inside string literals. CLI/docs/guard only — no query-builder or CRUD changes.

v1.1 (hardening): the post-v1.0 bug-bash (phases F0–F14, a systematic cross-engine pass over the whole surface) plus the correctness fixes it surfaced — CreateBatch now chunks multi-row INSERTs to a safe universal 2,000-bind-parameter constant, versioned migrations work on SQL Server, a MariaDB schema-diff false positive is gone, dialect-aware savepoints, multi-tenant SchemaPerTenant write routing, and three eager-loading fixes, plus -- rejected in raw queries (BB-5…BB-13). Adds automatic MariaDB detection and an inbound PostgreSQL LISTEN/NOTIFY listener (ADR-0019). No breaking changes.

The v1.1.1–v1.1.5 patches carry cross-engine correctness fixes — CreateBatch primary-key back-fill on Oracle/MySQL/SQL Server, recursive CTEs and set-op pagination on Oracle/SQL Server, BeforeCreate/BeforeUpdate hooks on the batch and upsert paths, and ErrInvalidQuery wrapping for rejected operators and raw queries — detailed in the CHANGELOG. All non-breaking.

v0.13 (Phase 6 HA cut): opt-in read replicas (WithReplicas) with read/write split, read-your-writes (Sticky), and automatic failover to the primary on transient replica errors (with a configurable replica cooldown, ADR-0015); a copy-on-write query-builder clone that drops a "fat base" derive to 1 alloc/op; and a runnable stress/load harness in benchmarks/stress. No breaking changes.

v0.12 (Phase 6 sliver): opt-in compile-time column type-safety on top of the code generator — quark gen emits per-model <Model>Columns accessors and the query builder gains WhereP, so typos and wrong-typed values fail at build time. Pure compile-time sugar (ADR-0014): the string Where(...) API stays valid and interchangeable. Performance fix: audit row diff is now built only when an audit sink is configured (~9% allocation drop on InsertOne). No breaking changes.

v0.11 (Phase 6 cut): opt-in code generation — quark gen parses your model package with go/packages/go/types and emits a quark_gen.go per package that registers typed scanners (read path) and a typed INSERT binder (single-integer-PK models) into the runtime registry. The reflection path stays the permanent default (ADR-0002). Honest profiling finding in benchmarks/PROFILING.md: codegen ~2–5% scan, ~1% INSERT — Quark's per-op CPU is dominated by database/sql and the engine, not reflection. Generate for correctness and forward compatibility, not for speed. Reproducible benchmark harness in benchmarks/ (database/sql baseline + GORM + Quark). No breaking changes.

v0.10 (correctness): JSON[T] / Array[T] round-trip on SQL Server (they were corrupted by a []byte→VARBINARY→NVARCHAR conversion); savepoint rollback discards After*/OnCommit/OnRollback hooks queued in that scope so undone work no longer fires its side-effects on the outer commit; a real cross-engine deadlock-retry integration test (PG/MySQL/MariaDB) backs WithDeadlockRetry; raw SQL under RowLevelSecurityNative emits a quark.tenant.raw_under_native_rls warning. No breaking changes.

v0.9 (Phase 5 cut — multi-tenancy + transactional hooks + events + audit): PostgreSQL RowLevelSecurityNative delegates tenant isolation to the database via set_config('app.tenant_id', …) + CREATE POLICY (the quarktenant install-rls-policies CLI generates the DDL); After* hooks now fire post-commit under Client.Tx and there are new BeforeFind/AfterFind hooks; Tx.OnCommit/Tx.OnRollback + quark.TxFromContext expose commit/rollback side-effects; a real EventBus (Client.UseEventBus) publishes created/updated/deleted events; Client.EnableAuditLog records an immutable change trail in quark_audit, written atomically with each write. Two breaking-minor changes — see docs/MIGRATION_v0.9.0.md.

v0.8 (Phase 4 cut — observability + caché de producción): 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; the cache backing is wrapped with stampedeStore — singleflight + ±jitter + XFetch (ADR-0011); cache invalidation is per-row (<table>:<pk>) on top of the table tag, with the Redis tag-TTL fix (NX + GT MAX); WithDeadlockRetry re-runs the closure on PG 40P01 / MySQL 1213 / MSSQL 1205 / Oracle ORA-00060 with exponential backoff + jitter.

v0.7: per-column timezones (quark:"tz=Europe/Madrid" or Client-wide WithDefaultTZ), UTC-always wire contract.

v0.6 (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, orchestrated Backfill, per-Client model registry, distributed migration lock, Array[T].

v0.5 (Phase 0 cleanup): the cross-engine integration matrix (PostgreSQL, MySQL, MariaDB, MSSQL via testcontainers; Oracle excluded pending an image issue) is blocking in CI.

v0.4 (Phase 2 — 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 (Phase 1 — rich types + dirty tracking): Nullable[T], JSON[T], RegisterTypeMapper, optimistic locking, soft-delete scopes.

Breaking changes are documented in docs/MIGRATION_vX.Y.Z.md per version (MIGRATION_v0.9.0.md covers the two breaking-minor changes in v0.9; none for v1.0, v0.13, v0.12, v0.11, v0.10, 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.


🏗️ Why I built this

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.


🚀 Quick Start

Install the library and the driver module for your engine. The driver module registers the database/sql driver and teaches Quark to recognise that driver's errors — importing the bare driver instead opens the database just as well, but IsUniqueViolation, deadlock retry and replica failover then answer false for everything (Quark logs a WARN at New when that happens).

go get github.com/jcsvwinston/quark
go get github.com/jcsvwinston/quark/drivers/sqlite
Engine Driver module (go get + blank import) Name for quark.New
SQLite github.com/jcsvwinston/quark/drivers/sqlite sqlite
PostgreSQL github.com/jcsvwinston/quark/drivers/postgres pgx
MySQL / MariaDB github.com/jcsvwinston/quark/drivers/mysql mysql
SQL Server github.com/jcsvwinston/quark/drivers/mssql sqlserver
Oracle github.com/jcsvwinston/quark/drivers/oracle oracle

What go.mod lists is not what a binary links. The root module still requires the five engine drivers and the test-container modules: cmd/quark is a tool that talks to whatever database it is pointed at, so the CLI links every engine, and the engine suites run from this module. A program that imports quark carries 128 modules in its build list and links 11 of them; the 117 it never links are not in the binary, and go build does not even download them. They are in the graph that go list -m all, an SBOM or a dependency dashboard reads, and go mod download all fetches about 910 MiB where the build needs 133 MiB. Every figure, with the command behind it, is in docs/dependency-surface.md; moving the CLI to a module of its own is proposed there — with what it would cost — in ADR-0024, and not taken here.

package main

import (
    "context"
    "log"

    "github.com/jcsvwinston/quark"
    _ "github.com/jcsvwinston/quark/drivers/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 the driver import and the driver name, zero query code changes:

import _ "github.com/jcsvwinston/quark/drivers/postgres"

client, _ = quark.New("pgx", "postgres://user:pass@localhost/db")

The driver module is the only per-engine difference: the import above changes, the code that follows it does not.


🎬 Demo

Recording coming soon. To preview Quark locally right now, follow the Quick Start above in a new module: it is eight lines and a SQLite file.


Why Quark?

Most Go ORMs make you choose between safety and ergonomics. Quark doesn't.

Compared against GORM v1.30 (generics API), sqlx v1.4 and ent v0.14, as of 2026-09.

Quark GORM sqlx ent
Native Generics (no interface{}) ✅ ✅ since v1.30¹ ❌ ✅
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 1.30 (2025) added an official generics API — gorm.G[User](db).Where(...).Find(ctx) — alongside the classic interface{}-based one, which the bulk of its documentation and ecosystem still uses. Both are supported; the generic one is opt-in per call.
² 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 supports CreateInBatches; batch DELETE and batch UPDATE require custom loops.
⁵ ent runs on database/sql: ent.Driver(entsql.OpenDB(dialect, db)) wraps an existing *sql.DB, and ent.Open calls sql.Open underneath. It does not manage its own pool.

For a cell-by-cell justification with code examples, see docs/comparison.md.


✨ Features

  • 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-optimal UpsertBatch, atomic UpdateBatch
  • Eager Loading — Single-query Preload() eliminates N+1 queries
  • Auto-Migrations & Sync — Migrate() creates tables; Sync() evolves them, including column renames
  • Test kit — quarktest gives each test its own database, migrates it from your models, and rolls back what the test wrote
  • RLS preflight — verify-rls-policies reads your isolation policies' predicates, not just their names, so a policy that isolates nothing fails the check instead of passing it
  • 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(), and Paginate() prevent OOM on large datasets
  • CLI — quark model generate, quark migrate up, quark inspect schema and more

🔒 SQLGuard — Security by Default

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 enabled

Enable 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))

📖 Core Operations

CRUD
// 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()
Batch Operations
// DeleteBatch — chunked IN clauses, respects dialect limits
affected, err := quark.For[User](ctx, client).DeleteBatch([]any{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
err = quark.For[User](ctx, client).UpdateBatch(users)
Query Builder
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")
Transactions & Savepoints
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
})

🏢 Multi-Tenancy

cfg := quark.DefaultTenantConfig()
cfg.Strategy  = quark.DatabasePerTenant  // or SchemaPerTenant, RowLevelSecurityClient
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()

📦 Caching (L2)

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


🔭 OpenTelemetry

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

🗄 Migrations

Auto-Migrate & Sync
// Create table if not exists
client.Migrate(ctx, &User{}, &Order{})

// Evolve schema: add columns, rename with quark:"rename:old_col",
// drop only when Limits.SafeMigrations is false
client.Sync(ctx, quark.SyncOptions{}, &User{})
Versioned Migrations
// 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 status

🛠️ CLI

Install:

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

The CLI is its own Go module, so @latest resolves its own tags (cmd/quark/vX.Y.Z) and its version numbers are not the library's — quark version prints both, the CLI's and the Quark release inside it. A selector naming a library version (@v1.13.0) does not resolve the CLI: use @latest, or a cmd/quark version.

Releases cut from the first signed tag onward also publish prebuilt archives (Linux, macOS, Windows × amd64, arm64), each with an SPDX SBOM, a checksum file, a cosign signature over that file and a build provenance attestation. Grab one from the releases page and verify it before you run it — the two commands are on Verifying a release.

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

📐 Project Structure

github.com/jcsvwinston/quark
├── *.go                  Core ORM (client, query builder, CRUD, dialects, migrations, cache, tenancy, replicas, sharding)
├── quarkdriver/          Contract a driver module implements (error classifier, LISTEN/NOTIFY listener); leaf package
│   └── drivertest/       Conformance kit for driver modules
├── drivers/              One module per engine (own go.mod): postgres, mysql, sqlite, mssql, oracle
├── cache/
│   ├── memory/           In-memory L2 cache
│   └── redis/            Redis L2 cache
├── migrate/              Versioned migration registry and migrator
├── quarkmigrate/         Programmatic migration helpers used by the CLI and by applications
├── quarktenant/          Tenant provisioning and native RLS policy install/verify (PostgreSQL)
├── quarktest/            Test kit: throwaway SQLite clients for unit tests
├── seed/                 Seeder registry used by generated seeders
├── otel/                 OpenTelemetry middleware (traces and metrics)
├── internal/             Private implementation (guard, schema, introspection, driver classifiers)
├── cmd/
│   └── quark/            CLI tool
├── acceptance/           Cross-engine acceptance harness (own module), gated on a generated API manifest
├── benchmarks/           Benchmark harness (own module) against database/sql, GORM and ent
├── bugbash/              Bug-bash harness (own module)
├── website/              Docusaurus source of the published documentation
└── docs/                 Internal notes: ADRs, playbooks, release notes, roadmap

⚙️ Configuration Reference

client, err := quark.New("postgres", "postgres://user:pass@localhost/db",
    quark.WithLimits(quark.Limits{
        MaxQueryLength:     10 * 1024,       // reject generated SQL above this byte length
        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
)

More knobs — deadlock retry, slow-query logging, default timezone, cache jitter/XFetch/cross-instance stampede control, read replicas and their selection strategy/cooldown, pool idle-time — are covered in the Configuration Reference.


🤝 Contributing

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://...

📄 License

Apache 2.0 — see LICENSE

Documentation

Overview

Package quark provides a modern, type-safe ORM for Go. It supports multiple SQL dialects and is designed to be framework-agnostic.

Index

Constants

View Source
const (
	// LockNone means no lock clause is emitted (the default).
	LockNone = quarkdriver.LockNone
	// LockForUpdate locks the rows for update; other transactions cannot
	// read-with-lock or write the matching rows until the current
	// transaction commits or rolls back. Most engines support it.
	LockForUpdate = quarkdriver.LockForUpdate
	// LockForShare takes a shared read lock — other transactions can also
	// read-with-lock but not write. Supported on PG / MySQL 8+ / MariaDB;
	// not on SQLite. MSSQL approximates with HOLDLOCK.
	LockForShare = quarkdriver.LockForShare
)
View Source
const GenContractVersion = 3

GenContractVersion is the version of the code-generation contract: the shape of the generated files and the signatures of the RegisterTyped* functions below. The generator stamps a matching `//quark:gen vN` header into every file it emits.

Bump this on any breaking change to the generated-file shape or the registrar signatures. Generated code that registers a different version is ignored at lookup time (the operation falls back to reflection), so a stale binary never calls into incompatible generated code.

v2 (F6-2): generated files emit a real typed scanner (read path) instead of the F6-1 stub. v3 (F6-3a): integer-PK models also emit a real INSERT binder. Older files are ignored and fall back to reflection.

View Source
const RowLevelSecurity = RowLevelSecurityClient

RowLevelSecurity is the legacy name for RowLevelSecurityClient. The constant value is identical, so existing code and serialized configs continue to work without changes.

Deprecated: use RowLevelSecurityClient. The name change clarifies that this strategy is client-side WHERE injection, not engine-enforced RLS — see ADR-0012 and RowLevelSecurityNative for the engine-enforced PostgreSQL variant. Scheduled for removal in v2.0.0, no earlier than 2026-12-08. See docs/deprecations/DEP-2026-001-rowlevelsecurity-alias.md for the migration path and the reasoning behind both dates.

Variables

View Source
var (
	// ErrNotFound indicates that no record was found for the given criteria.
	ErrNotFound = errors.New("record not found")

	// ErrInvalidModel indicates that the provided model is invalid or not registered.
	ErrInvalidModel = errors.New("invalid model")

	// ErrInvalidQuery indicates that the query is malformed or invalid. Re-exported
	// from internal/guard, where ValidateOperator / ValidateRawQuery wrap it with
	// %w, so errors.Is(err, ErrInvalidQuery) holds for a rejected operator, a raw
	// query missing placeholders, or a suspicious raw pattern — as well as for the
	// query-construction errors raised with it in this package.
	ErrInvalidQuery = guard.ErrInvalidQuery

	// ErrInvalidIdentifier indicates that a table or column identifier is invalid.
	// Re-exported from internal/guard, where ValidateIdentifier wraps it with %w,
	// so errors.Is(err, ErrInvalidIdentifier) works for a rejected identifier from
	// any call site (Where/OrderBy/GroupBy columns, table names, migration ops,
	// CTEs, event channels…) — consistent with ErrInvalidJoin / ErrInvalidJSONPath.
	ErrInvalidIdentifier = guard.ErrInvalidIdentifier

	// ErrInvalidJSONPath indicates that a JSON path passed to WhereJSON is malformed
	// or contains characters that could enable SQL injection. Quark accepts dotted
	// paths shaped like "user.name"; see guard.ValidateJSONPath for the grammar.
	// Array indexes and engine-specific syntax are out of scope for WhereJSON;
	// use RawQuery for those.
	ErrInvalidJSONPath = errors.New("invalid JSON path")

	// ErrInvalidJoin indicates that a JOIN ... ON clause passed to Join,
	// LeftJoin, or RightJoin does not match the minimal identifier-only
	// grammar Quark accepts while a structured Join().On() builder is pending
	// (Phase 2 AST). See guard.ValidateJoinOn for the grammar; use a
	// structured Join (when available) or RawQuery for shapes outside it.
	ErrInvalidJoin = errors.New("invalid JOIN ON clause")

	// ErrStaleEntity indicates that an optimistic-locking update failed
	// because the row's version column had been bumped by another writer
	// since the entity was loaded. The caller should reload the row, replay
	// the change against the fresh state, and retry — or surface the
	// conflict to the user. Returned by Update / UpdateFields / Tracked.Save
	// when the model carries a quark:"version" field, and by UpdateBatch under
	// [Query.CheckVersions], joined once per stale row with that row's key
	// (QK-45). UpdateBatch without CheckVersions does not return it; Quark 2.0
	// makes the check the default (DEP-2026-003).
	ErrStaleEntity = errors.New("stale entity (optimistic-locking conflict)")

	// ErrUnsupportedFeature indicates that a feature is not supported by the
	// active database dialect. Returned by builder methods (e.g. ForUpdate
	// on SQLite) so callers can branch by dialect or fall back to a different
	// strategy. The error message includes the dialect name and the feature
	// being requested. It holds the value of
	// quarkdriver.ErrUnsupportedFeature, which a dialect returns (ADR-0026),
	// so errors.Is matches under either name.
	ErrUnsupportedFeature = quarkdriver.ErrUnsupportedFeature

	// ErrInvalidTimezone indicates that a model field carries a
	// quark:"tz=..." tag whose value is not a valid IANA timezone name
	// (i.e. time.LoadLocation rejected it). It is surfaced fail-fast by
	// Client.RegisterModel and Client.Migrate so an invalid timezone
	// breaks the application at startup, not on the first query that
	// touches the column. The wrapped error names the field, the column
	// and the offending timezone string.
	ErrInvalidTimezone = errors.New("invalid column timezone")

	// ErrInvalidTag indicates a model struct carries tag tokens quark does
	// not read — a misspelled quark:"..." token, a db option other than
	// size/precision/scale, a pk value other than "true", or a foreign tag
	// key like column:/notnull:. Surfaced by Client.RegisterModel and
	// Client.Migrate so a typo breaks at startup naming every offending
	// field, instead of producing DDL that silently omits NOT NULL, UNIQUE
	// or whole columns (DX-8).
	ErrInvalidTag = errors.New("invalid struct tag")

	// ErrDialectNotSupported indicates that the database dialect is not supported.
	ErrDialectNotSupported = errors.New("dialect not supported")

	// ErrConnection indicates a database connection error.
	ErrConnection = errors.New("database connection error")

	// ErrTimeout indicates that a query timed out.
	ErrTimeout = errors.New("query timeout")

	// ErrConstraintViolation indicates a database constraint violation.
	ErrConstraintViolation = errors.New("constraint violation")

	// ErrEventEmitFailed indicates that an EventBus.Publish call failed
	// AFTER the originating write was already persisted. The data is
	// committed; only the event emission failed. Callers that see this
	// wrapped on a non-transactional CRUD return must NOT retry the
	// write (that would double-write) — they should retry the emit or
	// rely on the subscriber being idempotent (at-least-once delivery,
	// no outbox; see ADR-0013). Under an explicit transaction the emit
	// runs post-commit via Tx.OnCommit and the failure is logged with
	// the OTel-style event `quark.event.emit_failure` rather than
	// propagated (the commit already returned success).
	ErrEventEmitFailed = errors.New("event emit failed after commit")

	// ErrListenerClosed indicates an operation was attempted on an
	// EventListener (PostgreSQL LISTEN/NOTIFY) after Close was called.
	// The dedicated connection has been returned to the pool; create a
	// fresh listener via ListenerFactory.CreateListener. See ADR-0019.
	ErrListenerClosed = quarkdriver.ErrListenerClosed

	// ErrNoSubscription indicates Receive was called on an EventListener
	// that has no active channel subscription — Listen must be called at
	// least once before Receive. See ADR-0019.
	ErrNoSubscription = quarkdriver.ErrNoSubscription
)

Common errors returned by quark operations.

View Source
var ErrGeneratedStub = errors.New("quark: generated code does not cover this operation (UPDATE/partial/batch binding, or a non-integer primary key); the reflection path is used instead")

ErrGeneratedStub is returned by a generated scanner or binder for an operation the generated code does not cover: today, BindUpdate (the generated binder handles INSERT only) and models whose primary key is not an integer. The runtime treats it as "not covered" and takes the reflection path, so it is transparent to callers — it only surfaces when generated code is invoked directly, or when a generated file predates the current contract version and the version gate keeps it from being used.

View Source
var ErrIrreversibleOperation = fmt.Errorf("operation cannot be reversed from the plan alone")

ErrIrreversibleOperation is returned by Plan.Down for an operation whose inverse is not derivable from the plan. Check with errors.Is.

View Source
var ErrLockTimeout = quarkdriver.ErrLockTimeout

ErrLockTimeout is returned by AcquireMigrationLock when the lock cannot be acquired within the given timeout. Distinct from ErrUnsupportedFeature (which means the dialect doesn't model distributed locks at all). Distinct from generic driver errors. It holds the value of quarkdriver.ErrLockTimeout, so errors.Is matches an error a driver module built from either name.

View Source
var ErrRLSNotEnforced = errors.New("row-level security is not enforced")

ErrRLSNotEnforced is returned, wrapped, by a RowLevelSecurityNative router that cannot confirm the engine enforces row-level security on the table a query is about to touch: the table has row security disabled, has no policy, or the catalog could not be read. The router refuses to serve rather than return every tenant's rows over a database whose policies were never installed (A8 S8). quarktenant.ErrRLSNotEnforced is this value.

View Source
var ErrTenantMismatch = errors.New("tenant mismatch inside transaction")

ErrTenantMismatch is returned, wrapped, by a query built inside a transaction whose context resolves a different tenant from the one the transaction was opened for. The transaction fixes the tenant (ADR-0025): under RowLevelSecurityNative the engine is already filtering by the tenant TenantRouter.Tx set on that connection, and under DatabasePerTenant the pool was chosen for it, so a query naming another tenant cannot be honoured without silently reading or writing across tenants. A context that resolves NO tenant inherits the transaction's instead.

View Source
var HasPlaceholders = guard.HasPlaceholders

HasPlaceholders checks if a query string contains parameter placeholders.

Functions

func Call

func Call(ctx context.Context, provider ClientProvider, procedure string, args ...any) error

Call executes a stored procedure that does not return a result set, but may modify OUT parameters.

func CanonicalType added in v0.11.0

func CanonicalType(s string) string

CanonicalType normalizes a Go type string so the reflection renderer (reflect.Type.String) and the go/types renderer agree. They diverge on alias spellings: reflect prints byte as uint8, rune as int32, and the empty interface as "interface {}", while go/types may print "byte", "rune", and "any". Both sides run their type strings through this, so the hash is stable regardless of which spelling the source used.

func CheckGeneratedDrift added in v0.11.0

func CheckGeneratedDrift(t reflect.Type) (drifted bool, hasGenerated bool)

CheckGeneratedDrift reports whether model t has generated code whose recorded ModelHash no longer matches the model's current shape — i.e. the struct changed but `quark gen` was not re-run. It returns (drifted, hasGenerated): hasGenerated is false when t has no generated code at all (nothing to drift from). This is the optional stale-codegen check ADR-0014 requires F6-1 to make possible; the runtime never calls it automatically.

func DefaultShardResolver added in v1.0.0

func DefaultShardResolver(ctx context.Context) string

DefaultShardResolver reads the shard key set by WithShardKey. Pass it to NewShardRouter for the common case.

func DeleteBatchOf added in v1.8.0

func DeleteBatchOf[T any, V any](q *Query[T], ids []V) (int64, error)

DeleteBatch deletes multiple records by their primary key values using DELETE … WHERE pk IN (…) statements, chunked to batchChunkSize to stay within every supported dialect's placeholder limit (Oracle: 1000, MSSQL: ~2100, others: larger).

The query's conditions are ANDed with the ids, as in Delete (QK-40): an id whose row does not satisfy them is not deleted.

Example:

affected, err := quark.For[User](ctx, client).DeleteBatch([]any{1, 2, 3})

DeleteBatchOf is the typed-slice form of Query.DeleteBatch (AQ-07): it accepts the ids as they usually arrive — []int64 from a previous query, []string for natural keys — and performs the []any conversion once, internally. Package-level function because Go methods cannot introduce a second type parameter.

ids := []int64{1, 2, 3}
n, err := quark.DeleteBatchOf(quark.For[User](ctx, client), ids)

func EscapeLike added in v1.15.0

func EscapeLike(text string) string

EscapeLike escapes text so that a LIKE pattern built around it matches the text literally: `%`, `_` and the backslash itself are prefixed with a backslash. The result is meant for the escaped LIKE surfaces — Query.WhereLike, TypedStringColumn.LikeEscaped, Like — which declare the escape character on the statement; in a plain `Where(col, "LIKE", …)` the backslash keeps whatever meaning the engine gives it by default.

The set is the portable one. SQL Server also reads `[` as the start of a character class, but Oracle refuses an escape character in front of anything that is not `%`, `_` or the escape itself (ORA-01424), so `[` cannot be escaped everywhere. The text searches — Query.WhereContains and friends — escape it on SQL Server and nowhere else, because they know the dialect; when composing a pattern by hand for SQL Server, write `\[` yourself.

quark.For[Doc](ctx, c).WhereLike("title", "%"+quark.EscapeLike(userText)+"%")

func GeneratedBinderRegistered added in v0.11.0

func GeneratedBinderRegistered(t reflect.Type) bool

GeneratedBinderRegistered reports whether a compatible generated binder is registered for t that actually handles inserts — i.e. a real binder, not the stub (which returns ErrGeneratedStub). Intended for tests that want to assert the generated write path is exercised rather than reflection.

func HashModelFields added in v0.11.0

func HashModelFields(fields []ModelField) string

HashModelFields returns a deterministic hash of a model's persisted shape from its fields. It is the single source of truth for the model hash: the runtime (ModelHash) and the generator both call it, so an unchanged model hashes identically on both sides. Field order does not matter.

func IsDeadlock added in v1.7.0

func IsDeadlock(err error) bool

IsDeadlock reports whether err is a deadlock that the database resolved by choosing this transaction as the victim. Such a transaction is fully rolled back and is safe to re-run: retrying is the correct response, not an error to surface.

Client.Tx already retries deadlocks when the client is built with WithDeadlockRetry; this predicate is for callers driving transactions themselves, or deciding whether a failed unit of work is worth requeuing.

SQLite never reports a deadlock — it is single-writer, and its SQLITE_BUSY is lock contention, which retrying in a tight loop makes worse. It is intentionally not classified here.

func IsUniqueViolation added in v1.7.0

func IsUniqueViolation(err error) bool

IsUniqueViolation reports whether err was caused by a unique or primary-key constraint. It is the signal to turn a failed insert into "that value is already taken" rather than an internal error:

if err := users.Create(ctx, u); err != nil {
    if quark.IsUniqueViolation(err) {
        http.Error(w, "email already registered", http.StatusConflict)
        return
    }
    return err
}

It does NOT report foreign-key, not-null or check violations; those still arrive as ErrConstraintViolation. Deliberately narrow: a caller acting on "unique" wants to point at one field, and widening the predicate later would silently change what that branch catches.

func ModelHash added in v0.11.0

func ModelHash(t reflect.Type) string

ModelHash returns a deterministic hash of model type t's persisted shape: for each exported field carrying a `db` tag, the field name, column name, primary-key flag, and Go type. It is the reflection-side caller of HashModelFields; the generator computes the same hash from the AST. The F6-1 conformance test asserts the two agree for a sample model, guarding against the two-tag-interpreters drift risk noted in ADR-0014.

func Notify

func Notify(ctx context.Context, provider ClientProvider, channel, payload string) error

Notify triggers a database PubSub notification (PostgreSQL `pg_notify`). It is unrelated to EventBus.Publish — Notify is the raw LISTEN/NOTIFY emit, not a CRUD lifecycle event. Only PostgreSQL is supported; other dialects return an error.

func RegisterDialect

func RegisterDialect(name string, d Dialect)

RegisterDialect allows developers to register custom database dialects. This enables support for proprietary or non-standard databases.

Example:

quark.RegisterDialect("cockroach", myCockroachDialect)

A client opened with that driver name resolves it through the registry:

client, err := quark.New("cockroach", dsn)

and a pool opened under another driver name takes it explicitly:

d, _ := quark.DetectDialectByName("cockroach")
client, err := quark.NewWithDB("pgx", db, quark.WithDialect(d))

The registry is quarkdriver's (ADR-0026): RegisterDialect calls quarkdriver.RegisterDialect, so a dialect a driver module registers there without importing this package and one registered here are found the same way. RegisterDialect is safe to call concurrently with itself, DetectDialect and DetectDialectByName. Registering a name again replaces the dialect registered under it.

func RegisterGeneratedMeta added in v0.11.0

func RegisterGeneratedMeta(t reflect.Type, meta GeneratedMeta)

RegisterGeneratedMeta records the contract version and model hash for a generated model. Called from the init() of a generated file; not intended for hand use.

func RegisterTypeMapper added in v0.3.0

func RegisterTypeMapper(t reflect.Type, m TypeMapper)

RegisterTypeMapper registers a custom Go-type → SQL-type mapping for the migration / sync layer. Pointer types are stripped before registration: registering for time.Duration also covers *time.Duration. Re-registering the same type overwrites the previous mapper.

Example: storing google/uuid.UUID as native UUID on Postgres and as a 36-char string on the rest:

quark.RegisterTypeMapper(reflect.TypeOf(uuid.UUID{}), func(d string, _ quark.TypeOptions) string {
    if d == "postgres" {
        return "UUID"
    }
    return "VARCHAR(36)"
})

The mapper is consulted by client.Migrate, client.Sync and client.PlanMigration, before the dialect is asked for the column's type (quarkdriver.ColumnTyper). database/sql's Scanner / driver.Valuer interfaces still apply to the read/write side — a type registered here must also implement those interfaces (or be already supported by the underlying driver) for round-trip to work.

func RegisterTypedBinder added in v0.11.0

func RegisterTypedBinder(t reflect.Type, fn TypedBinder)

RegisterTypedBinder records the generated insert/update binder for model type t. Called from the init() of a generated file; not intended for hand use.

func RegisterTypedScanner added in v0.11.0

func RegisterTypedScanner(t reflect.Type, fn TypedScanner)

RegisterTypedScanner records the generated row scanner for model type t. Called from the init() of a generated file; not intended for hand use.

func ScanTarget added in v0.11.0

func ScanTarget(ptr any) any

ScanTarget returns the rows.Scan target for a model field pointer, matching the no-timezone scan behavior of the reflection path (including the string/[]byte time parsing that drivers like SQLite require). It is called by generated scanners — which run only when the per-column timezone feature is inactive, so a nil location is always correct. Not intended for hand use.

func ScatterCount added in v1.2.0

func ScatterCount[T any](ctx context.Context, r *ShardRouter, build func(*Query[T]) *Query[T]) (int64, error)

ScatterCount runs the count built by build on every shard concurrently and SUMS the per-shard counts — the cross-shard COUNT of ADR-0016. build may add a WHERE, or be nil for a full count. Any shard error is returned (the sum would otherwise be silently low). Disjoint shards make the sum exact — a row lives on exactly one shard.

func ScatterGather added in v1.2.0

func ScatterGather[T any](ctx context.Context, r *ShardRouter, build func(*Query[T]) *Query[T], merge ScatterMerge[T]) ([]T, error)

ScatterGather runs the read built by build on every shard CONCURRENTLY and merges the rows — the explicit cross-shard fan-out of ADR-0016 (never the implicit fallback of a forgotten shard key). build configures the per-shard query (the same query runs on each shard); merge orders and caps the combined result:

users, err := quark.ScatterGather(ctx, router,
    func(q *quark.Query[User]) *quark.Query[User] {
        return q.Where("active", "=", true).OrderBy("created_at", "DESC").Limit(20)
    },
    quark.ScatterMerge[User]{
        Less:  func(a, b User) bool { return a.CreatedAt.After(b.CreatedAt) },
        Limit: 20, // global top-20 across all shards
    },
)

If any shard errors, ScatterGather returns that error (from the lowest-named shard that failed) — a partial result over disjoint shards is incomplete, so it is reported rather than silently truncated. Aggregates beyond count (AVG/MIN/MAX) and cross-shard GROUP BY are NOT merged across shards — run those per shard. The merged rows are materialised in memory.

func ShardKeyFromContext added in v1.0.0

func ShardKeyFromContext(ctx context.Context) string

ShardKeyFromContext returns the shard key set by WithShardKey, or "".

func Sticky added in v0.13.0

func Sticky(ctx context.Context) context.Context

Sticky returns a context that pins subsequent read queries to the primary connection instead of a read replica. Use it for read-your-writes: a read that must observe a write you just made (replicas are eventually consistent, so a normal read may be stale). Reads inside Client.Tx already use the transaction's connection and need no Sticky.

It is a no-op when no replicas are configured.

func StubBinder added in v0.11.0

func StubBinder(any, BindMode) ([]string, []any, error)

StubBinder is the inert binder registered by generated code that predates typed binders (contract v1 and v2). See ErrGeneratedStub.

func StubScanner added in v0.11.0

func StubScanner(*sql.Rows, any) error

StubScanner is the inert scanner registered by generated code that predates typed scanners (contract v1). See ErrGeneratedStub.

func TrackReads added in v1.4.0

func TrackReads(ctx context.Context) context.Context

TrackReads returns a context that counts single-row primary-key reads per table for N+1 detection (#247). Attach it at a request/operation boundary; every quark read built from the returned context (or a child of it) shares the same counters. When a table accumulates 10 point reads by primary key in one tracked context — the classic "First/Find per row of a previous List" loop — the client logs one structured WARN for that context+table suggesting Preload.

Only active when the client runs with WithStrictReads at StrictReadsWarn or StrictReadsReject; on an untracked context, or under StrictReadsOff, nothing is counted and nothing is allocated on the read path. Cache-served reads count too: the pattern costs a round trip per row again the moment the entries expire.

func WithShardKey added in v1.0.0

func WithShardKey(ctx context.Context, key string) context.Context

WithShardKey returns a context carrying the shard key for the operations run with it. A ShardRouter built with DefaultShardResolver reads this value.

func WithShardKeyOf added in v1.2.0

func WithShardKeyOf(ctx context.Context, entity ShardKeyer) context.Context

WithShardKeyOf returns a context carrying entity.ShardKey() as the shard key, so a write routes to the shard the entity belongs to:

ctx = quark.WithShardKeyOf(ctx, &user) // user implements ShardKeyer
err := quark.For[User](ctx, shardRouter).Create(&user)

It is exactly WithShardKey(ctx, entity.ShardKey()) — a convenience that keeps the partition field in one place (the model's ShardKey method). The router is unchanged and still unaware of sharding (ADR-0016): routing happens in GetClient(ctx), so the key must be in context before For[T] resolves the shard — this helper puts it there from the entity.

Types

type AfterCreateHook

type AfterCreateHook interface {
	AfterCreate(ctx context.Context) error
}

AfterCreateHook is executed after an entity is created.

type AfterDeleteHook

type AfterDeleteHook interface {
	AfterDelete(ctx context.Context) error
}

AfterDeleteHook is executed after an entity is deleted.

type AfterFindHook added in v0.9.0

type AfterFindHook interface {
	AfterFind(ctx context.Context) error
}

AfterFindHook is executed once the rows of a read query have been scanned into the result slice (or single value). The hook receives the request context and fires synchronously on the result-bearing return path; returning a non-nil error replaces the would-be-returned value with that error.

Implement on a *T where T is the model type. The hook fires exactly once per query — not once per row.

Typical uses: post-scan enrichment, lazy join hydration not covered by Query.Preload, or read auditing.

type AfterUpdateHook

type AfterUpdateHook interface {
	AfterUpdate(ctx context.Context) error
}

AfterUpdateHook is executed after an entity is updated.

type Array added in v0.6.0

type Array[T any] struct {
	V []T
}

Array[T] is the typed wrapper for a column that holds a list of T. It round-trips through JSON regardless of dialect, so the same model definition works on every supported engine without per-dialect Scan / Value code:

Postgres → JSONB
MySQL / MariaDB → JSON
SQLite → TEXT
SQL Server → NVARCHAR(MAX)
Oracle → CLOB

The semantic clarity vs. `JSON[[]T]`: `Tags Array[string]` reads as "the column holds a list of strings"; the helper methods (`Len`, `Slice`, `Contains`) carry that intent through call sites that read the field.

Trade-off: this wrapper does NOT use Postgres' native `INT[]` / `TEXT[]` array types — operators like `@>`, `&&`, and `array_agg` won't fire on Array[T] columns. For PG-native arrays with operators drop down to `pgx`/`pgtype.Array` directly or use `RawQuery`. The "neutral wrapper" spec in TASKS § Bloque B explicitly asked for a dialect-independent type that doesn't import `pgtype`; JSON-backed is the simplest shape that satisfies that constraint.

Example:

type Post struct {
    ID   int64                 `db:"id" pk:"true"`
    Tags quark.Array[string]   `db:"tags"`
}

p := Post{Tags: quark.Array[string]{V: []string{"go", "orm"}}}
_ = client.Migrate(ctx, &Post{})
_ = quark.For[Post](ctx, client).Create(&p)

func (Array[T]) Len added in v0.6.0

func (a Array[T]) Len() int

Len returns the number of elements in the array. Safe on the zero value (returns 0).

func (*Array[T]) Scan added in v0.6.0

func (a *Array[T]) Scan(src any) error

Scan implements sql.Scanner. Accepts []byte and string sources (the two forms drivers return for JSON columns). NULL clears V to nil. An empty / whitespace-only payload also resolves to nil so a column default of `'[]'` round-trips through the zero value cleanly.

func (Array[T]) Slice added in v0.6.0

func (a Array[T]) Slice() []T

Slice returns the underlying slice. Mutations on the returned slice affect the Array's storage — useful for appending without re-allocating, dangerous if the Array value is used after.

func (Array[T]) Value added in v0.6.0

func (a Array[T]) Value() (driver.Value, error)

Value implements driver.Valuer by JSON-marshalling V. A nil V serialises to `[]` (empty array) rather than `null` — the more useful default when the column is also used in SQL operations. Pair with quark.Nullable[Array[T]] when you need to distinguish NULL from empty.

The result is a string, not []byte, for the same reason as JSON.Value: a []byte binds as VARBINARY on go-mssqldb and corrupts the NVARCHAR(MAX) column via an implicit UTF-16 reinterpretation.

type AuditConfig added in v0.9.0

type AuditConfig struct {
	// UserFromContext, when set, extracts the acting user identifier
	// from the request context for the `user_id` column. Returns ""
	// when unknown. nil leaves user_id empty.
	UserFromContext func(context.Context) string

	// TenantFromContext, when set, extracts the tenant identifier for
	// the `tenant_id` column. Returns "" when unknown. nil leaves
	// tenant_id empty. (When using a TenantRouter the value is
	// already on the context the resolver reads — reuse the same
	// extractor here.)
	TenantFromContext func(context.Context) string

	// IncludeTables, when non-empty, restricts auditing to exactly
	// these table names. Empty means "all tables" (subject to
	// ExcludeTables). The audit table itself (`quark_audit`) is
	// always excluded regardless of this list — auditing the audit
	// log would recurse.
	IncludeTables []string

	// ExcludeTables names tables that are never audited. Takes
	// precedence over IncludeTables.
	ExcludeTables []string
}

AuditConfig configures the optional audit log (F5-7). Pass it to Client.EnableAuditLog. The zero value audits every table with empty user/tenant attribution; populate the callbacks and filters to taste.

type BackfillSpec added in v0.6.0

type BackfillSpec struct {
	// Name uniquely identifies this backfill across runs. Used as
	// the primary key of `quark_backfill_state`. Two backfills with
	// the same Name share the same resume token — that's how a
	// retry resumes. Different backfills MUST use different Names
	// or they'll trample each other's state.
	Name string

	// Table is the source table being iterated.
	Table string

	// PKColumn is the column the helper orders + paginates by.
	// Defaults to "id" if empty. The column must be a sortable
	// integer-like type (int64 / bigint) — text PKs aren't
	// supported in F3-6-core. Composite PKs aren't supported
	// either; the helper takes the first PK column only.
	PKColumn string

	// BatchSize is the number of PKs fetched per round-trip.
	// Defaults to 1000. Larger batches are more efficient but
	// hold locks longer; tune for your workload.
	BatchSize int

	// Process is invoked once per batch with the PK list in
	// ascending order. The callback can do any SQL it wants
	// (typically UPDATE ... WHERE id IN (...) or similar). Errors
	// abort the backfill and propagate; the state table records
	// the highest PK from successful batches, so a retry resumes
	// from there.
	Process func(ctx context.Context, batchPKs []int64) error
}

BackfillSpec describes a single backfill operation. A backfill iterates a table by primary key in batches, invokes a user callback per batch, and persists the highest PK seen so a process kill (or a deliberate retry) resumes at the next un-processed row rather than re-running the entire table.

The user callback receives the slice of PKs in the current batch and is responsible for whatever data work the backfill needs — the helper deliberately doesn't read row contents itself. Why: backfill SQL is rarely "SELECT * + transform"; it's usually "UPDATE ... WHERE id IN (...)" or "INSERT ... SELECT ... WHERE id IN (...)" where the user knows the relevant columns and the helper would just be in the way trying to scan them generically.

type BaseMiddleware

type BaseMiddleware struct{}

BaseMiddleware provides default implementations that pass through to the next handler. Embed this in your middleware so you only need to override the methods you care about.

func (BaseMiddleware) WrapExec

func (BaseMiddleware) WrapExec(next ExecFunc) ExecFunc

func (BaseMiddleware) WrapQuery

func (BaseMiddleware) WrapQuery(next QueryFunc) QueryFunc

func (BaseMiddleware) WrapQueryRow

func (BaseMiddleware) WrapQueryRow(next QueryRowFunc) QueryRowFunc

type BaseQuery

type BaseQuery struct {
	// contains filtered or unexported fields
}

BaseQuery holds the non-generic state of a database query.

type BeforeCreateHook

type BeforeCreateHook interface {
	BeforeCreate(ctx context.Context) error
}

BeforeCreateHook is executed before an entity is created.

type BeforeDeleteHook

type BeforeDeleteHook interface {
	BeforeDelete(ctx context.Context) error
}

BeforeDeleteHook is executed before an entity is deleted.

type BeforeFindHook added in v0.9.0

type BeforeFindHook interface {
	BeforeFind(ctx context.Context) error
}

BeforeFindHook is executed before a read query is dispatched — once per call to Query.List, Query.First, Query.Find, or the streaming variants (Query.Iter / Query.Cursor). The hook receives the request context the query was constructed with and fires synchronously: returning a non-nil error aborts the query before any SQL is emitted, and the error propagates to the caller.

Implement on a *T where T is the model type. Quark resolves the hook by checking whether `&T{}` satisfies the interface — no registration needed.

Typical uses: injecting an additional tenant predicate, enabling soft-delete semantics by default, or logging read intent.

type BeforeUpdateHook

type BeforeUpdateHook interface {
	BeforeUpdate(ctx context.Context) error
}

BeforeUpdateHook is executed before an entity is updated.

type BindMode added in v0.11.0

type BindMode int

BindMode selects which column set a TypedBinder produces. Insert binds every persisted column; Update binds the non-PK columns of a full-row update. (Partial updates — buildUpdateMap / UpdateFields — are handled by F6-3 and may extend this enum; new values append, never reorder.)

const (
	// BindInsert requests the columns and args for an INSERT.
	BindInsert BindMode = iota
	// BindUpdate requests the non-PK columns and args for a full UPDATE.
	BindUpdate
)

type CacheConfig

type CacheConfig struct {
	TTL     time.Duration
	Tags    []string
	Enabled bool
}

CacheConfig holds the caching parameters for a specific query.

type CacheLocker added in v1.2.0

type CacheLocker interface {
	// AcquireLock attempts to claim per-key recompute rights WITHOUT blocking.
	// It returns acquired=true and a release func to the single winner; every
	// other caller gets acquired=false (and a nil release) and should
	// wait-and-reread the winner's value. The lock auto-expires after ttl so a
	// crashed holder cannot wedge the key. release is safe to call once; it must
	// only delete the lock if this caller still owns it (token-checked), so a
	// late release after ttl expiry never clobbers a new holder.
	AcquireLock(ctx context.Context, key string, ttl time.Duration) (acquired bool, release func() error, err error)
}

CacheLocker is an OPTIONAL capability a CacheStore may implement to enable cross-instance stampede coordination (ADR-0020). The stampede wrapper detects it by type-assertion: when present AND WithCacheCrossInstance is enabled, a cache miss on a hot key is serialised across processes — one holder recomputes while the others wait-and-reread — instead of every process recomputing (in-process singleflight only dedupes within one process). Stores that do not implement it fall back to singleflight + XFetch unchanged.

type CacheStore

type CacheStore interface {
	// Get retrieves a value from the cache.
	Get(ctx context.Context, key string) ([]byte, error)
	// Set stores a value in the cache with a specific TTL and associated tags.
	Set(ctx context.Context, key string, val []byte, ttl time.Duration, tags ...string) error
	// Delete removes a specific key.
	Delete(ctx context.Context, key string) error
	// InvalidateTags removes all entries associated with the given tags (usually table names).
	InvalidateTags(ctx context.Context, tags ...string) error
}

CacheStore defines the contract for any caching backend. Implementations should be provided in separate packages (e.g., quark/cache/redis).

type CaseBuilder added in v1.14.0

type CaseBuilder struct {
	// contains filtered or unexported fields
}

CaseBuilder accumulates the branches of a CASE expression. It is an Expr once it has at least one WHEN, so it can be used anywhere an Expr goes; Else is optional and returns the builder so the call can end there.

func Case added in v1.14.0

func Case() *CaseBuilder

Case builds a searched `CASE WHEN … THEN … [ELSE …] END` expression:

quark.Case().
    When(quark.Eq(quark.Col("status"), quark.Lit("paid")), quark.Lit(1)).
    Else(quark.Lit(0))

It is what makes a conditional aggregate expressible — `SUM(CASE WHEN … THEN 1 ELSE 0 END)` — by wrapping it in Func("SUM", …). A CASE with no WHEN branch is rejected: it is always a mistake, and engines disagree on whether to reject it themselves.

One portability note worth knowing before it costs an afternoon: every Lit() binds as a parameter, so a CASE whose branches are ALL literals has no branch with a known type. PostgreSQL then infers `text` for the whole expression, and wrapping it in SUM fails with "function sum(text) does not exist". Give at least one branch a typed expression — a Col(), typically — and the CASE takes its type. The other five engines are more forgiving, which is what makes this one easy to miss.

func (*CaseBuilder) Else added in v1.14.0

func (c *CaseBuilder) Else(result Expr) *CaseBuilder

Else sets the `ELSE <result>` arm. Without it a CASE that matches no branch yields NULL, which is the SQL default and rarely what an aggregate wants.

func (*CaseBuilder) ToSQL added in v1.14.0

func (c *CaseBuilder) ToSQL(d Dialect, g *SQLGuard) (string, []any, error)

func (*CaseBuilder) When added in v1.14.0

func (c *CaseBuilder) When(cond, result Expr) *CaseBuilder

When adds a `WHEN <cond> THEN <result>` branch. Branches render in the order they are added, which is the order the engine evaluates them.

type Check added in v0.6.0

type Check = quarkdriver.Check

Check is one CHECK constraint declared on a table, with its raw catalog expression.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the main entry point for quark ORM operations. It wraps a database connection and provides type-safe query building.

func New

func New(driverName, dataSource string, opts ...any) (*Client, error)

New creates a new quark Client with the given driver name and data source.

Example:

client, err := quark.New("sqlite", "example.db",
    quark.WithMaxOpenConns(25),
    quark.WithMaxIdleConns(5),
)

For PostgreSQL:

client, err := quark.New("pgx", "postgres://user:pass@localhost/db",
    quark.WithMaxOpenConns(25),
)

The dialect is auto-detected from the driver name. You can override it with WithDialect().

func NewWithDB added in v1.8.0

func NewWithDB(driverName string, db *sql.DB, opts ...any) (*Client, error)

NewWithDB creates a quark Client on top of an EXISTING *sql.DB, reusing the caller's connection pool instead of opening a second one. This is the seam for host frameworks that already own a pool (e.g. Nucleus): the host keeps managing the pool's lifecycle and sizing, and quark speaks through it.

driverName is still required — it drives dialect auto-detection exactly as in New (override with WithDialect for a custom driver). The handle is pinged before the client is returned, so a dead pool fails fast with ErrConnection.

Ownership rules, on purpose:

  • Client.Close does NOT close a borrowed *sql.DB — the owner opened it, the owner closes it. Replica pools opened by quark via WithReplicas ARE closed, since quark opened those.

  • PoolOption values (WithMaxOpenConns, …) are applied to the SHARED handle: passing one is an explicit request to tune the host's pool. Omit them to leave the host's pool configuration untouched.

  • Client.WithOptions derives the new client over the same shared handle.

    db, _ := sql.Open("pgx", dsn) // the host's pool client, err := quark.NewWithDB("pgx", db)

func (*Client) AcquireMigrationLock added in v0.6.0

func (c *Client) AcquireMigrationLock(ctx context.Context, name string, timeout time.Duration) (MigrationLock, error)

AcquireMigrationLock attempts to acquire a cluster-wide advisory lock named `name` for migration operations. The first concurrent caller wins; subsequent callers block up to `timeout` (or receive ErrLockTimeout if the timeout elapses).

Typical use:

lock, err := client.AcquireMigrationLock(ctx, "schema-migrations", 30*time.Second)
if err != nil {
    return err
}
defer lock.Release(ctx)

if err := client.Migrate(ctx, &User{}, &Order{}); err != nil {
    return err
}

Behaviour per dialect (see TASKS § F3-1):

  • PostgreSQL: `pg_advisory_lock(hashtext(name))` on a dedicated connection. Released by `pg_advisory_unlock` on Release.
  • MySQL / MariaDB: `GET_LOCK(name, timeout_seconds)` + `RELEASE_LOCK`.
  • MSSQL: `sp_getapplock @LockMode='Exclusive', @LockOwner='Session'`
  • `sp_releaseapplock`.
  • Oracle: `DBMS_LOCK.ALLOCATE_UNIQUE` + `REQUEST(X_MODE, release_on_commit => FALSE)` — session-scoped, survives the implicit commits of DDL. Requires `GRANT EXECUTE ON DBMS_LOCK` (see ADR-0018).
  • SQLite: returns `ErrUnsupportedFeature` — no distributed-lock primitive; use a `BEGIN IMMEDIATE` transaction inside the process for single-writer semantics.

func (*Client) AddForeignKey

func (c *Client) AddForeignKey(ctx context.Context, table, constraintName string, columns []string, refTable string, refColumns []string, onDelete, onUpdate string) error

AddForeignKey adds a FOREIGN KEY constraint to an existing table. constraintName is the constraint identifier; refTable is the referenced table; columns and refColumns are matched by position.

onDelete and onUpdate are referential actions ("CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"); an empty one writes no clause, and the engine applies its default, NO ACTION. They are written as the dialect's engine takes them (quarkdriver.ReferentialActioner): Oracle has no clause for NO ACTION, so it is left out, and an action the engine does not have — RESTRICT on SQL Server; on Oracle anything but ON DELETE CASCADE and ON DELETE SET NULL — returns an error wrapping ErrUnsupportedFeature that names it, and nothing is sent.

Example:

client.AddForeignKey(ctx, "orders", "fk_orders_user", []string{"user_id"}, "users", []string{"id"}, "CASCADE", "SET NULL")

func (*Client) ApplyPlan added in v0.6.0

func (c *Client) ApplyPlan(ctx context.Context, plan Plan) error

ApplyPlan executes the operations in `plan` against the database in the order they appear. What each op writes is the dialect's answer (A11 Q2) — the `Dialect.AlterTable*` helpers, and the optional interfaces of quarkdriver: ColumnAlterer for an OpAlterColumn, ObjectDropper for the drops, IdempotentDDL for an index, and TableRebuilder for an engine that rebuilds a table instead of altering it (SQLite). A dialect that implements none of them gets the SQL standard's statements.

**Transactional behaviour** (F3-4-tx): the dialect decides, through [Dialect.SupportsTransactionalDDL] — never its name (A11 Q2).

  • **PostgreSQL, MSSQL, SQLite** — DDL is transactional on these engines (SupportsTransactionalDDL is true). ApplyPlan opens a BEGIN, runs all ops, and COMMITs. On ANY failure the transaction is rolled back, leaving the schema in its pre-plan state. This is the safety net users should rely on when running migrations against production.

  • **MySQL, MariaDB, Oracle** (SupportsTransactionalDDL is false) — DDL implicitly commits the current transaction on every statement, so wrapping is pointless. Instead, ApplyPlan uses a **resumable** path backed by a `quark_migration_state` checkpoint table (F3-4-resumable). Each successfully applied op is recorded by `(plan_hash, op_index)`; a re-invocation against the same plan (identified by `Plan.Hash()`) skips ops that were already recorded. A mid-plan failure can therefore be fixed (the underlying constraint addressed, the connection restored, etc.) and resumed simply by calling ApplyPlan again with the same plan — no re-applying earlier successful ops, no manual state management.

    Drift detection: if the plan changed between runs (different ops, different table names, anything that flips the hash), the state from the prior run doesn't apply and the new plan starts fresh from op 0. This is the safety boundary that prevents "resume from op 3" against a plan whose op 3 means something different.

    Schema check (QK-51): a checkpoint answers only for the schema it was recorded against. Before skipping anything, ApplyPlan reads the live schema through the dialect's SchemaIntrospector, and when it shows that an op the checkpoint records as applied is not there — the table the op created is missing, the column it dropped is back, the column it altered still reads as the op's Old — it discards the plan's rows and applies the plan from op 0. Before, the same plan applied again after the schema was put back (tables dropped and recreated, a restore) was skipped whole and ApplyPlan returned nil. Only evidence of absence counts: an op whose effect the catalog does not settle is taken as applied, so a mid-plan failure fixed by hand still resumes, and a plan applied again over a schema that holds it is still a no-op. A dialect without a SchemaIntrospector keeps the checkpoint as recorded.

    **Concurrency**: on non-transactional engines, two processes calling ApplyPlan against the same plan simultaneously race on the state table — both read `resumeFrom = -1`, both try to apply op 0, one (or both) of them hit DDL errors (table already exists) and a `duplicate-PK` on the state insert. `Client.AcquireMigrationLock` (F3-1) is the right primitive to serialise — wrap your ApplyPlan call:

    lock, err := client.AcquireMigrationLock(ctx, "schema", 30*time.Second) if err != nil { return err } defer lock.Release(ctx) err = client.ApplyPlan(ctx, plan)

    The reason this isn't done automatically inside ApplyPlan: not every dialect implements `MigrationLocker` yet (Oracle is pending) and the lock name / timeout are workflow choices that belong to the caller. Future: a `Client.MigrateAtomic` wrapper that bundles lock + plan + apply, target for F3-5.

The returned error carries the index of the op that failed and the op's String() rendering for debuggability, regardless of which path was taken.

Operation-specific caveats:

  • **OpAlterColumn** changes the type, nullability, default and primary key in place (A8 S4). Dropping a primary key needs the constraint's name from the engine's catalog, so a dialect without a quarkdriver.ColumnAlterer gets ErrUnsupportedFeature for that one change. Renaming a column is not an ALTER COLUMN and is refused.
  • **Referential actions** — the ON DELETE and ON UPDATE of an OpAddForeignKey and of the foreign keys of an OpCreateTable — are written as the dialect's engine takes them (quarkdriver.ReferentialActioner): Oracle's NO ACTION is left out, and an action the engine does not have (SQL Server's RESTRICT, Oracle's ON UPDATE CASCADE) refuses the whole plan with ErrUnsupportedFeature before its first operation runs (QK-49).
  • **On SQLite** (a quarkdriver.TableRebuilder) a column change and the constraint ops rebuild the table inside the plan's transaction; a CHECK the rebuild cannot read back makes it refuse with ErrUnsupportedFeature rather than lose the check.

Every statement it sends — its own and those of the dialect's ColumnAlterer and TableRebuilder, which receive the same Executor — passes the middleware chain and reaches the observers: what it executes as StatementDDL, what it reads as StatementIntrospection.

func (*Client) Backfill added in v0.6.0

func (c *Client) Backfill(ctx context.Context, spec BackfillSpec) error

Backfill iterates BackfillSpec.Table by primary key in batches, invoking BackfillSpec.Process for each batch. Persists the highest PK seen in `quark_backfill_state` so a kill / error / deliberate retry resumes at the next un-processed PK instead of re-running the entire table.

Workflow when something goes wrong:

  1. Backfill processes batches 0..N successfully; state.last_pk records the max PK from batch N.
  2. Batch N+1 fails (callback error, process killed, DB connection lost, etc.). State remains at batch N's max.
  3. Caller fixes the underlying issue and re-invokes Backfill with the same Name. The helper reads state.last_pk and starts from `WHERE pk > last_pk` — no re-processing of batches 0..N.

Once the helper sees an empty batch (no PKs left), the backfill is complete. A subsequent re-invocation with the same Name finds nothing to do and returns nil immediately — idempotent.

Concurrency: like ApplyPlan's resumable path, Backfill is not safe for concurrent invocations against the same Name. Wrap with Client.AcquireMigrationLock if you need cross-process serialisation. The state table's primary key on Name prevents silent corruption — concurrent runs would race on the UPDATE, not produce divergent state.

Every statement it sends passes the middleware chain and reaches the observers: the batch reads as StatementQuery, the state table's writes as StatementDDL and its reads as StatementIntrospection, like the rest of Quark's bookkeeping.

func (*Client) BeginTx

func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

BeginTx starts a new database transaction with the given options.

Example:

tx, err := client.BeginTx(ctx, nil)
if err != nil { log.Fatal(err) }
defer tx.Rollback()

quark.ForTx[User](ctx, tx).Create(&user)
tx.Commit()

func (*Client) BlockedPanicCleanups added in v1.4.0

func (c *Client) BlockedPanicCleanups() uint64

BlockedPanicCleanups reports how many detached panic-path cleanups have overrun the watchdog deadline on this Client since it was created. The cleanup only exists under the RowLevelSecurityNative strategy: when a driver panics inside an implicit transaction, the rollback + connection hand-back runs on a detached goroutine, because after a panic database/sql may still hold internal locks that would turn a same-goroutine rollback into a deadlock (see cleanupAbandonedImplicitTx). The trade-off is that when those locks are never released, the detached cleanup blocks silently and the tx/conn pair stays held. Each unit here is one cleanup that did not finish within the deadline (the client's QueryTimeout — see cleanupWatchdogDeadline); the same event is logged once at ERROR through the Client's logger. The cleanup itself keeps trying — this counter only makes the blockage observable, it does not cancel it. A cleanup that finishes after the deadline is still counted: the counter never resets and is safe for concurrent use.

func (*Client) Close

func (c *Client) Close() error

Close closes the underlying database connection and any read-replica pools (F6-5). The primary's error is returned; replica close errors are joined so none is silently dropped.

For a client built with NewWithDB the primary *sql.DB is BORROWED: Close leaves it open (its owner closes it) and only releases the replica pools quark itself opened.

func (*Client) CreateIndex

func (c *Client) CreateIndex(ctx context.Context, table, indexName string, columns []string, unique bool) error

CreateIndex creates an index on the given table and columns. If unique is true, a UNIQUE INDEX is created. If the index already exists the error is silently ignored for compatible dialects.

Example:

client.CreateIndex(ctx, "users", "idx_users_email", []string{"email"}, true)

func (*Client) DeferredCommitFailures added in v1.3.2

func (c *Client) DeferredCommitFailures() uint64

DeferredCommitFailures reports how many deferred implicit-transaction commits have failed on this Client since it was created. Deferred commits only exist under the RowLevelSecurityNative strategy, and since QCD-FW §1 only on the multi-row QueryContext path (QueryRowContext — every write with RETURNING included — commits synchronously before returning): the *sql.Rows implicit transaction commits when the operation's ctx ends — an instant after the operation completes (see nativeRLSExecutor) — so a commit failure happens AFTER the operation already returned success to the caller — each unit here is a write (or read snapshot) that was silently rolled back by the engine. The failure is also logged at ERROR level through the Client's logger at the moment it happens; this counter is the aggregate for operators to alert on. It never resets; it is safe for concurrent use.

func (*Client) Dialect

func (c *Client) Dialect() Dialect

Dialect returns the dialect being used.

func (*Client) DisableAuditLog added in v0.9.0

func (c *Client) DisableAuditLog()

DisableAuditLog turns audit logging back off. Subsequent CRUD stops writing quark_audit rows; the table and its existing rows are left intact. Safe to call when auditing was never enabled.

func (*Client) EnableAuditLog added in v0.9.0

func (c *Client) EnableAuditLog(ctx context.Context, cfg AuditConfig) error

EnableAuditLog turns on the optional audit log for the Client. It migrates the `quark_audit` table (idempotent) and installs the configuration so every subsequent Create / Update / Delete writes an audit row.

The audit row is written **inline on the same connection / transaction as the CRUD operation**, not post-commit. When the operation runs inside Client.Tx, the audit row joins that transaction and commits (or rolls back) atomically with the data — you never end up with committed data lacking its audit trail, nor an audit row for rolled-back work. This is the "junto al commit, no después" guarantee from ADR-0013; it is intentionally stronger than the post-commit `OnCommit` emission used by the EventBus (F5-6), where losing an event on a crash is acceptable but losing an audit record is not. For non-transactional CRUD the audit INSERT is a separate statement immediately after the write — there is a small crash window there, closed by wrapping your writes in Client.Tx.

The diff payload depends on the operation:

  • Create: the full inserted row, `{column: value}`.
  • Delete: the full row being deleted, `{column: value}`.
  • Update via Query.Update / Query.UpdateFields: the new values, `{column: value}` (no prior value — there is no snapshot).
  • Update via Tracked.Save: a per-column delta, `{column: {"old": …, "new": …}}`, because the tracked snapshot carries the prior values.

Bulk paths (CreateBatch / UpdateBatch / DeleteBatch / DeleteBy / UpdateMap) are not audited — same scope boundary as hooks and events.

EnableAuditLog is intended to be called once at setup. Calling it again replaces the configuration.

func (*Client) Exec

func (c *Client) Exec(ctx context.Context, query string, args ...any) error

Exec executes a raw SQL statement (INSERT, UPDATE, DELETE, DDL). This is primarily used for migrations and schema changes. It passes the middleware chain and reaches the observers as a RAW_EXEC event of kind raw.

func (*Client) GetClient

func (c *Client) GetClient(ctx context.Context) (*Client, error)

GetClient implements ClientProvider for the basic Client.

func (*Client) IntrospectSchema added in v0.6.0

func (c *Client) IntrospectSchema(ctx context.Context) (Schema, error)

IntrospectSchema reads the current state of the database's schema and returns it as a dialect-neutral Schema. It's the first half of the F3 migration story: the diff comparator (F3-3) takes the Schema produced here plus the Schema derived from the Go models and emits the operations needed to bring them into alignment.

Supported dialects: PostgreSQL, SQLite, MySQL, MariaDB, MSSQL, Oracle. A dialect that doesn't implement SchemaIntrospector returns `ErrUnsupportedFeature`.

Surface: tables, columns (primary-key membership included via [Column.PrimaryKey]), non-PK indexes, foreign keys, and CHECK constraints. SQLite returns `Checks=nil` (the only catalog read it doesn't implement; see the Check godoc for the rationale).

Code that reads Schema should treat the unpopulated slices as "not yet introspected" (or, for SQLite Checks, "intentionally not surfaced"), not "no constraints exist".

The dialect reads the catalog through an Executor whose statements pass the middleware chain and reach the observers as StatementIntrospection.

func (*Client) Logger added in v1.11.0

func (c *Client) Logger() *slog.Logger

Raw returns the underlying *sql.DB for advanced operations. Use with caution - this bypasses quark's safety features. Logger returns the logger the client was built with (WithLogger), or the process default: the migrator and anything else that reports on the client's behalf writes through it instead of to stdout (QK-6).

func (*Client) Migrate

func (c *Client) Migrate(ctx context.Context, models ...any) error

Migrate creates tables for the given models if they don't exist. This is a simplistic auto-migration tool for development. It uses the "db" and "pk" tags to generate CREATE TABLE statements. It also creates join tables for many-to-many relations.

Its statements pass the middleware chain and reach the observers as schema work (StatementDDL).

func (*Client) MigrateRegistered added in v0.6.0

func (c *Client) MigrateRegistered(ctx context.Context) error

MigrateRegistered is a convenience for `Migrate(ctx, c.RegisteredModels()...)`. Returns nil immediately if no models have been registered (no-op rather than error — letting the caller initialise the Client in stages without worrying about the registration phase).

func (*Client) PlanMigration added in v0.6.0

func (c *Client) PlanMigration(ctx context.Context, models ...any) (Plan, error)

PlanMigration computes the Plan of operations needed to align the database schema with the Go-side models. It does NOT execute anything — the returned Plan is inert.

The pipeline is:

  1. Build a `desired` Schema by reflecting on the model structs (the same metadata path Client.Migrate uses to render CREATE TABLE DDL).
  2. Read the `current` Schema via Client.IntrospectSchema.
  3. Call Diff to produce the ordered op list.
  4. Wrap the ops in a Plan.

Surface caveats — known asymmetries between desired and current:

  • **Type strings**: the desired Schema uses the migrator's column types (`BIGINT`, `VARCHAR(255)`) while the introspector returns whatever the catalog stores (lowercase, parameter-bearing, or canonical form per dialect). The diff's [normalizeType] helper collapses these to a comparable form (case-fold, PG `character varying` ↔ `varchar` alias, MySQL display-width strip), so a clean round-trip works on all 5 supported dialects since F3-3-types. Edge cases not yet normalised: PG `int8`/`int4`/`int2` aliases (don't arise from introspection; only relevant if the desired Schema is hand-constructed with those names).
  • **Indexes declared on the model** (quark:"index" / quark:"index=<name>") are part of the desired schema: a declared index that is missing is proposed as OpCreateIndex, and one whose live shape differs (columns, uniqueness) as DROP + CREATE. A quark:"unique" column is a column constraint whose backing index the engine names; Diff matches it by shape, so it never drifts.
  • **Everything else the model cannot declare** — FKs, CHECKs and indexes with no tag — is **copied** from the current schema into the desired one before diffing, so a plan never proposes dropping a catalog object the tags are simply silent about. Those are managed by hand (CreateIndex / AddForeignKey) or by a hand-built Schema (see Diff).
  • **m2m join tables ARE part of the desired schema**: for every `rel:"many_to_many"` + `m2m:"join:fk:ref_fk"` tag, the desired Schema includes the join table with the same shape Client.Migrate's createJoinTables emits (two int FK columns, composite PK). Without this, the diff would propose DROPping a table Quark itself created — destroying the join rows if the plan were applied (superapp finding, task_b03f2155). An explicit model mapping the join-table name takes precedence over the synthetic shape.

SQLite quirk: same Checks=nil handling as the rest of F3-3-core — no spurious drops when the database doesn't introspect checks.

func (*Client) PlanMigrationRegistered added in v0.6.0

func (c *Client) PlanMigrationRegistered(ctx context.Context) (Plan, error)

PlanMigrationRegistered is a convenience for `PlanMigration(ctx, c.RegisteredModels()...)`. Returns an empty Plan when no models are registered (consistent with the IsEmpty() semantics used elsewhere — no models means nothing to plan against).

func (*Client) Raw

func (c *Client) Raw() *sql.DB

func (*Client) RawQuery

func (c *Client) RawQuery(ctx context.Context, query string, args ...any) (*sql.Rows, error)

RawQuery executes a raw SQL query with the given arguments. By default, this requires placeholders to prevent SQL injection. Enable with WithLimits(Limits{AllowRawQueries: true}).

func (*Client) RegisterModel added in v0.6.0

func (c *Client) RegisterModel(models ...any) error

RegisterModel records one or more model values on the Client so follow-on calls like Client.MigrateRegistered and Client.PlanMigrationRegistered don't need the caller to re-list them. The typical setup pattern:

client, _ := quark.New(...)
client.RegisterModel(&User{}, &Order{}, &Invoice{})
if err := client.MigrateRegistered(ctx); err != nil { ... }

Each model goes through the same reflection validation as Client.Migrate (must be a struct value or pointer to struct; nil is rejected) so registration fails fast on a bad model rather than at migration time.

Calling RegisterModel multiple times APPENDS to the registry — it does NOT deduplicate. If you call `RegisterModel(&User{})` twice, Client.MigrateRegistered will see User twice. That's intentional: Quark doesn't try to be clever about identity of reflect.Type-equal values; the caller controls the list.

Safe for concurrent use — the registry is mutex-protected. In practice you'll call this once at startup, not from request handlers.

The per-Client registry is intentionally additive — it does NOT replace the global type-meta cache in `internal/schema`, which is correct as global state (deterministic per `reflect.Type`). F3-7 only adds per-Client state for "which models does this Client manage", not for "what's the cached meta of type X".

func (*Client) RegisteredModels added in v0.6.0

func (c *Client) RegisteredModels() []any

RegisteredModels returns a snapshot of the models registered on this Client via Client.RegisterModel. The returned slice is a COPY — mutations to the returned value don't affect the internal registry. Order matches the registration order.

Useful for introspection / debugging and for the user CLI wrappers (e.g. `quarkmigrate.Run` could accept a Client with pre-registered models instead of taking the variadic models argument — though that's not yet wired).

func (*Client) Sync

func (c *Client) Sync(ctx context.Context, opts SyncOptions, models ...any) error

Sync synchronizes the database schema with the provided models. It detects missing columns, renames, and can drop columns if safe mode is disabled.

Sync reads the current columns through the dialect's SchemaIntrospector and writes column types through its quarkdriver.ColumnTyper, like Client.Migrate; a dialect without a SchemaIntrospector cannot be synced and Sync returns ErrUnsupportedFeature.

Its statements pass the middleware chain and reach the observers: the catalog reads as StatementIntrospection, the CREATE and ALTER TABLE as StatementDDL.

func (*Client) Tx

func (c *Client) Tx(ctx context.Context, fn func(tx *Tx) error) error

Tx executes fn within a transaction. If fn returns nil, the transaction is committed. If fn returns an error or panics, the transaction is rolled back.

Example:

err := client.Tx(ctx, func(tx *quark.Tx) error {
    quark.ForTx[User](ctx, tx).Create(&user)
    quark.ForTx[Order](ctx, tx).Create(&order)
    return nil // auto-commit
})

When WithDeadlockRetry(maxAttempts) is configured (F4-7), a deadlock raised from inside fn triggers a fresh-transaction retry with exponential backoff + jitter, up to maxAttempts total attempts. Non-deadlock errors propagate immediately. Disabled by default — callers explicitly opt in to retry semantics.

func (*Client) UseEventBus added in v0.9.0

func (c *Client) UseEventBus(bus EventBus)

UseEventBus wires an EventBus to the Client's CRUD pipeline. After this call, every Create / Update / Delete publishes a lifecycle Event once the write is durable: post-commit via Tx.OnCommit when the operation runs inside an explicit transaction, or inline after the statement for non-transactional CRUD.

Delivery is synchronous and at-least-once with no outbox (ADR-0013). A Publish failure never rolls back the committed write. In the non-transactional path the failure surfaces to the CRUD caller wrapped in ErrEventEmitFailed; in the transactional path it is logged with the event `quark.event.emit_failure` (the commit has already returned success, so there is nothing to propagate to).

Passing nil disables emission. UseEventBus is intended to be called once at setup, before queries run.

func (*Client) Validate

func (c *Client) Validate(ctx context.Context, model any) error

Validate runs the model's own Validator, when it implements one, and then checks its fields against the standard validation tags (e.g. validate:"required"). Create, CreateBatch, Upsert and UpsertBatch call it on each entity before they write; Update does not.

func (*Client) WithOptions added in v0.3.0

func (c *Client) WithOptions(opts ...any) (*Client, error)

WithOptions creates a new client with the same database connection but different options. This is useful for tests that need to create clients with different configurations.

A client built with NewWithDB derives the new client over the SAME shared *sql.DB (there is no DSN to reopen from); a client built with New re-opens its own pool from the stored DSN, as before.

type ClientProvider

type ClientProvider interface {
	GetClient(ctx context.Context) (*Client, error)
}

ClientProvider is an interface that provides a database client. Both *Client and *TenantRouter implement this.

type Column added in v0.6.0

type Column = quarkdriver.Column

Column is one column in a table.

type ColumnTypeMapper added in v1.0.0

type ColumnTypeMapper = quarkdriver.ColumnTypeMapper

ColumnTypeMapper is the optional Dialect interface for translating a neutral/foreign column-type string (the "TEXT" of a hand-built Plan) into the dialect's native form before it reaches DDL.

type Cursor

type Cursor[T any] struct {
	// contains filtered or unexported fields
}

Cursor provides manual iteration over query results. Similar to sql.Rows but type-safe for model T.

func (*Cursor[T]) Close

func (c *Cursor[T]) Close() error

Close releases resources and notifies observers.

F5-4: when the underlying rows close cleanly (no rows.Err) and the model implements AfterFindHook, the hook fires here exactly once. A row-level error short-circuits AfterFind because the read effectively did not "succeed". Errors from AfterFind replace the would-be-returned err, mirroring Query.Iter.

func (*Cursor[T]) Err

func (c *Cursor[T]) Err() error

Err returns any error encountered during iteration.

func (*Cursor[T]) Next

func (c *Cursor[T]) Next() bool

Next advances to the next row. Returns false when done or on error.

func (*Cursor[T]) Scan

func (c *Cursor[T]) Scan(dest *T) error

Scan copies the current row into the destination struct.

type DBConn added in v0.6.0

type DBConn = quarkdriver.DBConn

DBConn is the per-connection subset the lock implementations consume.

type DBConnector added in v0.6.0

type DBConnector = quarkdriver.DBConnector

DBConnector is the narrow subset of *sql.DB the lock implementations need.

type Dialect

type Dialect = quarkdriver.Dialect

Dialect is the interface a database dialect implements: placeholder syntax, identifier quoting, pagination, upsert, locking and DDL. Each supported database (PostgreSQL, MySQL, SQLite, etc.) implements it.

It is declared in quarkdriver, the leaf package a driver module imports (ADR-0026), where its methods are documented; this name is an alias of the same type. A dialect written against quark.Dialect is a quarkdriver.Dialect and the reverse, so an application keeps writing quark.WithDialect(quark.PostgreSQL()) and a driver module implements the interface without importing package quark.

func DetectDialect

func DetectDialect(driverName string) (Dialect, error)

DetectDialect attempts to auto-detect the dialect from a driver name.

func DetectDialectByName

func DetectDialectByName(name string) (Dialect, error)

DetectDialectByName attempts to get a dialect by name from all registered dialects including custom ones. This is useful when you know the exact dialect name.

func MSSQL

func MSSQL() Dialect

MSSQL returns the Microsoft SQL Server dialect instance.

func MariaDB

func MariaDB() Dialect

MariaDB returns a MariaDB dialect instance.

func MySQL

func MySQL() Dialect

MySQL returns the MySQL dialect instance.

func Oracle

func Oracle() Dialect

Oracle returns the Oracle Database dialect instance.

func PostgreSQL

func PostgreSQL() Dialect

PostgreSQL returns the PostgreSQL dialect instance.

func SQLite

func SQLite() Dialect

SQLite returns the SQLite dialect instance.

type Event added in v0.9.0

type Event interface {
	Kind() string
	Table() string
	Payload() any
}

Event is a single CRUD lifecycle event published to an EventBus after the originating write is durable. Kind is one of "created", "updated", or "deleted"; Table is the affected table name; Payload is the model value involved in the operation (the inserted / updated / deleted struct pointer).

Implementations are free to type-switch on Payload to recover the concrete model. Quark emits a value that satisfies this interface; see Client.UseEventBus.

type EventBus

type EventBus interface {
	Publish(ctx context.Context, event Event) error
}

EventBus receives CRUD lifecycle events. Implement it to route events to a logger, OpenTelemetry, or an external broker (NATS / Kafka / Redis Streams). Wire an implementation with Client.UseEventBus.

Delivery semantics are **synchronous, at-least-once, no outbox**: the event is published after the write commits (post-commit via Tx.OnCommit under an explicit transaction, or inline after the statement for non-transactional CRUD). If Publish returns an error the data is already persisted — the failure does not roll anything back. See ErrEventEmitFailed and ADR-0013 for the rationale and the explicit decision NOT to ship a transactional outbox in v0.9.

type EventListener

type EventListener = quarkdriver.Listener

EventListener defines an interface for listening to database events.

It is an alias for quarkdriver.Listener, defined there so that the PostgreSQL implementation can live in the driver module that has the pgx connection it needs. Callers see no difference.

type EventPayload

type EventPayload = quarkdriver.EventPayload

EventPayload represents a message received from a database event channel.

It is an alias: the type is defined in quarkdriver, because a listener module has to construct it without compiling the ORM (ADR-0026 shape). Callers see no difference.

type ExecFunc

type ExecFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (sql.Result, error)

ExecFunc is the signature for SQL execution functions used by middleware.

type Executor

type Executor = quarkdriver.Executor

Executor is the common interface for *sql.DB and *sql.Tx. It allows Query[T] to execute against either a raw connection or a transaction.

It is declared in quarkdriver, the dialect contract's leaf package (ADR-0026), because a dialect's quarkdriver.ColumnAlterer reads the catalog through it; this name is an alias of the same type.

type Expr added in v0.4.0

type Expr interface {
	ToSQL(d Dialect, g *SQLGuard) (sql string, args []any, err error)
}

Expr is the composable expression node Phase 2's query builder rests on. Each node implements ToSQL, returning a SQL fragment with `?` as a neutral bind marker plus the args that fill those markers.

Callers (WhereExpr, HavingExpr) hand the rendered fragment to the existing buildWhereClause / substitutePathMarkers pipeline that already rewrites `?` to the dialect's placeholder syntax at the correct arg index. That keeps the AST dialect-agnostic at construction time and lets us compose deep trees without juggling indices.

The exported constructors below (Col, Lit, And, Or, Not, Cmp, Eq, Ne, Lt, Gt, Lte, Gte, In, NotIn, Func) are the v0.4 AST surface. Subqueries, Cast, and Exists arrive in the subqueries-and-CTE PR.

func Add added in v1.14.0

func Add(lhs, rhs Expr) Expr

Add, Subtract, Multiply and Divide render binary arithmetic, parenthesised so composition does not depend on the reader (or the engine) agreeing about precedence.

The motivating use is an UPDATE that reads the column it writes:

q.Where("id", "=", id).UpdateMap(map[string]any{
    "stock": quark.Subtract(quark.Col("stock"), quark.Lit(1)),
})

which is not the same as loading the row, subtracting in Go and writing it back: that version loses the atomicity, and two concurrent decrements can leave one of them unapplied.

The operator is a constant in each constructor, so nothing a caller types reaches the SQL surface.

func And added in v0.4.0

func And(parts ...Expr) Expr

And composes two or more expressions with logical AND. Empty And is a no-op (renders to ""). Single-element And renders the inner expression without parentheses; two or more get wrapped so precedence is explicit.

func Cmp added in v0.4.0

func Cmp(lhs Expr, op string, rhs Expr) Expr

Cmp is the general comparison constructor. Operator goes through SQLGuard.ValidateOperator so the AST cannot smuggle arbitrary tokens into the SQL surface.

func Col added in v0.4.0

func Col(name string) Expr

Col references a column by name. The name is validated through SQLGuard.ValidateQualifiedIdentifier — the AST inherits the same identifier safety the rest of the builder enforces, and additionally accepts a one-level qualified form ("orders.total") with each segment validated and quoted separately, so a JOIN query can disambiguate shared column names through the AST (AQ-01). The wildcard "*" is accepted as-is for use inside aggregate calls (e.g. Func("COUNT", Col("*"))).

func Contains added in v1.15.0

func Contains(lhs Expr, text string) Expr

Contains is the AST form of Query.WhereContains.

func CountDistinct added in v1.14.0

func CountDistinct(e Expr) Expr

CountDistinct renders `COUNT(DISTINCT <expr>)`.

Oracle rejects a bare aggregate in the SELECT list of a query that does not group (ORA-00937, "not a single-group group function"), so pair it with GroupBy when the query has to run there.

func CumeDist added in v1.14.0

func CumeDist() Expr

CumeDist renders `CUME_DIST()`: the cumulative distribution — the fraction of partition rows at or before this one.

func DenseRank added in v0.4.0

func DenseRank() Expr

DenseRank renders `DENSE_RANK()`.

func Divide added in v1.14.0

func Divide(lhs, rhs Expr) Expr

Divide renders `(<lhs> / <rhs>)`. Division by zero is the engine's to report: engines disagree (an error on PostgreSQL, NULL on SQLite) and papering over that would hide a real difference.

func EndsWith added in v1.15.0

func EndsWith(lhs Expr, text string) Expr

EndsWith is the AST form of Query.WhereEndsWith.

func Eq added in v0.4.0

func Eq(lhs, rhs Expr) Expr

Eq, Ne, Lt, Gt, Lte, Gte are the syntactic shortcuts for Cmp with the most common operators. Built on top of Lit / Col for the typical "Col(x) = Lit(v)" shape.

func Exists added in v0.4.0

func Exists(s *Subquery) Expr

Exists renders `EXISTS (<subquery>)`. Typically used as a top-level WhereExpr predicate.

Example:

hasOrders, _ := quark.For[Order](ctx, client).
    Where("user_id", "=", quark.Col("users.id")).
    Select("1").
    AsSubquery()
q := quark.For[User](ctx, client).WhereExpr(quark.Exists(hasOrders))

(Correlated subqueries require the inner query to reference the outer table by qualified name; see the JoinOn grammar for what's accepted.)

func FirstValue added in v1.14.0

func FirstValue(col Expr) Expr

FirstValue renders `FIRST_VALUE(<col>)`, the column's value in the first row of the window frame.

func Func added in v0.4.0

func Func(name string, args ...Expr) Expr

Func calls a SQL function. The name is normalised to upper-case and matched against a whitelist; unknown names return ErrInvalidQuery rather than reaching the SQL surface. Empty argument list is allowed — emit a bare "FUN()" — for COUNT(*), use Col("*") explicitly.

func Gt added in v0.4.0

func Gt(lhs, rhs Expr) Expr

func Gte added in v0.4.0

func Gte(lhs, rhs Expr) Expr

func In added in v0.4.0

func In(lhs Expr, values ...Expr) Expr

In renders "lhs IN (v1, v2, …)". Empty values list is a logic error and returns ErrInvalidQuery — `WHERE x IN ()` is non-portable across dialects (Postgres errors, MySQL silently matches nothing) so we refuse to emit it. Use a no-rows query instead.

func InSub added in v0.4.0

func InSub(lhs Expr, s *Subquery) Expr

InSub renders `lhs IN (<subquery>)`. Useful for the common `WHERE x IN (SELECT id FROM ...)` shape; the inner Subquery should `Select("…")` exactly one column for the comparison.

func JSONExtract added in v1.14.0

func JSONExtract(column, path string) Expr

JSONExtract projects a member of a JSON column, so a JSON value can reach the SELECT list the same way WhereJSON already reaches the WHERE clause. The dialect renders it — `jsonb_extract_path_text` on PostgreSQL, `JSON_EXTRACT` on MySQL and SQLite, `JSON_VALUE` on SQL Server and Oracle — and validates the path, which is a dotted identifier chain ("user.name"), not JSONPath ("$.user.name").

func Lag added in v0.4.0

func Lag(col Expr, offset int) Expr

Lag renders `LAG(<col>, <offset>)`. The offset is bound as a parameter so the path is uniform with the rest of the AST — no SQL-surface integers, no per-dialect numeric formatting concerns.

func LastValue added in v1.14.0

func LastValue(col Expr) Expr

LastValue renders `LAST_VALUE(<col>)`. Read the frame note on Window before reaching for it: with the default frame the "last" row is the current one, which is rarely what the caller means.

func Lead added in v0.4.0

func Lead(col Expr, offset int) Expr

Lead renders `LEAD(<col>, <offset>)`.

func Like added in v1.15.0

func Like(lhs Expr, pattern string) Expr

Like is the AST form of Query.WhereLike: `lhs LIKE pattern` with the escape character declared, so the pattern means the same on every engine. The plain, engine-default form remains Cmp(lhs, "LIKE", Lit(pattern)).

func Lit added in v0.4.0

func Lit(v any) Expr

Lit binds a Go value as a SQL parameter. The value never reaches the SQL surface — it always travels through args, regardless of how nested the expression tree is.

func Lt added in v0.4.0

func Lt(lhs, rhs Expr) Expr

func Lte added in v0.4.0

func Lte(lhs, rhs Expr) Expr

func Multiply added in v1.14.0

func Multiply(lhs, rhs Expr) Expr

Multiply renders `(<lhs> * <rhs>)`.

func NTile added in v1.14.0

func NTile(buckets int) Expr

NTile renders `NTILE(<buckets>)`: the bucket a row falls in when the partition is split into n roughly equal groups. The bucket count is bound as a parameter, same as Lag/Lead's offset.

func Ne added in v0.4.0

func Ne(lhs, rhs Expr) Expr

func Not added in v0.4.0

func Not(e Expr) Expr

Not negates an expression. Renders as "NOT (<inner>)" so precedence is explicit.

func NotExists added in v0.4.0

func NotExists(s *Subquery) Expr

NotExists is the negated form: `NOT EXISTS (<subquery>)`.

func NotIn added in v0.4.0

func NotIn(lhs Expr, values ...Expr) Expr

NotIn is the negation. Same emptiness rules apply.

func NotInSub added in v0.4.0

func NotInSub(lhs Expr, s *Subquery) Expr

NotInSub is the negated form: `lhs NOT IN (<subquery>)`.

func NotLike added in v1.15.0

func NotLike(lhs Expr, pattern string) Expr

NotLike is the negation of Like.

func NthValue added in v1.14.0

func NthValue(col Expr, n int) Expr

NthValue renders `NTH_VALUE(<col>, <n>)`, 1-based.

Five of the six engines have it. SQL Server does NOT: it fails with "'NTH_VALUE' is not a recognized built-in function name". Quark does not emulate it there — an emulation would have different NULL and frame behaviour — so a query using it is not portable to SQL Server. Reach for Lag/Lead or a ranked subquery if you need that engine.

func Or added in v0.4.0

func Or(parts ...Expr) Expr

Or composes two or more expressions with logical OR. Same parenthesis rules as And.

func Over added in v0.4.0

func Over(inner Expr, w *Window) Expr

Over wraps an inner Expr with a Window: `<inner> OVER (<window>)`. Typical use is wrapping a window-function leaf (RowNumber, Rank, DenseRank, Lag, Lead) but any aggregate function from the AST whitelist (`COUNT`, `SUM`, etc.) is also valid as the inner — the SQL spec defines them all as windowable.

func PercentRank added in v1.14.0

func PercentRank() Expr

PercentRank renders `PERCENT_RANK()`: the row's rank as a fraction of the partition, from 0 to 1.

func Rank added in v0.4.0

func Rank() Expr

Rank renders `RANK()`.

func RowNumber added in v0.4.0

func RowNumber() Expr

RowNumber renders `ROW_NUMBER()`. Meaningless outside Over().

func StartsWith added in v1.15.0

func StartsWith(lhs Expr, text string) Expr

StartsWith is the AST form of Query.WhereStartsWith.

func Sub added in v0.4.0

func Sub(s *Subquery) Expr

Sub wraps a Subquery as an Expr leaf so it can take the place of a Lit or Col anywhere an Expr is accepted (e.g. `Eq(Col("user_id"), Sub(maxID))`, `Cmp(Col("price"), ">", Sub(avgPrice))`). Renders as `(<inner-sql>)`.

func Subtract added in v1.14.0

func Subtract(lhs, rhs Expr) Expr

Subtract renders `(<lhs> - <rhs>)`.

type FieldMeta

type FieldMeta = schema.FieldMeta

FieldMeta is the metadata for a single struct field.

type ForeignKey added in v0.6.0

type ForeignKey = quarkdriver.ForeignKey

ForeignKey is one FOREIGN KEY constraint declared on a table.

type FrameBound added in v1.14.0

type FrameBound string

FrameBound is one edge of a window frame. The constructors below are the only way to make one, so no caller string reaches the frame clause.

func CurrentRow added in v1.14.0

func CurrentRow() FrameBound

CurrentRow is the row being computed.

func Following added in v1.14.0

func Following(n int) FrameBound

Following is n rows or units forward.

func Preceding added in v1.14.0

func Preceding(n int) FrameBound

Preceding is n rows (ROWS) or n units of the order value (RANGE) back.

func UnboundedFollowing added in v1.14.0

func UnboundedFollowing() FrameBound

UnboundedFollowing is the end of the partition.

func UnboundedPreceding added in v1.14.0

func UnboundedPreceding() FrameBound

UnboundedPreceding is the start of the partition.

type GeneratedMeta added in v0.11.0

type GeneratedMeta struct {
	// ContractVersion is the GenContractVersion the file was generated for.
	ContractVersion int
	// ModelHash is ModelHash(t) computed at generation time.
	ModelHash string
}

GeneratedMeta carries the bookkeeping a generated file registers alongside its scanner/binder: the contract version it was emitted against and a hash of the model's shape at generation time. The hash lets an optional drift check (CheckGeneratedDrift) warn when a model has changed but `quark gen` was not re-run.

type Index added in v0.6.0

type Index = quarkdriver.Index

Index is one secondary (non-primary-key) index on a table.

type JSON added in v0.3.0

type JSON[T any] struct {
	V T
}

JSON[T] wraps a Go value so it round-trips through a SQL JSON column via encoding/json. Use it when you want a typed view of a JSON-shaped column without writing scan / value code by hand.

JSON implements database/sql.Scanner and database/sql/driver.Valuer, so every driver Quark supports handles the round-trip with no extra reflect in Quark's hot paths. The migrate layer detects JSON[T] and emits the dialect-native JSON column type:

Postgres → JSONB
MySQL / MariaDB → JSON
SQLite → TEXT (with json_* functions available at query time)
SQL Server → NVARCHAR(MAX)
Oracle → CLOB

Example:

type Settings struct{ Theme string `json:"theme"`; Volume int `json:"volume"` }

type Profile struct {
    ID       int64                  `db:"id" pk:"true"`
    Settings quark.JSON[Settings]   `db:"settings"`
}

p := Profile{Settings: quark.JSON[Settings]{V: Settings{Theme: "dark", Volume: 7}}}
_ = client.Migrate(ctx, &Profile{})
_ = quark.For[Profile](ctx, client).Create(&p)

func (*JSON[T]) Scan added in v0.3.0

func (j *JSON[T]) Scan(src any) error

Scan implements sql.Scanner. Accepts []byte and string sources (the two forms drivers return for JSON columns). NULL clears V to its zero value rather than erroring; pair with quark.Nullable[JSON[T]] when you want to distinguish NULL from "valid but empty" payloads.

func (JSON[T]) Value added in v0.3.0

func (j JSON[T]) Value() (driver.Value, error)

Value implements driver.Valuer by JSON-marshalling V into the column.

The marshalled JSON is returned as a string, not []byte: go-mssqldb binds a []byte parameter as VARBINARY, and storing that into the NVARCHAR(MAX) JSON column triggers an implicit VARBINARY→NVARCHAR conversion that reinterprets the UTF-8 bytes as UTF-16 and corrupts the payload. Returning a string binds as NVARCHAR (and as the equivalent text type on every other driver), so the round-trip is clean across all dialects.

type JoinBuilder added in v0.4.0

type JoinBuilder[T any] struct {
	// contains filtered or unexported fields
}

JoinBuilder is the structured form returned by Join, LeftJoin, and RightJoin. Complete the JOIN by chaining `.On(left, op, right)` (the typed identifier form) or `.OnRaw(onClause)` (the legacy free-form string for ON clauses outside the simple binary grammar). Both chain-terminate by returning *Query[T] so subsequent builder calls pick up where the JOIN left off.

JoinBuilder values are immutable; the underlying *Query[T] is cloned before the JOIN is appended, matching the rest of the builder's thread-safety contract.

func (*JoinBuilder[T]) On added in v0.4.0

func (b *JoinBuilder[T]) On(left, op, right string) *Query[T]

func (*JoinBuilder[T]) OnExpr added in v1.14.0

func (b *JoinBuilder[T]) OnExpr(e Expr) *Query[T]

On appends an INNER/LEFT/RIGHT JOIN with a single binary identifier comparison as the ON clause: `<left> <op> <right>`. The three arguments are concatenated as `left + " " + op + " " + right` and the resulting clause is validated as a whole against `guard.ValidateJoinOn` at exec time, surfacing `ErrInvalidJoin` for any shape outside the identifier-only grammar (literal RHS, function calls, parens, comments, mismatched operators). The grammar accepts the binary comparison operators `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=` and AND-chained compound clauses.

Most JOINs need only this form — the typical `users.id = orders.user_id` shape. For multi-condition ON clauses or any expression the ValidateJoinOn grammar accepts (AND-chained identifier comparisons), use `OnRaw`.

Example:

quark.For[Order](ctx, client).
    Join("users").On("users.id", "=", "orders.user_id").
    List()

OnExpr closes the JOIN with a condition built from the expression AST, which is what makes a literal in the ON clause possible:

q.LeftJoin("order_items").OnExpr(quark.And(
    quark.Eq(quark.Col("orders.id"), quark.Col("order_items.order_id")),
    quark.Gt(quark.Col("order_items.qty"), quark.Lit(1)),
))

On and OnRaw take caller strings, so they are held to an identifier-only grammar and reject literals — the ON clause is one of the few places where a string goes into SQL without a bind marker. An AST clause is not a string: its literals bind as parameters, so the restriction does not apply.

The difference is not cosmetic for an outer join. Moving the same predicate to WHERE turns a LEFT JOIN into an inner one, because the unmatched rows it was meant to preserve arrive with NULLs and the WHERE drops them.

func (*JoinBuilder[T]) OnRaw added in v0.4.0

func (b *JoinBuilder[T]) OnRaw(onClause string) *Query[T]

OnRaw appends the JOIN with a free-form ON clause string. The clause must match the minimal identifier-only grammar that `guard.ValidateJoinOn` enforces (AND-chained binary comparisons of qualified identifiers, e.g. `users.id = orders.user_id AND users.tenant_id = orders.tenant_id`). Literals, function calls, subqueries, and parentheses are rejected. Drop down to `RawQuery` for shapes outside this grammar.

OnRaw is the migration path for callers of the v0.3.x string-raw `Join(table, onClause)`: rewrite as `Join(table).OnRaw(onClause)`.

type KeysetPage added in v1.15.0

type KeysetPage[T any] struct {
	Items   []T
	Next    string
	HasMore bool
}

KeysetPage is one page of a keyset (seek) pagination: the rows, and the opaque token that names where they stopped. Hand Next back to Query.PaginateAfter to get the rows after them; HasMore says whether there are any. Unlike Page, it carries no total and no page number: a keyset page costs one statement and the same time whether it is the first or the thousandth, because the server seeks to the last row read instead of walking past every page before it (A8 S9).

type Limits

type Limits struct {
	MaxQueryLength     int
	MaxResults         int
	MaxJoins           int
	MaxWhereConditions int
	QueryTimeout       time.Duration
	AllowRawQueries    bool
	SafeMigrations     bool
}

Limits defines security and performance limits for queries.

For the numeric fields, zero means "not set": WithLimits fills any zero-valued numeric field from DefaultLimits, so a partial literal such as Limits{MaxResults: 500} raises one limit without silently zeroing the rest. A negative value is passed through untouched — that is the explicit form for "no cap" where a consumer honours it (e.g. MaxQueryLength < 0 disables the generated-SQL length check).

The boolean fields (AllowRawQueries, SafeMigrations) are NOT normalized: an explicit false is indistinguishable from the zero value, so a partial literal carries AllowRawQueries=false and SafeMigrations=false regardless of the defaults. To change one field while keeping every default — including SafeMigrations=true — start from DefaultLimits and modify it. When a partial literal leaves SafeMigrations=false, New logs one structured WARN pointing here — see WithLimits.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns sensible default limits.

type ListenerFactory added in v0.9.0

type ListenerFactory struct {
	// contains filtered or unexported fields
}

ListenerFactory is a dialect-agnostic factory for creating [EventListener]s over a database PubSub channel (PostgreSQL LISTEN/NOTIFY). It is unrelated to the EventBus CRUD-event interface above — this is the *inbound* channel-listener side.

Renamed from the v0.8.0 `EventBus` struct in v0.9.0 to free the `EventBus` name for the CRUD-event interface. The PostgreSQL listener is implemented over a dedicated pool connection (ADR-0019); other dialects return ErrDialectNotSupported.

func NewListenerFactory added in v0.9.0

func NewListenerFactory(client *Client) *ListenerFactory

NewListenerFactory creates a ListenerFactory for the given client.

Renamed from `NewEventBus` in v0.9.0; see ListenerFactory.

func (*ListenerFactory) CreateListener added in v0.9.0

func (f *ListenerFactory) CreateListener() (EventListener, error)

CreateListener returns an EventListener for the client's dialect.

PostgreSQL is supported: the listener pins a dedicated connection from the pool (acquired lazily on the first Listen) and consumes notifications via pgx's WaitForNotification (ADR-0019). It is single-goroutine — see EventListener and pgListener. Every other dialect returns ErrDialectNotSupported: LISTEN/NOTIFY has no portable equivalent and Quark does not emulate it with polling.

type LockMode added in v0.4.0

type LockMode = quarkdriver.LockMode

LockMode is the kind of pessimistic lock requested for a SELECT. It is declared in quarkdriver with the dialect contract whose LockSuffix takes it (ADR-0026); this name is an alias of the same type.

type LockOptions added in v0.4.0

type LockOptions = quarkdriver.LockOptions

LockOptions describes the pessimistic-lock behaviour for a SELECT. The zero value (LockMode == LockNone) emits nothing — callers opt in via ForUpdate / ForShare on Query[T]. It is declared in quarkdriver, where Dialect.LockSuffix names it (ADR-0026); this name is an alias of the same type, IsZero method included.

type LoggerEventBus added in v0.9.0

type LoggerEventBus struct {
	// contains filtered or unexported fields
}

LoggerEventBus is an in-tree EventBus that writes each event to a slog.Logger at Info level. Useful for development, smoke tests, and as a reference implementation for custom buses.

func NewLoggerEventBus added in v0.9.0

func NewLoggerEventBus(logger *slog.Logger) *LoggerEventBus

NewLoggerEventBus returns a LoggerEventBus writing to logger. A nil logger falls back to slog.Default.

func (*LoggerEventBus) Publish added in v0.9.0

func (b *LoggerEventBus) Publish(ctx context.Context, event Event) error

Publish logs the event. Never returns an error — logging is best-effort and a logger sink cannot meaningfully fail.

type MSSQLDialect

type MSSQLDialect struct {
	// contains filtered or unexported fields
}

MSSQLDialect implements the Microsoft SQL Server dialect.

func (*MSSQLDialect) AcquireMigrationLock added in v0.6.0

func (d *MSSQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)

AcquireMigrationLock uses `sp_getapplock` with @LockOwner='Session', scoped to the dedicated connection's session. Returns an integer status: 0 (granted immediately), 1 (granted after wait), -1 (timeout), -2 (cancel), -3 (deadlock), -999 (parameter / fatal error). We map -1 to `ErrLockTimeout` and the others to descriptive errors.

Timeout is in milliseconds; we round the Go Duration to ms.

func (*MSSQLDialect) AlterColumn added in v1.16.0

AlterColumn restates the type and the nullability with ALTER COLUMN, and treats a default as the named constraint it is on SQL Server: the old one is dropped by the name sys.default_constraints gives it, the new one is named DF_<table>_<column>. The primary key is a constraint named pk_<table> when added; the one to drop is found in sys.key_constraints.

func (*MSSQLDialect) AlterTableAddColumn

func (m *MSSQLDialect) AlterTableAddColumn(table, column, dataType string) string

func (*MSSQLDialect) AlterTableAlterColumn

func (m *MSSQLDialect) AlterTableAlterColumn(table, column, newDataType string) string

func (*MSSQLDialect) AlterTableDropColumn

func (m *MSSQLDialect) AlterTableDropColumn(table, column string) string

func (*MSSQLDialect) AutoIncrementColumn added in v1.16.0

func (*MSSQLDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is BIGINT IDENTITY(1,1) PRIMARY KEY.

func (*MSSQLDialect) BoolLiteral added in v1.16.0

func (*MSSQLDialect) BoolLiteral(v bool) string

BoolLiteral is 1 or 0: SQL Server's BIT rejects TRUE and FALSE.

func (*MSSQLDialect) BuildProcedureCall

func (m *MSSQLDialect) BuildProcedureCall(procedure string, argCount int) string

func (*MSSQLDialect) BuildRoutineQuery

func (m *MSSQLDialect) BuildRoutineQuery(routine string, argCount int) string

func (*MSSQLDialect) ColumnType added in v1.16.0

func (*MSSQLDialect) ColumnType(s quarkdriver.ColumnSpec) string

ColumnType is SQL Server's type for a column of the given kind.

func (*MSSQLDialect) CreateIndexIfNotExists added in v1.16.0

func (m *MSSQLDialect) CreateIndexIfNotExists(table, index string, columns []string, unique bool) string

CreateIndexIfNotExists guards a plain CREATE INDEX with a lookup in sys.indexes.

func (*MSSQLDialect) CreateTableIfNotExists added in v1.16.0

func (m *MSSQLDialect) CreateTableIfNotExists(table, body string) string

CreateTableIfNotExists guards a plain CREATE TABLE with a lookup in sys.tables: SQL Server has no CREATE TABLE IF NOT EXISTS.

func (*MSSQLDialect) CurrentTimestamp

func (m *MSSQLDialect) CurrentTimestamp() string

func (*MSSQLDialect) DropCheck added in v1.16.0

func (m *MSSQLDialect) DropCheck(table, constraint string) string

DropCheck is the standard ALTER TABLE … DROP CONSTRAINT.

func (*MSSQLDialect) DropForeignKey added in v1.16.0

func (m *MSSQLDialect) DropForeignKey(table, constraint string) string

DropForeignKey is the standard ALTER TABLE … DROP CONSTRAINT.

func (*MSSQLDialect) DropIndex added in v1.16.0

func (m *MSSQLDialect) DropIndex(table, index string) string

DropIndex names the table: DROP INDEX … ON ….

func (*MSSQLDialect) IntrospectSchema added in v0.6.0

func (d *MSSQLDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

IntrospectSchema reads the MSSQL schema via `sys.tables`, `sys.columns`, `sys.types`, and `sys.default_constraints`. MSSQL stores defaults in a separate catalog joined on parent object_id + column_id, so default-extraction needs a LEFT JOIN; everything else is a straight catalog read.

MSSQL caveats handled here:

  • System-shipped tables are filtered via `is_ms_shipped = 0` plus `name NOT LIKE 'quark[_]%' ESCAPE`-style char class (the `[_]` bracket prevents `_` from being interpreted as the wildcard).
  • Type reassembly: `sys.types` returns the bare type name (`varchar`, `decimal`); we glue `(N)` / `(P,S)` / `(MAX)` onto it from the adjacent columns. `max_length = -1` is the MSSQL convention for VARCHAR(MAX) / NVARCHAR(MAX). For nvarchar, `max_length` is bytes (chars * 2), so we divide by 2 when emitting the displayed type — matches what a user would write in DDL.
  • Default values: MSSQL wraps them in parens (`(0)`, `(getdate())`, `('draft')`). We pass that through raw — the diff layer (F3-3) is responsible for unwrapping if it needs to compare against the Go-side DDL.

func (*MSSQLDialect) IsAlreadyExists added in v1.16.0

func (m *MSSQLDialect) IsAlreadyExists(quarkdriver.SchemaObject, error) bool

IsAlreadyExists recognises nothing: the guards make the statements conditional.

func (*MSSQLDialect) JSONExtract

func (m *MSSQLDialect) JSONExtract(column, path string) (string, []any, error)

func (*MSSQLDialect) LastInsertIDQuery

func (m *MSSQLDialect) LastInsertIDQuery(table, pkColumn string) string

func (*MSSQLDialect) LimitOffset

func (m *MSSQLDialect) LimitOffset(limit, offset int) string

func (*MSSQLDialect) LockSuffix added in v0.4.0

func (m *MSSQLDialect) LockSuffix(opts LockOptions) (string, string, error)

MSSQL uses table hints attached to the FROM clause (not a SELECT suffix). Single-row pessimistic-style locks come from (UPDLOCK, ROWLOCK) for ForUpdate and (HOLDLOCK, ROWLOCK) for ForShare. SkipLocked maps to READPAST; NoWait has no direct hint, so it errors out rather than silently blocking. A shared lock that skips locked rows has no form either: HOLDLOCK is SERIALIZABLE, and SQL Server takes READPAST only under READ COMMITTED or REPEATABLE READ (error 650, found by the dialect kit's LockSuffix check — drivertest.VerifyDialect, A11 Q4).

func (*MSSQLDialect) Name

func (d *MSSQLDialect) Name() string

func (*MSSQLDialect) Placeholder

func (m *MSSQLDialect) Placeholder(index int) string

Placeholder returns "@pindex", without fmt for the reason given on PostgresDialect.Placeholder.

func (*MSSQLDialect) Placeholders

func (m *MSSQLDialect) Placeholders(n int) []string

func (*MSSQLDialect) Quote

func (m *MSSQLDialect) Quote(identifier string) string

func (*MSSQLDialect) ReferentialAction added in v1.17.0

func (m *MSSQLDialect) ReferentialAction(_ quarkdriver.ReferentialEvent, action string) quarkdriver.ActionSupport

ReferentialAction: SQL Server takes NO ACTION, CASCADE, SET NULL and SET DEFAULT in both clauses, and has no RESTRICT — a syntax error (Msg 156). A cycle or a second cascade path to one table is refused by the engine itself (Msg 1785), which no answer about one action can foresee.

func (*MSSQLDialect) ReleaseSavepointStmt added in v1.1.0

func (m *MSSQLDialect) ReleaseSavepointStmt(name string) string

func (*MSSQLDialect) RenameColumn

func (m *MSSQLDialect) RenameColumn(table, oldName, newName string) string

func (*MSSQLDialect) RenameTable

func (m *MSSQLDialect) RenameTable(oldName, newName string) string

func (*MSSQLDialect) Returning

func (m *MSSQLDialect) Returning(columns ...string) string

func (*MSSQLDialect) RollbackToSavepointStmt added in v1.1.0

func (m *MSSQLDialect) RollbackToSavepointStmt(name string) string

func (*MSSQLDialect) SavepointStmt added in v1.1.0

func (m *MSSQLDialect) SavepointStmt(name string) string

func (*MSSQLDialect) SupportsLastInsertID

func (m *MSSQLDialect) SupportsLastInsertID() bool

func (*MSSQLDialect) SupportsReturning

func (m *MSSQLDialect) SupportsReturning() bool

func (*MSSQLDialect) SupportsTransactionalDDL

func (m *MSSQLDialect) SupportsTransactionalDDL() bool

func (*MSSQLDialect) UpsertSQL

func (m *MSSQLDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string

UpsertSQL for MSSQL: uses MERGE statement appended as a WITH-style hint. MSSQL requires MERGE syntax which cannot be appended to a plain INSERT, so we return a marker that buildUpsert handles specially.

type MariaDBDialect

type MariaDBDialect struct {
	MySQLDialect // embed MySQL — identical wire protocol and driver
}

MariaDBDialect implements the MariaDB dialect. MariaDB is a fork of MySQL with significant additions:

  • RETURNING clause in INSERT/DELETE/UPDATE (10.5+)
  • Native sequences via CREATE SEQUENCE (10.3+)
  • Temporal tables / system-versioned tables (10.3.4+)
  • JSON_TABLE support (10.6+)
  • INTERSECT / EXCEPT set operations (10.3+)
  • Descending indexes (10.6+)
  • UUID() and UUID_SHORT() built-ins
  • IGNORE INDEX / USE INDEX hints identical to MySQL

func (*MariaDBDialect) AcquireMigrationLock added in v0.6.0

func (d *MariaDBDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)

func (*MariaDBDialect) AlterTableAlterColumn

func (m *MariaDBDialect) AlterTableAlterColumn(table, column, newDataType string) string

AlterTableAlterColumn uses MODIFY COLUMN (same as MySQL).

func (*MariaDBDialect) AutoIncrementColumn added in v1.16.0

func (*MariaDBDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is BIGINT AUTO_INCREMENT PRIMARY KEY.

func (*MariaDBDialect) BoolLiteral added in v1.16.0

func (*MariaDBDialect) BoolLiteral(v bool) string

BoolLiteral is 1 or 0.

func (*MariaDBDialect) ColumnType added in v1.16.0

ColumnType is MariaDB's type for a column of the given kind.

func (*MariaDBDialect) CreateSequence

func (m *MariaDBDialect) CreateSequence(name string, start, increment int64) string

CreateSequence returns the DDL to create a named sequence (MariaDB 10.3+).

func (*MariaDBDialect) CreateSystemVersionedTable

func (m *MariaDBDialect) CreateSystemVersionedTable(table string, columnDefs string) string

CreateSystemVersionedTable returns the DDL for a system-versioned (temporal) table. Requires MariaDB 10.3.4+.

func (*MariaDBDialect) DropCheck added in v1.17.0

func (m *MariaDBDialect) DropCheck(table, constraint string) string

DropCheck is the standard ALTER TABLE … DROP CONSTRAINT on MariaDB, which has no DROP CHECK: the MySQL spelling it inherited was a syntax error (Error 1064) on MariaDB 11.4, found by the dialect kit's ObjectDropper check (drivertest.VerifyDialect, A11 Q4).

func (*MariaDBDialect) HistoryBetween

func (m *MariaDBDialect) HistoryBetween(table, from, to string) string

HistoryBetween returns SELECT … FOR SYSTEM_TIME BETWEEN for a time range.

func (*MariaDBDialect) HistoryQuery

func (m *MariaDBDialect) HistoryQuery(table string) string

HistoryQuery returns SELECT … FOR SYSTEM_TIME ALL to query full row history.

func (*MariaDBDialect) IntrospectSchema added in v0.6.0

func (d *MariaDBDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

func (*MariaDBDialect) JSONExtract

func (m *MariaDBDialect) JSONExtract(column, path string) (string, []any, error)

JSONExtract uses the MariaDB / MySQL JSON_VALUE syntax (10.2.3+). MariaDB also accepts the arrow operator col->>'$.key' from 10.4.3+.

func (*MariaDBDialect) JSONTable

func (m *MariaDBDialect) JSONTable(source, path string, columns ...string) string

JSONTable returns a JSON_TABLE expression (MariaDB 10.6+). source: SQL expression producing JSON; path: root path e.g. '$[*]'; columns: column definitions e.g. "id INT PATH '$.id'".

The path is validated against guard.ValidateJSONTablePath (JSONPath grammar rooted at "$"). source and columns must be trusted strings — the JSON_TABLE row syntax intermixes column types and PATH literals, so binding it as a parameter is not possible. Callers MUST NOT pass user-controlled values for source or columns. If invalid, the returned SQL embeds an obvious sentinel that fails parsing at execution time, surfacing the misuse rather than silently producing executable injection.

TODO(public-api): when JSONTable graduates from internal-only to a public builder, change the signature to return (string, error) so callers can detect validation failure with errors.Is rather than scanning the SQL for JSON_TABLE_PATH_INVALID.

func (*MariaDBDialect) LastInsertIDQuery

func (m *MariaDBDialect) LastInsertIDQuery(table, pkColumn string) string

LastInsertIDQuery is kept as fallback for engines older than 10.5.

func (*MariaDBDialect) LimitOffset

func (m *MariaDBDialect) LimitOffset(limit, offset int) string

LimitOffset for MariaDB uses standard LIMIT … OFFSET … syntax (unlike MySQL which uses LIMIT offset, count).

func (*MariaDBDialect) LockSuffix added in v0.4.0

func (m *MariaDBDialect) LockSuffix(opts LockOptions) (string, string, error)

MariaDB pessimistic locking. FOR UPDATE [SKIP LOCKED|NOWAIT] works on MariaDB 10.6+ just like MySQL. Shared locks are the one divergence: MariaDB has no FOR SHARE (that is MySQL-8 syntax — it raises Error 1064); it uses the older LOCK IN SHARE MODE, which does not accept SKIP LOCKED or NOWAIT. So emit LOCK IN SHARE MODE for ForShare, and reject the modifiers rather than emitting SQL MariaDB can't parse. (BB-3)

func (*MariaDBDialect) Name

func (d *MariaDBDialect) Name() string

func (*MariaDBDialect) NextVal

func (m *MariaDBDialect) NextVal(sequenceName string) string

NextVal returns the SQL expression that reads the next value from a sequence.

func (*MariaDBDialect) RenameColumn

func (m *MariaDBDialect) RenameColumn(table, oldName, newName string) string

RenameColumn uses the standard SQL syntax supported since MariaDB 10.4.2.

func (*MariaDBDialect) Returning

func (m *MariaDBDialect) Returning(columns ...string) string

Returning generates a RETURNING clause compatible with MariaDB 10.5+.

func (*MariaDBDialect) SupportsLastInsertID

func (m *MariaDBDialect) SupportsLastInsertID() bool

SupportsLastInsertID returns false when RETURNING is used. The ORM prefers RETURNING over LAST_INSERT_ID() for MariaDB.

func (*MariaDBDialect) SupportsReturning

func (m *MariaDBDialect) SupportsReturning() bool

SupportsReturning returns true: MariaDB 10.5+ supports RETURNING in INSERT … RETURNING, DELETE … RETURNING and UPDATE … RETURNING.

func (*MariaDBDialect) SupportsTransactionalDDL

func (m *MariaDBDialect) SupportsTransactionalDDL() bool

SupportsTransactionalDDL returns false — MariaDB (like MySQL) performs implicit commits around DDL statements.

func (*MariaDBDialect) UpsertSQL

func (m *MariaDBDialect) UpsertSQL(conflictCols, updateCols []string, argOffset int) string

UpsertSQL for MariaDB: INSERT … ON DUPLICATE KEY UPDATE (same as MySQL)

type Middleware

type Middleware interface {
	WrapExec(next ExecFunc) ExecFunc
	WrapQuery(next QueryFunc) QueryFunc
	WrapQueryRow(next QueryRowFunc) QueryRowFunc
}

Middleware wraps query execution for cross-cutting concerns like logging, retry logic, caching, rate limiting, etc. Every statement Quark sends to the engine passes through it — the query builder's, schema work, introspection, savepoints and raw SQL alike — by the method it is sent with (Exec, Query, QueryRow); StatementKindFromContext says what the statement is for. Middlewares compose in the order they are registered: the first registered is the outermost.

type MigrationLock added in v0.6.0

type MigrationLock = quarkdriver.MigrationLock

MigrationLock is the handle returned by Client.AcquireMigrationLock; the caller must invoke Release before the *Client is closed.

type MigrationLocker added in v0.6.0

type MigrationLocker = quarkdriver.MigrationLocker

MigrationLocker is the optional interface a Dialect implements to support distributed migration locks. PG / MySQL / MariaDB / MSSQL / Oracle implement it; SQLite does not.

type ModelField added in v0.11.0

type ModelField struct {
	// Name is the Go struct field name.
	Name string
	// Column is the column name from the db tag (sizing options stripped).
	Column string
	// GoType is the field's Go type, run through CanonicalType.
	GoType string
	// IsPK reports whether the field carries `pk:"true"`.
	IsPK bool
}

ModelField is one persisted field of a model, reduced to the attributes that define the model's shape. Both the runtime (from reflection) and the generator (from the AST) build a slice of these and feed it to HashModelFields, so the two derive identical hashes for an unchanged model. Exported so the generator can construct and hash fields with the exact same algorithm as the runtime.

type ModelMeta

type ModelMeta = schema.ModelMeta

ModelMeta is the cached metadata for a model struct.

func GetModelMeta

func GetModelMeta[T any]() *ModelMeta

GetModelMeta returns the cached metadata for model type T.

func GetModelMetaByType

func GetModelMetaByType(t reflect.Type) *ModelMeta

GetModelMetaByType returns the cached metadata for a reflect.Type.

type MySQLDialect

type MySQLDialect struct {
	// contains filtered or unexported fields
}

MySQLDialect implements the MySQL dialect.

func (*MySQLDialect) AcquireMigrationLock added in v0.6.0

func (d *MySQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)

AcquireMigrationLock uses MySQL's `GET_LOCK(name, timeout_seconds)`, which is session-bound (released when the connection ends). Returns 1 on success, 0 on timeout, NULL on error. We dedicate a connection for the lock's lifetime; `Release` calls `RELEASE_LOCK` and returns the connection to the pool.

Timeout argument: GET_LOCK accepts seconds (negative = wait forever); we round Duration to seconds. Sub-second timeouts are rounded UP to 1 second — the next-best approximation of the caller's intent given the protocol granularity.

MariaDB shares MySQL's GET_LOCK semantics — same code path.

func (*MySQLDialect) AlterColumn added in v1.16.0

AlterColumn restates the whole column with MODIFY COLUMN — the type, the nullability and the default travel together or the ones left out are reset — and adds or drops the primary key by role.

func (*MySQLDialect) AlterTableAddColumn

func (m *MySQLDialect) AlterTableAddColumn(table, column, dataType string) string

func (*MySQLDialect) AlterTableAlterColumn

func (m *MySQLDialect) AlterTableAlterColumn(table, column, newDataType string) string

func (*MySQLDialect) AlterTableDropColumn

func (m *MySQLDialect) AlterTableDropColumn(table, column string) string

func (*MySQLDialect) AutoIncrementColumn added in v1.16.0

func (*MySQLDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is BIGINT AUTO_INCREMENT PRIMARY KEY.

func (*MySQLDialect) BoolLiteral added in v1.16.0

func (*MySQLDialect) BoolLiteral(v bool) string

BoolLiteral is 1 or 0.

func (*MySQLDialect) BuildProcedureCall

func (m *MySQLDialect) BuildProcedureCall(procedure string, argCount int) string

func (*MySQLDialect) BuildRoutineQuery

func (m *MySQLDialect) BuildRoutineQuery(routine string, argCount int) string

func (*MySQLDialect) ColumnType added in v1.16.0

func (*MySQLDialect) ColumnType(s quarkdriver.ColumnSpec) string

ColumnType is MySQL's type for a column of the given kind.

func (*MySQLDialect) CreateIndexIfNotExists added in v1.16.0

func (m *MySQLDialect) CreateIndexIfNotExists(table, index string, columns []string, unique bool) string

CreateIndexIfNotExists is a plain CREATE INDEX: MySQL and MariaDB have no IF NOT EXISTS on it. The "Duplicate key name" error (1061) it raises when the index exists counts as success.

func (*MySQLDialect) CreateTableIfNotExists added in v1.16.0

func (m *MySQLDialect) CreateTableIfNotExists(table, body string) string

CreateTableIfNotExists is CREATE TABLE IF NOT EXISTS.

func (*MySQLDialect) CurrentTimestamp

func (m *MySQLDialect) CurrentTimestamp() string

func (*MySQLDialect) DropCheck added in v1.16.0

func (m *MySQLDialect) DropCheck(table, constraint string) string

DropCheck is ALTER TABLE … DROP CHECK (MySQL 8.0.16+).

func (*MySQLDialect) DropForeignKey added in v1.16.0

func (m *MySQLDialect) DropForeignKey(table, constraint string) string

DropForeignKey is ALTER TABLE … DROP FOREIGN KEY.

func (*MySQLDialect) DropIndex added in v1.16.0

func (m *MySQLDialect) DropIndex(table, index string) string

DropIndex names the table: DROP INDEX … ON ….

func (*MySQLDialect) IntrospectSchema added in v0.6.0

func (d *MySQLDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

IntrospectSchema reads the MySQL/MariaDB schema using `INFORMATION_SCHEMA.TABLES` and `INFORMATION_SCHEMA.COLUMNS`. Both engines share the same catalog structure for the column-level surface, so a single implementation covers them (the two Dialect types just delegate here).

MySQL caveats handled here:

  • Scope: `TABLE_SCHEMA = DATABASE()` — the current database, which is MySQL's analogue of PG's `current_schema()`. Cross-database introspection is out of scope (caller would need to switch DBs explicitly).
  • Type representation: we use `COLUMN_TYPE` (full type string with parameters and modifiers — `int(11) unsigned`, `varchar(255)`, `decimal(10,2)`) instead of reassembling from `DATA_TYPE`. MySQL returns this verbatim, which means the round-trip vs the Go migrate-side DDL is comparable without per-type switches.
  • System tables: MySQL exposes `mysql`, `information_schema`, `performance_schema`, `sys` as system databases. Our scope is the user's current DB, so those don't surface; we additionally filter `quark_%` for our internal tables.

func (*MySQLDialect) IsAlreadyExists added in v1.16.0

func (m *MySQLDialect) IsAlreadyExists(object quarkdriver.SchemaObject, err error) bool

IsAlreadyExists recognises error 1061, "Duplicate key name", on a CREATE INDEX.

func (*MySQLDialect) JSONExtract

func (m *MySQLDialect) JSONExtract(column, path string) (string, []any, error)

func (*MySQLDialect) LastInsertIDQuery

func (m *MySQLDialect) LastInsertIDQuery(table, pkColumn string) string

func (*MySQLDialect) LimitOffset

func (m *MySQLDialect) LimitOffset(limit, offset int) string

func (*MySQLDialect) LockSuffix added in v0.4.0

func (m *MySQLDialect) LockSuffix(opts LockOptions) (string, string, error)

MySQL 8.0+ supports SKIP LOCKED and NOWAIT. Older 5.x ignores those modifiers (driver does not error; lock still taken).

func (*MySQLDialect) Name

func (d *MySQLDialect) Name() string

func (*MySQLDialect) Placeholder

func (m *MySQLDialect) Placeholder(index int) string

func (*MySQLDialect) Placeholders

func (m *MySQLDialect) Placeholders(n int) []string

func (*MySQLDialect) Quote

func (m *MySQLDialect) Quote(identifier string) string

func (*MySQLDialect) RenameColumn

func (m *MySQLDialect) RenameColumn(table, oldName, newName string) string

func (*MySQLDialect) RenameTable

func (m *MySQLDialect) RenameTable(oldName, newName string) string

func (*MySQLDialect) Returning

func (m *MySQLDialect) Returning(columns ...string) string

func (*MySQLDialect) SupportsLastInsertID

func (m *MySQLDialect) SupportsLastInsertID() bool

func (*MySQLDialect) SupportsReturning

func (m *MySQLDialect) SupportsReturning() bool

func (*MySQLDialect) SupportsTransactionalDDL

func (m *MySQLDialect) SupportsTransactionalDDL() bool

func (*MySQLDialect) UpsertSQL

func (m *MySQLDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string

UpsertSQL for MySQL: INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)

Without updateCols the clause is a no-op, `<first conflict col> = <itself>`: ON DUPLICATE KEY UPDATE needs one assignment, and the row must be left as it was. It was `= VALUES(<col>)`, which is the same thing only when the duplicate is on that column. ON DUPLICATE KEY UPDATE fires on a duplicate of any unique key, so a duplicate on the primary key wrote the incoming value into the existing row's column (QK-61).

type Nullable added in v0.3.0

type Nullable[T any] = sql.Null[T]

Nullable[T] is a generic wrapper for a column value that may be SQL NULL. It is a thin alias of database/sql.Null[T] (Go 1.22+) so a Nullable[T] already implements both database/sql.Scanner and database/sql/driver.Valuer — drivers handle the round-trip through their existing fast paths and Quark's reflect-based scan / write code does not need to special-case it.

Replace the long-standing pointer-as-nullable idiom (e.g. *time.Time, *string) with Nullable[T] when you want explicit "is set" semantics without a heap allocation per field. The migrate layer recognises the type and emits the SQL type for T (no NOT NULL, since the column is nullable by definition).

Example:

type Profile struct {
    ID   int64                `db:"id" pk:"true"`
    Bio  quark.Nullable[string]    `db:"bio"`
    Born quark.Nullable[time.Time] `db:"born"`
}

p := Profile{
    Bio:  quark.SomeOf("hello"),
    Born: quark.NullOf[time.Time](),
}

func NullOf added in v0.3.0

func NullOf[T any]() Nullable[T]

NullOf returns the SQL-NULL value of Nullable[T] — Nullable[T]{} with the generic spelt out at the call site for readability.

func SomeOf added in v0.3.0

func SomeOf[T any](v T) Nullable[T]

SomeOf returns a non-null Nullable[T] wrapping v. Equivalent to Nullable[T]{V: v, Valid: true} — provided as a constructor so callers don't have to spell the struct literal.

type OTelEventBus added in v0.9.0

type OTelEventBus struct {
	// contains filtered or unexported fields
}

OTelEventBus is an in-tree EventBus that records each event as a structured log line tagged for OpenTelemetry log/trace correlation. It deliberately does NOT pull in the OTel SDK — Quark's otel package owns the tracer/meter wiring, and forcing a span here would couple the core package to the SDK. Instead it writes a correlation-ready slog record; deployments running the otel bridge pick it up as a log record on the active span. Swap in a real span-emitting bus by implementing EventBus yourself if you need first-class spans.

func NewOTelEventBus added in v0.9.0

func NewOTelEventBus(logger *slog.Logger) *OTelEventBus

NewOTelEventBus returns an OTelEventBus. A nil logger falls back to slog.Default.

func (*OTelEventBus) Publish added in v0.9.0

func (b *OTelEventBus) Publish(ctx context.Context, event Event) error

Publish records the event as a correlation-tagged log line. Never returns an error.

type OpAddCheck added in v0.6.0

type OpAddCheck struct {
	Table string
	Check Check
}

OpAddCheck / OpDropCheck — emitted only when both schemas populate Checks (which means neither side is SQLite — see the SQLite Checks=nil contract in Check's godoc). When one side is SQLite, Diff skips the check comparison rather than treating Checks=nil as "no checks" (which would falsely emit DropCheck for every check on the other side).

func (OpAddCheck) String added in v0.6.0

func (o OpAddCheck) String() string

type OpAddColumn added in v0.6.0

type OpAddColumn struct {
	Table  string
	Column Column
}

OpAddColumn is emitted when the desired schema adds a column to a table both sides have. Carries the full Column so the executor can splice the right `<type> [NULL|NOT NULL] [DEFAULT ...]` into the ALTER TABLE ADD COLUMN statement.

func (OpAddColumn) String added in v0.6.0

func (o OpAddColumn) String() string

type OpAddForeignKey added in v0.6.0

type OpAddForeignKey struct {
	Table      string
	ForeignKey ForeignKey
}

OpAddForeignKey / OpDropForeignKey — same model as indexes: name is the match key. SQLite's "" name for inline FKs (see schema.ForeignKey godoc) is handled by Diff matching on (Columns, RefTable, RefColumns) when Name is "" on both sides.

func (OpAddForeignKey) String added in v0.6.0

func (o OpAddForeignKey) String() string

type OpAlterColumn added in v0.6.0

type OpAlterColumn struct {
	Table string
	Old   Column
	New   Column
}

OpAlterColumn is emitted when both sides have a column with the same name but at least one attribute differs (Type, Nullable, or Default). The op carries BOTH the old and the new column so the executor / CLI can render the delta precisely and so resumable migrations (F3-4) can decide whether the alter is safe to retry.

Diff convention: we emit AT MOST ONE OpAlterColumn per (table, column). If both Type and Nullable changed, a single op describes both deltas; the executor decides whether per-attribute ALTERs are needed or a single multi-attribute ALTER is supported.

func (OpAlterColumn) String added in v0.6.0

func (o OpAlterColumn) String() string

type OpCreateIndex added in v0.6.0

type OpCreateIndex struct {
	Table string
	Index Index
}

OpCreateIndex / OpDropIndex are emitted by Diff when the index list of a table both sides have differs. We match indexes by name — there's no fuzzy "same columns, different name" matching in this PR. Renames look like DROP + CREATE, which is what the engines do anyway. If F3-3-execute later wants to detect renames for safety reasons, that's a separate pass.

func (OpCreateIndex) String added in v0.6.0

func (o OpCreateIndex) String() string

type OpCreateTable added in v0.6.0

type OpCreateTable struct {
	Table Table
}

OpCreateTable is emitted when the desired schema has a table that the current schema lacks. The full Table value (columns, indexes, FKs, checks) is carried, and the executor emits ALL of it: the CREATE TABLE carries the foreign keys and the checks inline as table constraints — the only form SQLite accepts, and one every engine accepts — and the indexes follow as CREATE INDEX statements. Diff orders the create-table ops so a referenced table is created before the table that references it (see Diff). Before A8 S3 the executor read only the columns and returned nil, and the same desired schema re-proposed its indexes and keys forever (QK-27).

func (OpCreateTable) String added in v0.6.0

func (o OpCreateTable) String() string

type OpDropCheck added in v0.6.0

type OpDropCheck struct {
	Table string
	Check string
}

func (OpDropCheck) String added in v0.6.0

func (o OpDropCheck) String() string

type OpDropColumn added in v0.6.0

type OpDropColumn struct {
	Table  string
	Column string
}

OpDropColumn is emitted when the current schema has a column the desired schema lacks. Destructive — same caveat as OpDropTable.

func (OpDropColumn) String added in v0.6.0

func (o OpDropColumn) String() string

type OpDropForeignKey added in v0.6.0

type OpDropForeignKey struct {
	Table      string
	ForeignKey string // catalog name; "" on SQLite inline FKs
}

func (OpDropForeignKey) String added in v0.6.0

func (o OpDropForeignKey) String() string

type OpDropIndex added in v0.6.0

type OpDropIndex struct {
	Table string
	Index string
}

func (OpDropIndex) String added in v0.6.0

func (o OpDropIndex) String() string

type OpDropTable added in v0.6.0

type OpDropTable struct {
	Table string
}

OpDropTable is emitted when the current schema has a table that the desired schema lacks. Destructive; F3-3 doesn't gate on a "safe mode" flag — that belongs to the executor / CLI (F3-4 + F3-5) which can prompt or refuse to drop without an explicit flag.

func (OpDropTable) String added in v0.6.0

func (o OpDropTable) String() string

type Operation added in v0.6.0

type Operation interface {
	// String returns a stable human-readable description for the
	// plan output and test failure messages. Format is
	// `<VERB> <subject>` — no DDL syntax, since DDL depends on the
	// dialect.
	String() string
	// contains filtered or unexported methods
}

Operation is one unit of schema change emitted by Diff. It's a dialect-neutral plan node: each op carries the identifiers and neutral shape needed to reconstruct a single DDL statement, and the executor (F3-3-execute, follow-up PR) translates it to per-dialect SQL via the existing migrator helpers.

Operation is a sealed interface — the concrete types in this file are the only valid implementations. F3-3 deliberately models ops as values rather than method calls so the diff stays inspectable (the CLI plan command in F3-5 can render each op as text without touching SQL) and testable (unit tests assert on op structure, not on emitted SQL).

func Diff added in v0.6.0

func Diff(desired, current Schema) []Operation

Diff returns the ordered list of [Operation]s that, applied in order, would bring `current` into alignment with `desired`. Both arguments are dialect-neutral Schema values typically produced by Client.IntrospectSchema (for `current`) and by a future models-to-schema pass (for `desired`, F3-3-plan).

Ordering rules:

  1. Tables present in desired but not in current → OpCreateTable first (so subsequent ops can reference them).
  2. Per table that both sides have, in this exact order: a) ADD COLUMN then ALTER COLUMN (so the new shape is in place before in-place alters). b) DROP CHECK → DROP FK → DROP INDEX → DROP COLUMN (reverse-dependency order: drop the dependent constraint before the column it references). c) CREATE INDEX after all column changes (add/alter/drop) so new indexes can reference new columns and don't trip over dropped ones. d) ADD FOREIGN KEY after CREATE INDEX (FKs typically require an index on the referencing column). e) ADD CHECK last.
  3. Tables present in current but not in desired → OpDropTable LAST (so FK references from other dropped tables are already gone).

Diff is pure and deterministic: same input always produces the same output, and tables/columns/indexes are sorted by name within each step so the plan is reviewable in tests and CLI output.

Diff intentionally does NOT compare:

  • Column.Type strings across dialects (PG "varchar(255)" vs MSSQL "nvarchar(255)" — F3-2 doesn't normalise types, F3-3 compares the strings verbatim and emits an OpAlterColumn if they differ. The caller is expected to feed two schemas from the same dialect, OR explicitly accept the alter noise.).
  • Check.Expression text (each dialect has its own canonical form — see the Check godoc). When both sides have a check by the same name, Diff treats them as equal regardless of expression text. AST-level equivalence is out of scope for this PR.
  • Checks on a side where Checks=nil (the SQLite contract). When desired.Checks=nil OR current.Checks=nil for a table, the check comparison for that table is skipped entirely.

type Option

type Option func(*Client)

Option configures a Client.

func WithCacheCrossInstance added in v1.2.0

func WithCacheCrossInstance() Option

WithCacheCrossInstance enables cross-instance cache-stampede coordination (ADR-0020). Off by default. When enabled AND the installed CacheStore implements CacheLocker (the redis store does; the in-memory store provides a single-process equivalent), a cache miss on a hot key is serialised across processes: one instance acquires a per-key lock and recomputes while the others wait briefly and re-read its result, instead of every instance hitting the database. Costs one lock round-trip per hot-key miss — opt in when you run multiple instances against a shared cache and a hot key's recompute is expensive. No effect without WithCacheStore, or when the store does not implement CacheLocker (the wrapper then falls back to in-process singleflight + XFetch unchanged).

func WithCacheJitter added in v0.8.0

func WithCacheJitter(pct float64) Option

WithCacheJitter tunes the ±jitter factor applied to every TTL when a CacheStore is installed (F4-5, ADR-0011). Default 0.1 (±10%). Range [0, 1]; values outside are clamped. Setting to 0 disables jitter but keeps singleflight and XFetch on — the "todo o nada" of ADR-0011 applies to the wrapper's installation, not to each individual protection. No effect when WithCacheStore is not used.

func WithCacheStore

func WithCacheStore(s CacheStore) Option

WithCacheStore sets the caching backend for the client.

func WithCacheXFetchBeta added in v0.8.0

func WithCacheXFetchBeta(beta float64) Option

WithCacheXFetchBeta tunes the XFetch probabilistic-early-refresh parameter (F4-5, ADR-0011). Default 1.0; range >= 0. Higher β makes early refresh more aggressive; β = 0 disables XFetch entirely (still keeps singleflight and jitter active). No effect when WithCacheStore is not used.

Both the on/off flag AND the stored beta are written on every call, so a sequence like WithCacheXFetchBeta(2.0) → WithCacheXFetchBeta(0) leaves no residual β behind: the final state is XFetch off with stampedeXFetchBeta = 0.

func WithDeadlockRetry added in v0.8.0

func WithDeadlockRetry(maxAttempts int) Option

WithDeadlockRetry enables transparent retry of Client.Tx when the transaction is killed by a deadlock (F4-7).

maxAttempts is the total number of attempts (1 = no retry, the historical behaviour; 0 or negative also disables). When the transaction closure returns an error that isDeadlock recognises from the active driver (PG 40P01, MySQL 1213, MSSQL 1205, Oracle ORA-00060), the runner sleeps with exponential backoff + jitter and re-executes the closure against a fresh transaction. Non- deadlock errors propagate immediately.

The retry wraps the ENTIRE closure, not individual queries — a deadlock aborts the whole tx, so re-running just the failed query would race against a half-committed state. Disabled by default to keep the historical at-most-once-per-call semantics; opt in when the workload genuinely deadlocks under contention.

client, _ := quark.New("pgx", dsn, quark.WithDeadlockRetry(3))

func WithDefaultTZ added in v0.7.0

func WithDefaultTZ(loc *time.Location) Option

WithDefaultTZ sets the fallback timezone for time.Time columns that do not carry their own quark:"tz=..." tag.

The contract (see docs/adr/0010): time.Time values always go to the database as UTC — the column stores the same instant regardless of engine — and are converted to loc in memory when scanned back. loc therefore affects only how the struct field reads in Go, not what is persisted.

A column-level quark:"tz=America/New_York" tag always overrides this default. When neither a default nor a tag is set, time.Time values pass through to the driver untouched (the historical v0.6 behaviour), so this feature is fully opt-in.

client, _ := quark.New("pgx", dsn, quark.WithDefaultTZ(time.UTC))

func WithDialect

func WithDialect(d Dialect) Option

WithDialect sets the SQL dialect for the client. If not set, the dialect will be auto-detected from the database driver.

func WithLimits

func WithLimits(l Limits) Option

WithLimits sets the security and performance limits.

Zero-valued NUMERIC fields are filled from DefaultLimits before the struct is installed: zero means "not touched", never "no limit" or "no timeout". Without this normalization a partial literal — the natural way to raise one limit — left QueryTimeout at 0, and since every builder execution path derives context.WithTimeout from it, every query context was born already expired (#262). Negative values pass through untouched.

Asymmetry, on purpose: the boolean fields are NOT normalized (an explicit false is indistinguishable from the zero value). A partial literal therefore keeps AllowRawQueries=false AND SafeMigrations=false — the latter differs from the DefaultLimits value (true), which means Sync may drop columns. When you want the defaults for the booleans, start from DefaultLimits and modify only what you need.

Because that asymmetry is easy to miss, New logs one structured WARN (event quark.limits.partial_literal_safe_migrations_off) when the literal shows the partial-literal signal — the numeric normalization actually filled at least one zero field — AND SafeMigrations ended up false. A full literal (every numeric field set, e.g. DefaultLimits() with SafeMigrations flipped off on purpose) carries no signal and stays silent. The warning changes no behaviour.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for the client. Passing nil is a no-op: the client keeps its default (slog.Default) rather than dropping to a nil logger. This guarantees the internal log sites (slow-query log, deadlock-retry, post-commit hook errors, event-emit failures) always have a non-nil logger to write to.

func WithMiddleware

func WithMiddleware(m Middleware) Option

WithMiddleware adds middleware to the query execution chain. Middleware is applied in the order they are added: the first added is the outermost, so it sees a statement first and its result last.

func WithQueryObserver

func WithQueryObserver(o QueryObserver) Option

WithQueryObserver adds a query observer to the client. Multiple observers can be added and will be called in order.

func WithReplicaDownCooldown added in v0.13.0

func WithReplicaDownCooldown(d time.Duration) Option

WithReplicaDownCooldown sets how long a read replica stays out of rotation after a transient connection failure before it is retried (F6-6, ADR-0015). The default is 5s. Tune it to your topology: shorter for same-AZ replicas, longer for cross-region ones where a flap is costlier to re-probe. Only meaningful alongside WithReplicas; a non-positive value keeps the default.

func WithReplicaStrategy added in v1.0.0

func WithReplicaStrategy(s ReplicaStrategy) Option

WithReplicaStrategy sets how a routed read picks among healthy replicas (F6-5, ADR-0015): ReplicaRoundRobin (default), ReplicaRandom, or ReplicaLeastConn. Only meaningful alongside WithReplicas with more than one replica; with a single replica all strategies pick it. Every strategy honours the F6-6 health cooldown.

client, err := quark.New("pgx", primaryDSN,
    quark.WithReplicas(replica1DSN, replica2DSN),
    quark.WithReplicaStrategy(quark.ReplicaLeastConn),
)

func WithReplicas added in v0.13.0

func WithReplicas(dsns ...string) Option

WithReplicas registers read-replica DSNs (F6-5, ADR-0015). New opens one read-only connection pool per DSN (same pool options and dialect as the primary) and routes reads to them — both multi-row reads (List / Iter / eager-loading) and single-row reads (First / Find / Count / aggregates) — while writes always go to the primary. Pass the same engine's replica endpoints; a read-your-writes path uses Sticky. The selection strategy defaults to round-robin; change it with WithReplicaStrategy.

Opt-in: without it, every operation uses the single primary connection, unchanged. Reads inside Client.Tx and under RowLevelSecurityNative always use the primary regardless (see ADR-0015).

Failover (F6-6): if a replica fails a read with a transient connection error, the read fails over to the primary and the replica is taken out of rotation for a cooldown (default 5s), then retried — a downed replica degrades performance, not correctness.

client, err := quark.New("pgx", primaryDSN,
    quark.WithReplicas(replica1DSN, replica2DSN),
    quark.WithMaxOpenConns(16),
)

func WithSlowQueryThreshold added in v0.8.0

func WithSlowQueryThreshold(d time.Duration) Option

WithSlowQueryThreshold enables structured slow-query logging.

Every query, exec, query-row, raw-query and raw-exec whose duration exceeds the threshold is emitted at WARN through Client.logger with structured attributes (duration_ms, threshold_ms, operation, table, rows, sql). Bind arguments are NOT included — the SQL is the parameterised form, mirroring the F4-2 span redaction principle: logs MUST NOT see user values they have no authority to retain.

A threshold of 0 or negative (the default) disables the feature entirely — the check is a single cheap comparison on the hot observer path. Recommended starting point: 100ms.

client, _ := quark.New("pgx", dsn, quark.WithSlowQueryThreshold(100*time.Millisecond))

func WithStatementCache added in v1.17.0

func WithStatementCache(size int) Option

WithStatementCache keeps up to size prepared statements per connection pool and reuses them, instead of letting the driver prepare, execute and close a statement on every query (QK-37, ADR-0027). Off by default; size <= 0 keeps it off.

What it changes is the protocol, not the results. On MySQL and MariaDB, go-sql-driver/mysql sends a query that has arguments as three commands — prepare, execute, close — and two round trips; with the cache the prepare and the close happen once per statement and connection, and every later call is one execute. The server prepares exactly the statements it already prepared without the cache, so the SQL it runs, the binary protocol it answers in and the values the caller reads back are the same. Measured on the engine bench (benchmarks/engines, control MY-02, mysql:8.4), FindByPK took 46–48 % less time with the cache than without it, in the same rounds, on the CI runner, and 41–42 % less on a laptop; the figures and the machines are on the benchmarks page.

What is cached: the statements the query builder sends (StatementQuery and StatementExec) that carry at least one argument, no sql.NamedArg, and at most 4 KiB of SQL. A statement without arguments is left alone because the MySQL driver sends it as plain text today, and preparing it would change the protocol it travels in; a long one is almost always a multi-row batch whose text changes with the row count. Schema work, savepoints and raw SQL are never cached.

Where it applies: the client's pool and each replica pool get a cache of their own, least recently used out first. Inside a transaction, a statement is prepared on the transaction's connection the first time the transaction sends it, and reused for the rest of it. Statements sent on a *sql.Conn — the migration lock, the per-statement transaction of RowLevelSecurityNative — and on any executor a middleware substitutes go to the driver as before. The middleware chain and the observers see the same statements and the same events either way.

PostgreSQL and Oracle: the option is ignored, and New says so in its log. pgx already keeps a statement cache per connection, which also handles a plan that a schema change invalidated; a database/sql statement on top of it would prepare twice and handle neither. On Oracle, go-ora v2.9.0 — the driver drivers/oracle ships — answered a re-executed prepared SELECT COUNT(*) with 0 for a row that existed, once a DELETE and an INSERT had run between two executions, on one connection and without Quark in between; a stale count is how Quark tells a stale entity from an excluded one, so the cache stays off there until the driver is fixed (ADR-0027).

Sizing: each connection that runs a cached statement holds it prepared on the server until the statement is evicted or the connection closes, so a pool can hold up to size × MaxOpenConns statements. MySQL caps prepared statements per server with max_prepared_stmt_count (16382 by default), across every client of the server; size it so every application instance fits under it.

The MySQL alternative is interpolateParams=true in the DSN: the driver then writes the arguments into the SQL text and sends one query, with no server-side statement at all. The dialects reference page compares the two.

client, err := quark.New("mysql", dsn, quark.WithStatementCache(256))

func WithStrictColumns added in v1.8.0

func WithStrictColumns() Option

WithStrictColumns enables the opt-in column-membership check (AQ-02): a plain column reference in Where / WhereIn / WhereBetween / OrderBy / GroupBy / Select / Having and the aggregates (Sum / Avg / Min / Max) must be a column the model's metadata knows, or the query fails with ErrInvalidQuery naming the unknown column and listing the valid ones.

Why opt in: SQLGuard's charset validation stops injection, but a typo'd column name passes it — and on SQLite the double-quoted unknown column degrades to a string literal, so `Where("agee", ">", 1)` silently matches every row with err == nil; on other engines it fails only at runtime with the engine's own error. Strict columns turns both into a deterministic, actionable failure at query build time.

Escape hatches, by design: queries with JOINs are exempt (they legitimately reference other tables' columns), raw fragments (WhereExpr / WhereJSON / HavingAggregate / RawQuery) are never checked, and ORDER BY / GROUP BY may reference a SelectExpr alias. Off by default for backward compatibility; candidate to become the default in v2.

client, _ := quark.New("pgx", dsn, quark.WithStrictColumns())

func WithStrictReads added in v1.4.0

func WithStrictReads(mode StrictReadsMode) Option

WithStrictReads enables the opt-in strict-read mode (#247).

Rationale and scope, decided at design time:

  • List() is NOT affected: it keeps its historical safe-default cap of 100 rows plus its own WARN. Strict reads covers the entrypoints that have no implicit cap — Iter() and Cursor() — where forgetting Limit() silently streams the whole table.

  • Escalation is per client, not per query: WARN by default (StrictReadsWarn), reject with ErrInvalidQuery under StrictReadsReject. Legitimately unbounded reads (exports, backfills) opt out per query with Query.AllowUnbounded.

  • N+1 detection is per context, because "same read repeated" is only meaningful within one request/operation scope: prepare the context with TrackReads and every single-row read by primary key is counted per table; at the threshold (10) the client logs ONE structured WARN per context+table suggesting Preload. Without a TrackReads context nothing is counted. Detection never rejects.

  • Cost when off: a single integer comparison per read entrypoint — no map, no allocation, no context traversal.

    client, _ := quark.New("pgx", dsn, quark.WithStrictReads(quark.StrictReadsWarn))

type OracleDialect

type OracleDialect struct {
	// contains filtered or unexported fields
}

OracleDialect implements the Oracle Database dialect.

func (*OracleDialect) AcquireMigrationLock added in v1.0.0

func (d *OracleDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)

AcquireMigrationLock uses Oracle's `DBMS_LOCK` package — the session- scoped advisory lock that mirrors PG's `pg_advisory_lock` and MySQL's `GET_LOCK`. `ALLOCATE_UNIQUE(name)` maps the lock name to a handle (deterministic per name), and `REQUEST(handle, X_MODE, release_on_commit => FALSE)` takes the exclusive lock. Because the lock is session-scoped and explicitly NOT released on commit, it survives Oracle's implicit DDL commits and needs no open transaction — which is what lets it fit the connection-only DBConn interface (a lock-table `SELECT … FOR UPDATE` would need a held transaction the interface doesn't expose). See ADR-0018.

REQUEST returns 0 (acquired), 1 (timeout), 2 (deadlock), 3 (parameter error), 4 (already own this lock), 5 (illegal handle). 0 and 4 are success; 1 maps to ErrLockTimeout; the rest are errors.

Requires `GRANT EXECUTE ON DBMS_LOCK TO <user>` — DBMS_LOCK is not granted to schema users by default. The timeout is in whole seconds (DBMS_LOCK's granularity); sub-second waits round up to 1s.

Prerequisite caveat is documented in website/docs/guides/migrations.mdx.

func (*OracleDialect) AlterColumn added in v1.16.0

AlterColumn writes one MODIFY with only what changes — restating NOT NULL on a column that already is one is ORA-01442 — and adds or drops the primary key by role.

func (*OracleDialect) AlterTableAddColumn

func (o *OracleDialect) AlterTableAddColumn(table, column, dataType string) string

func (*OracleDialect) AlterTableAlterColumn

func (o *OracleDialect) AlterTableAlterColumn(table, column, newDataType string) string

func (*OracleDialect) AlterTableDropColumn

func (o *OracleDialect) AlterTableDropColumn(table, column string) string

func (*OracleDialect) AutoIncrementColumn added in v1.16.0

func (*OracleDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY.

func (*OracleDialect) BoolLiteral added in v1.16.0

func (*OracleDialect) BoolLiteral(v bool) string

BoolLiteral is 1 or 0: Oracle's NUMBER(1) rejects TRUE and FALSE.

func (*OracleDialect) BuildProcedureCall

func (o *OracleDialect) BuildProcedureCall(procedure string, argCount int) string

func (*OracleDialect) BuildRoutineQuery

func (o *OracleDialect) BuildRoutineQuery(routine string, argCount int) string

func (*OracleDialect) ColumnType added in v1.16.0

func (*OracleDialect) ColumnType(s quarkdriver.ColumnSpec) string

ColumnType is Oracle's type for a column of the given kind.

func (*OracleDialect) CreateIndexIfNotExists added in v1.16.0

func (o *OracleDialect) CreateIndexIfNotExists(table, index string, columns []string, unique bool) string

CreateIndexIfNotExists is CREATE INDEX IF NOT EXISTS, which Oracle accepts since 23ai. ORA-01408 ("such column list already indexed") counts as success.

func (*OracleDialect) CreateTableIfNotExists added in v1.16.0

func (o *OracleDialect) CreateTableIfNotExists(table, body string) string

CreateTableIfNotExists is a plain CREATE TABLE: Oracle has no IF NOT EXISTS on it. ORA-00955 ("name is already used by an existing object") counts as success.

func (*OracleDialect) CurrentTimestamp

func (o *OracleDialect) CurrentTimestamp() string

func (*OracleDialect) IntrospectSchema added in v1.0.0

func (o *OracleDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

IntrospectSchema reads the Oracle schema from the data dictionary (`USER_TABLES`, `USER_TAB_COLUMNS`, `USER_INDEXES`/`USER_IND_COLUMNS`, `USER_CONSTRAINTS`/`USER_CONS_COLUMNS`). It is scoped to the connected user's own schema — the `USER_*` views never cross schema boundaries.

Oracle-specific handling:

  • Identifiers come back UPPERCASE (Oracle folds unquoted names), so every table / column / referenced name is lowercased to match the neutral lowercase form the models and the other dialects use.
  • NUMBER reassembly: precision/scale are glued back on (`NUMBER(19)`, `NUMBER(10,2)`); a NULL precision stays bare `NUMBER`. CHAR/VARCHAR2 use CHAR_LENGTH so the displayed length matches the DDL the migrator wrote.
  • NOT NULL is stored as a system CHECK constraint (`"COL" IS NOT NULL`); those are filtered out of Checks — they are surfaced via Column.Nullable, not as user CHECK constraints.
  • CHECK predicates are read from `SEARCH_CONDITION_VC` (the VARCHAR2 mirror of the LONG `SEARCH_CONDITION`, available on 12.2+), avoiding LONG-column scan issues.
  • Oracle has no ON UPDATE referential action; OnUpdate is always "NO ACTION".

func (*OracleDialect) IsAlreadyExists added in v1.16.0

func (o *OracleDialect) IsAlreadyExists(object quarkdriver.SchemaObject, err error) bool

IsAlreadyExists recognises ORA-00955 on a CREATE TABLE, ORA-01408 on a CREATE INDEX and ORA-02264 ("name already used by an existing constraint") on an ADD CONSTRAINT.

func (*OracleDialect) JSONExtract

func (o *OracleDialect) JSONExtract(column, path string) (string, []any, error)

func (*OracleDialect) LastInsertIDQuery

func (o *OracleDialect) LastInsertIDQuery(table, pkColumn string) string

func (*OracleDialect) LimitOffset

func (o *OracleDialect) LimitOffset(limit, offset int) string

func (*OracleDialect) LockSuffix added in v0.4.0

func (o *OracleDialect) LockSuffix(opts LockOptions) (string, string, error)

Oracle pessimistic locking. SKIP LOCKED supported on 12c+.

FOR UPDATE [NOWAIT|SKIP LOCKED]

Oracle does not have a FOR SHARE; map LockForShare → ErrUnsupportedFeature rather than emitting an unsafe approximation.

func (*OracleDialect) MapColumnType added in v1.0.0

func (o *OracleDialect) MapColumnType(t string) string

MapColumnType translates a neutral column-type string into Oracle's native form. Oracle has no TEXT type (`ORA-00902: invalid datatype`), so the generic TEXT that other engines accept becomes CLOB — the semantic equivalent for unbounded text. Already-native types pass through unchanged, so this is safe to apply to every column type.

func (*OracleDialect) Name

func (d *OracleDialect) Name() string

func (*OracleDialect) Placeholder

func (o *OracleDialect) Placeholder(index int) string

Placeholder returns ":index", without fmt for the reason given on PostgresDialect.Placeholder.

func (*OracleDialect) Placeholders

func (o *OracleDialect) Placeholders(n int) []string

func (*OracleDialect) Quote

func (o *OracleDialect) Quote(identifier string) string

func (*OracleDialect) ReferentialAction added in v1.17.0

func (o *OracleDialect) ReferentialAction(event quarkdriver.ReferentialEvent, action string) quarkdriver.ActionSupport

ReferentialAction: Oracle has ON DELETE CASCADE and ON DELETE SET NULL. It has no ON UPDATE clause and no clause for NO ACTION (ORA-02000), which is what it does for a foreign key that names no action — so NO ACTION is left out, in either clause, and everything else is refused: ON DELETE RESTRICT (ORA-02000), ON DELETE SET DEFAULT (ORA-03001) and every ON UPDATE action but NO ACTION.

func (*OracleDialect) ReleaseSavepointStmt added in v1.1.0

func (o *OracleDialect) ReleaseSavepointStmt(name string) string

func (*OracleDialect) RenameColumn

func (o *OracleDialect) RenameColumn(table, oldName, newName string) string

func (*OracleDialect) RenameTable

func (o *OracleDialect) RenameTable(oldName, newName string) string

func (*OracleDialect) Returning

func (o *OracleDialect) Returning(columns ...string) string

func (*OracleDialect) RollbackToSavepointStmt added in v1.1.0

func (o *OracleDialect) RollbackToSavepointStmt(name string) string

func (*OracleDialect) SavepointStmt added in v1.1.0

func (o *OracleDialect) SavepointStmt(name string) string

func (*OracleDialect) SupportsLastInsertID

func (o *OracleDialect) SupportsLastInsertID() bool

func (*OracleDialect) SupportsReturning

func (o *OracleDialect) SupportsReturning() bool

func (*OracleDialect) SupportsTransactionalDDL

func (o *OracleDialect) SupportsTransactionalDDL() bool

SupportsTransactionalDDL returns false: Oracle commits implicitly before and after every DDL statement, so a ROLLBACK does not undo one. It said true until A11 Q2, and nothing acted on the answer — ApplyPlan decided by the dialect's name, which put Oracle on the resumable path; since it asks this method, the answer has to be the engine's.

func (*OracleDialect) UpsertSQL

func (o *OracleDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string

UpsertSQL for Oracle: MERGE syntax — same as MSSQL, built separately.

type Page

type Page[T any] struct {
	Items      []T   // The items for current page
	Total      int64 // Total count (if available)
	Page       int   // Current page number (0-indexed)
	PageSize   int   // Items per page
	TotalPages int64 // Calculated total pages
}

Page represents a paginated result set.

type Plan added in v0.6.0

type Plan struct {
	// Ops is the diff between the desired schema (derived from
	// models) and the current schema (from [Client.IntrospectSchema]).
	// See the [Diff] godoc for ordering guarantees.
	Ops []Operation
}

Plan is the result of Client.PlanMigration: the ordered list of [Operation]s that, applied to the database, would bring it into alignment with the Go-side models.

A Plan is inert — it doesn't execute itself. F3-3-execute (the follow-up PR) will add a Plan.Apply method that walks Ops and dispatches each to the per-dialect migrator helpers. The CLI plan command (F3-5) renders the Plan via Plan.String without ever touching SQL.

func (Plan) Down added in v1.14.0

func (p Plan) Down() (Plan, error)

Down returns the plan that undoes p: every operation inverted, in reverse order, so that applying p and then its Down leaves the schema where it started.

plan, _ := client.PlanMigration(ctx, &User{})
down, err := plan.Down()          // build it BEFORE applying plan
if err != nil {
    return err                     // not reversible; see below
}
if err := client.ApplyPlan(ctx, plan); err != nil {
    return err
}
// …later, to roll back:
err = client.ApplyPlan(ctx, down)

Reverse order matters as much as the inversion: a plan that creates a table and then indexes it has to drop the index before the table, or the drop refers to something that no longer exists.

What is not reversible, and why it errors rather than guessing

Two operations lose information, so their inverse cannot be derived from the plan alone:

  • OpDropTable carries only the table's NAME. Recreating it needs the columns, keys and indexes it had, which the plan does not record.
  • OpDropColumn carries only the column's name, not its type, nullability or default.

Down returns an error naming the operation instead of emitting a partial rollback. A rollback that silently restores a table without its indexes is worse than one that refuses: it reports success and leaves the schema subtly different.

Capture Down BEFORE applying the plan when you intend to roll back. Nothing stops you calling it after — the plan is a value — but a plan built from a schema you have already changed describes a different starting point.

func (Plan) Hash added in v0.6.0

func (p Plan) Hash() string

Hash returns a deterministic SHA-256 hex digest of the Plan's operation sequence. Used by Client.ApplyPlan's resumable path to detect plan drift between runs: if the same plan_hash carries over from a partially-applied run, the resume can pick up where it left off; if the hash differs, the Plan has changed and the resume is unsafe (e.g. the user added a column to their model between runs).

The digest input is the line-joined `op.String()` output of every op. Op.String() formats are documented as stable in the F3-3-core godoc, so the hash is reproducible across processes and binaries on the same Plan value. A change in any op's content — even cosmetic, like a renamed table — produces a new hash, which is the right safety boundary for resume.

Empty plans hash to the sha256 of the empty string (a constant) so two empty plans compare equal.

func (Plan) IsEmpty added in v0.6.0

func (p Plan) IsEmpty() bool

IsEmpty reports whether the Plan would be a no-op when applied. Equivalent to `len(p.Ops) == 0` but more readable in user code.

Use this as the "did anything drift?" check in CI / health endpoints — a non-empty Plan means the Go models and the database schema have diverged.

func (Plan) String added in v0.6.0

func (p Plan) String() string

String renders the Plan as a multi-line human-readable report. Each Op contributes one line via its own Operation.String. Empty plans render as "(no changes)".

The format is intentionally minimal so the F3-5 CLI can wrap it without parsing — table or coloured output is the CLI's responsibility, not the Plan's.

type PoolOption added in v0.3.0

type PoolOption interface {
	// contains filtered or unexported methods
}

PoolOption is a configuration option for the database connection pool. These are applied to the *sql.DB before creating the Client.

func WithConnMaxIdleTime added in v0.3.0

func WithConnMaxIdleTime(d time.Duration) PoolOption

WithConnMaxIdleTime sets the maximum amount of time a connection may be idle.

func WithConnMaxLifetime added in v0.3.0

func WithConnMaxLifetime(d time.Duration) PoolOption

WithConnMaxLifetime sets the maximum amount of time a connection may be reused.

func WithMaxIdleConns added in v0.3.0

func WithMaxIdleConns(n int) PoolOption

WithMaxIdleConns sets the maximum number of idle connections in the pool.

func WithMaxOpenConns added in v0.3.0

func WithMaxOpenConns(n int) PoolOption

WithMaxOpenConns sets the maximum number of open connections to the database.

type PostgresDialect

type PostgresDialect struct {
	// contains filtered or unexported fields
}

PostgresDialect implements the PostgreSQL dialect.

func (*PostgresDialect) AcquireMigrationLock added in v0.6.0

func (d *PostgresDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)

AcquireMigrationLock uses `pg_advisory_lock(hashtext(name))` on a dedicated connection. Session-level (not transaction-level), so the caller can run multiple statements under the lock without holding a long transaction open. Released by `pg_advisory_unlock` on Release.

Timeout is honoured via `SET lock_timeout` on the connection — PG's native way to bound advisory-lock waits. A timeout violation surfaces as SQLSTATE `55P03` (`lock_not_available`); we translate it to `ErrLockTimeout`.

func (*PostgresDialect) AlterColumn added in v1.16.0

AlterColumn writes the SQL standard's statements — ALTER COLUMN … TYPE, SET/DROP NOT NULL, SET/DROP DEFAULT — and adds or drops the primary key as a constraint; the name of the one to drop is read from pg_constraint.

func (*PostgresDialect) AlterTableAddColumn

func (p *PostgresDialect) AlterTableAddColumn(table, column, dataType string) string

func (*PostgresDialect) AlterTableAlterColumn

func (p *PostgresDialect) AlterTableAlterColumn(table, column, newDataType string) string

func (*PostgresDialect) AlterTableDropColumn

func (p *PostgresDialect) AlterTableDropColumn(table, column string) string

func (*PostgresDialect) AutoIncrementColumn added in v1.16.0

func (*PostgresDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is BIGSERIAL PRIMARY KEY.

func (*PostgresDialect) BoolLiteral added in v1.16.0

func (*PostgresDialect) BoolLiteral(v bool) string

BoolLiteral is TRUE or FALSE: PostgreSQL's BOOLEAN rejects 1 and 0.

func (*PostgresDialect) BuildProcedureCall

func (p *PostgresDialect) BuildProcedureCall(procedure string, argCount int) string

func (*PostgresDialect) BuildRoutineQuery

func (p *PostgresDialect) BuildRoutineQuery(routine string, argCount int) string

func (*PostgresDialect) ColumnType added in v1.16.0

ColumnType is PostgreSQL's type for a column of the given kind.

func (*PostgresDialect) CurrentTimestamp

func (p *PostgresDialect) CurrentTimestamp() string

func (*PostgresDialect) IntrospectSchema added in v0.6.0

func (d *PostgresDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

IntrospectSchema reads the PG schema by querying `information_schema` (more portable than `pg_catalog` and sufficient for the column-level surface F3-2 needs). The `current_schema()` (typically `public`) scopes the lookup so multi-schema setups don't drag in unrelated tables.

PG caveats handled here:

  • The data type returned by `data_type` is the SQL-standard form (`integer`, `bigint`, `character varying`). For native parameter-bearing types (`varchar(255)`, `numeric(10,2)`) we reassemble the precision/scale/length from the adjacent columns so the round-trip vs Go-side schema is comparable.
  • The `column_default` is preserved as-is — including PG's `nextval('seq')` wrappers around SERIAL/IDENTITY columns. The diff layer is responsible for recognising those.

func (*PostgresDialect) JSONExtract

func (p *PostgresDialect) JSONExtract(column, path string) (string, []any, error)

func (*PostgresDialect) LastInsertIDQuery

func (p *PostgresDialect) LastInsertIDQuery(table, pkColumn string) string

func (*PostgresDialect) LimitOffset

func (p *PostgresDialect) LimitOffset(limit, offset int) string

func (*PostgresDialect) LockSuffix added in v0.4.0

func (p *PostgresDialect) LockSuffix(opts LockOptions) (string, string, error)

PostgreSQL pessimistic locking (PG 9.5+ for SKIP LOCKED).

FOR UPDATE [SKIP LOCKED|NOWAIT]
FOR SHARE  [SKIP LOCKED|NOWAIT]

func (*PostgresDialect) Name

func (d *PostgresDialect) Name() string

func (*PostgresDialect) Placeholder

func (p *PostgresDialect) Placeholder(index int) string

Placeholder returns "$index". It is called once per bound parameter, so it formats the number with strconv into a stack buffer rather than through fmt, which cost 13 % of a 1000-row batch's CPU (QK-36); the statement builders that write one placeholder per value skip even this string, see writePlaceholder.

func (*PostgresDialect) Placeholders

func (p *PostgresDialect) Placeholders(n int) []string

func (*PostgresDialect) Quote

func (p *PostgresDialect) Quote(identifier string) string

func (*PostgresDialect) RenameColumn

func (p *PostgresDialect) RenameColumn(table, oldName, newName string) string

func (*PostgresDialect) RenameTable

func (p *PostgresDialect) RenameTable(oldName, newName string) string

func (*PostgresDialect) Returning

func (p *PostgresDialect) Returning(columns ...string) string

func (*PostgresDialect) SupportsLastInsertID

func (p *PostgresDialect) SupportsLastInsertID() bool

func (*PostgresDialect) SupportsReturning

func (p *PostgresDialect) SupportsReturning() bool

func (*PostgresDialect) SupportsTransactionalDDL

func (p *PostgresDialect) SupportsTransactionalDDL() bool

func (*PostgresDialect) UpsertSQL

func (p *PostgresDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string

UpsertSQL for PostgreSQL: INSERT … ON CONFLICT (cols) DO UPDATE SET col = EXCLUDED.col

type Predicate added in v0.12.0

type Predicate struct {
	// contains filtered or unexported fields
}

Predicate is a single typed WHERE condition produced by a TypedColumn accessor. It is opaque: build it through a TypedColumn method (Eq, Gt, In, …) and pass it to Query.WhereP. Because the only construction path is a generated column handle, the column identifier never originates from caller-supplied input.

type Query

type Query[T any] struct {
	BaseQuery
}

Query represents a type-safe database query builder for model T. All builder methods return a new Query (immutable/clone pattern) for thread-safety. Execution methods are in query_exec.go and query_crud.go

func For

func For[T any](ctx context.Context, provider ClientProvider) *Query[T]

For creates a Query builder for the given model type. This is the primary entry point for type-safe database operations.

Example:

type User struct {
    ID   int64  `db:"id"`
    Name string `db:"name"`
}

user, err := quark.For[User](ctx, client).Find(1)
users, err := quark.For[User](ctx, client).Where("active", "=", true).List()

func ForTx

func ForTx[T any](ctx context.Context, tx *Tx) *Query[T]

ForTx creates a Query builder for the given model type bound to a transaction. This is the transactional counterpart of For[T]().

Example:

err := client.Tx(ctx, func(tx *quark.Tx) error {
    return quark.ForTx[User](ctx, tx).Create(&user)
})

func WhereInOf added in v1.8.0

func WhereInOf[T any, V any](q *Query[T], column string, values []V) *Query[T]

WhereInOf is the typed-slice form of Query.WhereIn (AQ-07): it accepts any element type ([]int64, []string, …) and performs the []any conversion once, internally — the caller no longer hand-writes the conversion loop that a []int64 of ids from a previous query used to force. It is a package-level function because Go methods cannot introduce a second type parameter.

ids := []int64{1, 2, 3}
users, err := quark.WhereInOf(quark.For[User](ctx, client), "id", ids).List()

func (*Query[T]) AllowUnbounded added in v1.4.0

func (q *Query[T]) AllowUnbounded() *Query[T]

AllowUnbounded declares this read intentionally unbounded — an export, a backfill, a full-table scan the caller has thought about. Under WithStrictReads it exempts the query from the Iter()/Cursor() no-Limit WARN (StrictReadsWarn) and rejection (StrictReadsReject). It does NOT change what the query returns: List() keeps its safe default cap, and Iter()/Cursor() already stream every matching row. No effect when strict reads is off (#247).

err := quark.For[Event](ctx, client).AllowUnbounded().Iter(export)

func (*Query[T]) Apply

func (q *Query[T]) Apply(scopes ...Scope[T]) *Query[T]

Apply applies one or more Scope functions to the query. Scopes are composable, reusable query fragments.

Example:

activeUsers := func(q *quark.Query[User]) *quark.Query[User] {
    return q.Where("active", "=", true)
}
users, _ := quark.For[User](ctx, client).Apply(activeUsers).List()

func (*Query[T]) AsSubquery added in v0.4.0

func (q *Query[T]) AsSubquery() (*Subquery, error)

AsSubquery captures the current Query[T] as a renderable Subquery. The SELECT is rendered eagerly: identifier validation, soft-delete and tenant predicates, JOINs, GROUP BY, HAVING, ORDER BY, LIMIT all run at AsSubquery time.

The captured SQL uses '?' as the bind marker; the outer query swaps it for the dialect's placeholder when the wrapping Expr is rendered. The SELECT cols can be customised via the standard Select() before capture — typical use is `Select("id")` for an `IN (subquery)` shape.

Pessimistic lock options (`ForUpdate` / `ForShare` / `SkipLocked` / `NoWait`) are explicitly rejected on the inner query — MSSQL emits table hints (`WITH (UPDLOCK, ROWLOCK)`) inline in the FROM clause that are not legal inside a subquery context, and PG / MySQL / Oracle's `FOR UPDATE` suffix on a subquery is technically valid but misleading when the outer caller already drives row locking. Acquire locks on the outer query instead.

func (*Query[T]) Avg

func (q *Query[T]) Avg(column string) (float64, error)

Avg returns the average of the given column across all matching rows.

func (*Query[T]) Cache

func (q *Query[T]) Cache(ttl time.Duration, tags ...string) *Query[T]

Cache enables caching for this query results with the given TTL.

func (*Query[T]) CheckVersions added in v1.17.0

func (q *Query[T]) CheckVersions() *Query[T]

CheckVersions makes Query.UpdateBatch on a model with a quark:"version" field hold every row to the version it was loaded with, as Update, UpdateFields and Tracked.Save already do for one row:

err := quark.For[Account](ctx, client).CheckVersions().UpdateBatch(accounts)
if errors.Is(err, quark.ErrStaleEntity) {
    // nothing was written; err names every row whose version moved
}

The batch is all or nothing. A row whose UPDATE matches nothing is stale — another writer moved its version, or its key does not exist — unless the query's conditions exclude it, which skips it without error, as Update does. Every stale row is collected, the whole batch is rolled back, and the error joins one ErrStaleEntity per stale row, each naming its key. When the batch is written, each entity's in-memory version is bumped, so the next Update of the same struct does not report a conflict that did not happen; a batch that fails bumps none.

Without CheckVersions, UpdateBatch keeps its v1 behaviour: the version predicate is sent, a row it does not match is not written, the call returns nil, and the in-memory versions are not bumped. Quark 2.0 makes the checked behaviour the default for UpdateBatch (DEP-2026-003). On a model without a version column CheckVersions changes nothing.

func (*Query[T]) Count

func (q *Query[T]) Count() (int64, error)

Count returns the total number of matching rows.

func (*Query[T]) Create

func (q *Query[T]) Create(entity *T) error

Create inserts a new record. The entity must have a db tag on fields to be persisted. Returns with the ID set from the database. Create inserts a new record and recursively saves associations.

func (*Query[T]) CreateBatch

func (q *Query[T]) CreateBatch(entities []*T) error

CreateBatch inserts multiple records in a single SQL statement using bulk VALUES. Each entity gets its PK populated when the dialect supports RETURNING; otherwise PKs are left at their zero value (callers can re-query if needed).

Example:

users := []*User{{Name: "Alice"}, {Name: "Bob"}}
err := quark.For[User](ctx, client).CreateBatch(users)

func (*Query[T]) CrossJoin added in v1.14.0

func (q *Query[T]) CrossJoin(table string) *Query[T]

CrossJoin appends a CROSS JOIN — the cartesian product, which takes no ON clause. It returns the Query directly rather than a JoinBuilder, because there is no condition to attach.

func (*Query[T]) Cursor

func (q *Query[T]) Cursor() (*Cursor[T], error)

Cursor returns a Cursor for manual iteration over large result sets. The Cursor must be closed after use (defer cursor.Close()).

Example:

cursor, err := quark.For[User](ctx, client).Where("active", "=", true).Cursor()
if err != nil { log.Fatal(err) }
defer cursor.Close()

for cursor.Next() {
    var user User
    if err := cursor.Scan(&user); err != nil { break }
    process(user)
}

func (*Query[T]) Delete

func (q *Query[T]) Delete(entity *T) (int64, error)

Delete performs a soft delete by setting deleted_at = NOW(). If the model doesn't have deleted_at field, performs hard delete.

The query's conditions — its Where calls, and the tenant predicate under RowLevelSecurityClient — are ANDed with the key (QK-40): when no row with the key satisfies them, Delete removes nothing, returns (0, nil), and runs no AfterDelete hook, audit entry or event. Returns the number of rows affected.

func (*Query[T]) DeleteBatch

func (q *Query[T]) DeleteBatch(ids []any) (int64, error)

func (*Query[T]) DeleteBy

func (q *Query[T]) DeleteBy() (int64, error)

DeleteBy performs a hard delete with WHERE conditions. Requires Where clause for safety.

func (*Query[T]) Distinct

func (q *Query[T]) Distinct() *Query[T]

Distinct adds SELECT DISTINCT to the query.

func (*Query[T]) Except added in v0.4.0

func (q *Query[T]) Except(other *Query[T]) *Query[T]

Except renders `EXCEPT` (spelled `MINUS` on Oracle): the rows in the base that are not in the operand, deduplicated.

Supported on PostgreSQL, MariaDB (10.3+), MSSQL, Oracle and SQLite. MySQL returns ErrUnsupportedFeature at render time, for the same version-probe reason as Intersect.

func (*Query[T]) ExceptAll added in v1.3.0

func (q *Query[T]) ExceptAll(other *Query[T]) *Query[T]

ExceptAll is the multiset variant: `EXCEPT ALL` subtracts by multiplicity instead of deduplicating — a row present three times on the left and once on the right comes back twice.

Narrower support than Except: PostgreSQL and MariaDB (10.5+) only. SQL Server and SQLite have no EXCEPT ALL, Oracle only gained MINUS ALL in 21c — a version quark does not assume without a runtime probe — and MySQL has no EXCEPT at all; every one of them returns ErrUnsupportedFeature at render time.

func (*Query[T]) Find

func (q *Query[T]) Find(id any) (T, error)

Find retrieves a single row by primary key, ANDed with the query's conditions: the tenant predicate of RowLevelSecurityClient, the soft-delete filter, and any Where the caller added (QK-42, QK-40). A row the conditions exclude — another tenant's among them — is ErrNotFound, the same answer as a key that does not exist.

func (*Query[T]) First

func (q *Query[T]) First() (T, error)

First returns the first matching row or ErrNotFound.

The limit it sets is put back when it returns: it used to stay on the receiver, and a later List on the same query returned one row. Put back rather than set on a copy, because a copy of the query is one more allocation of its whole struct on every First and every Find — the engine bench (PG-02, MY-01) holds Find's allocations to 5 % of the record.

func (*Query[T]) ForShare added in v0.4.0

func (q *Query[T]) ForShare() *Query[T]

ForShare marks the query so the emitted SELECT acquires a shared read lock. Composes with SkipLocked / NoWait. Not supported on SQLite; MSSQL approximates with HOLDLOCK; PG / MySQL 8+ / MariaDB / Oracle support it natively.

func (*Query[T]) ForUpdate added in v0.4.0

func (q *Query[T]) ForUpdate() *Query[T]

ForUpdate marks the query so the emitted SELECT acquires a row-level FOR UPDATE lock. Composes with SkipLocked / NoWait. Returns the receiver (no error) so it chains naturally; SQL surface failures (unsupported dialect, invalid combination) surface at execution time.

rows, err := quark.For[Order](ctx, client).
    Where("status", "=", "pending").
    ForUpdate().
    Limit(50).
    List()

func (*Query[T]) FromCTE added in v1.14.0

func (q *Query[T]) FromCTE(name string) *Query[T]

FromCTE makes the SELECT read from a CTE declared with Query.With or Query.WithRecursive instead of the model's own table.

Without it, With() declares the CTE and the outer SELECT still reads the base table, so the only way to reach a CTE was to JOIN it — which works, and is the right shape when you want both. It is not the right shape when the derived rows ARE the query:

// average orders per user: the aggregate is over the derived rows, and
// the base table must not be in the FROM at all.
per, _ := quark.For[Order](ctx, client).
    GroupBy("user_id").
    SelectExpr("n", quark.Func("COUNT", quark.Col("*"))).
    AsSubquery()

avg, err := quark.For[Order](ctx, client).
    With("per_user", per).
    FromCTE("per_user").
    Avg("n")

It changes the SELECT path only. UPDATE, DELETE and INSERT keep writing to the model's table: a CTE is not a write target, and silently redirecting a write would be the worst possible reading of this call.

The scanned type is still T, so the CTE's projection has to carry the columns T expects — the same contract a JOIN already has.

Two implicit scopes behave differently under FromCTE, and the difference is deliberate:

  • The soft-delete filter is DROPPED. It is a property of the model's table, and a CTE need not carry deleted_at at all; emitting it anyway produced `SELECT * FROM "ids" WHERE "deleted_at" IS NULL` against a one-column CTE, which SQLite answered with zero rows and no error. Apply it in the subquery that builds the CTE, where it means something.
  • The tenant filter is KEPT. Dropping it would turn a missing column into cross-tenant reads, so the CTE must project the tenant column. If it does not, the query fails or returns nothing — which is the safe direction to be wrong in.

func (*Query[T]) FromTable added in v1.14.0

func (q *Query[T]) FromTable(name string) *Query[T]

FromTable reads from a named table instead of the one derived from T.

It exists for projections. For[T] takes both the row shape and the source table from T, so a struct that is a result shape rather than a model — the columns of a join, an aggregate roll-up — sends the query at a table named after the DTO, which does not exist:

type OrderEmail struct {
    OrderID int64  `db:"order_id"`
    Email   string `db:"email"`
}

rows, err := quark.For[OrderEmail](ctx, client).
    FromTable("orders").
    Join("users").On("orders.user_id", "=", "users.id").
    SelectExpr("order_id", quark.Col("orders.id")).
    SelectExpr("email", quark.Col("users.email")).
    List()

Like FromCTE it changes reads only, and for the same reason: a projection is not a write target. The soft-delete and tenant scopes follow the same rules as FromCTE — see its documentation, and note that a DTO has no model metadata to derive them from in the first place.

func (*Query[T]) FullJoin added in v1.14.0

func (q *Query[T]) FullJoin(table string) *JoinBuilder[T]

FullJoin opens a FULL OUTER JOIN. See Join for ON-clause grammar.

MySQL and MariaDB do not implement it. Quark does not rewrite it into the LEFT-UNION-RIGHT form those engines need — the rewrite changes how the query plans and how duplicates behave, and doing it silently would hide that. Write the union yourself there, or use an engine that has it.

func (*Query[T]) GroupBy

func (q *Query[T]) GroupBy(columns ...string) *Query[T]

GroupBy adds a GROUP BY clause.

func (*Query[T]) HardDelete

func (q *Query[T]) HardDelete(entity *T) (int64, error)

HardDelete permanently deletes the entity by its primary key.

The query's conditions are ANDed with the key, as in Delete: when no row with the key satisfies them, HardDelete removes nothing, returns (0, nil), and runs no AfterDelete hook, audit entry or event (QK-40).

func (*Query[T]) Having

func (q *Query[T]) Having(column string, operator string, value any) *Query[T]

Having adds a HAVING condition (used together with GroupBy).

The column argument is validated as a plain identifier — no parentheses, function calls, or expressions. To filter on aggregates such as COUNT(*) or SUM(col), use HavingAggregate instead.

func (*Query[T]) HavingAggregate added in v0.4.0

func (q *Query[T]) HavingAggregate(fn, column, operator string, value any) *Query[T]

HavingAggregate adds a HAVING condition over an aggregate function.

fn must be one of COUNT, SUM, AVG, MIN, MAX (case-insensitive). column is either a regular column name (validated through SQLGuard) or "*" — only accepted with COUNT, since "SUM(*)" / "AVG(*)" / etc. are not valid SQL. operator goes through the same whitelist Where uses (=, !=, <>, <, <=, >, >=, IN, NOT IN, BETWEEN, NOT BETWEEN, IS [NOT] NULL, LIKE, NOT LIKE).

Example:

groups, err := quark.For[Order](ctx, client).
    GroupBy("status").
    HavingAggregate("COUNT", "*", ">", 5).
    List()
// emitted: ... GROUP BY "status" HAVING COUNT(*) > $1

This closes the historic Having(column, op, value) limitation where the column went through ValidateIdentifier and aggregates therefore could not be expressed without RawQuery. The structured-AST form Having(Func("count", Col("*")), ">", 5) arrives with the full Phase 2 AST; HavingAggregate is the focused, type-safe shortcut for the overwhelmingly common case.

func (*Query[T]) HavingExpr added in v0.4.0

func (q *Query[T]) HavingExpr(e Expr) *Query[T]

HavingExpr adds a HAVING condition built from the Expr AST. Same rendering pipeline as WhereExpr; useful for aggregate predicates that need the full composition surface (Func("COUNT", Col("*")) > Lit(5), and so on).

func (*Query[T]) Intersect added in v0.4.0

func (q *Query[T]) Intersect(other *Query[T]) *Query[T]

Intersect renders `INTERSECT` between the base and the operand: the rows present in both, deduplicated.

Supported on PostgreSQL, MariaDB (10.3+), MSSQL, Oracle and SQLite. MySQL returns ErrUnsupportedFeature at render time — INTERSECT needs 8.0.31+ and Quark will not assume the server version without probing it.

func (*Query[T]) IntersectAll added in v1.3.0

func (q *Query[T]) IntersectAll(other *Query[T]) *Query[T]

IntersectAll is the multiset variant: `INTERSECT ALL` keeps duplicate rows rather than collapsing them, so a row appearing twice on both sides comes back twice.

Narrower support than Intersect: PostgreSQL and MariaDB (10.5+) only. SQL Server and SQLite have no INTERSECT ALL, Oracle only gained it in 21c — a version quark does not assume without a runtime probe — and MySQL has no INTERSECT at all; every one of them returns ErrUnsupportedFeature at render time.

func (*Query[T]) Iter

func (q *Query[T]) Iter(fn func(T) error) error

Iter executes the query and iterates over results one by one. Uses streaming to handle large datasets without loading all into memory.

Example:

err := quark.For[User](ctx, client).Where("active", "=", true).Iter(func(user User) error {
    process(user)
    return nil
})

func (*Query[T]) Join

func (q *Query[T]) Join(table string) *JoinBuilder[T]

Join opens an INNER JOIN against `table`. Complete the JOIN with `.On(left, op, right)` (typed binary identifier comparison) or `.OnRaw(onClause)` (free-form, validated through the same identifier grammar as `On`).

The structured form replaces the v0.3.x string-raw `Join(table, on)` signature; see `MIGRATION_v0.4.0.md` for the mechanical rewrite.

func (*Query[T]) LeftJoin

func (q *Query[T]) LeftJoin(table string) *JoinBuilder[T]

LeftJoin opens a LEFT JOIN. See Join for ON-clause grammar.

func (*Query[T]) Limit

func (q *Query[T]) Limit(n int) *Query[T]

Limit sets the maximum number of rows to return.

func (*Query[T]) List

func (q *Query[T]) List() ([]T, error)

List executes the query and returns all matching rows. If Limit() is not called, uses a safe default (100) to prevent OOM. Use Iter() for unbounded streaming or Paginate() for large datasets.

func (*Query[T]) Max

func (q *Query[T]) Max(column string) (float64, error)

Max returns the maximum value of the given column across all matching rows.

func (*Query[T]) Min

func (q *Query[T]) Min(column string) (float64, error)

Min returns the minimum value of the given column across all matching rows.

func (*Query[T]) MustAsSubquery added in v0.4.0

func (q *Query[T]) MustAsSubquery() *Subquery

MustAsSubquery is the panic-on-error variant of AsSubquery for use in expression composition where errors would otherwise have to be threaded through the AST. The error is realistic (invalid identifier, etc.) only when the inner query is malformed; for well-formed inputs it never triggers.

func (*Query[T]) NoWait added in v0.4.0

func (q *Query[T]) NoWait() *Query[T]

NoWait tells the database to fail immediately if any matching row is already locked by another transaction. Combine with ForUpdate / ForShare. Implementation-defined per dialect.

func (*Query[T]) Offset

func (q *Query[T]) Offset(n int) *Query[T]

Offset sets the number of rows to skip.

func (*Query[T]) OnlyTrashed added in v0.3.0

func (q *Query[T]) OnlyTrashed() *Query[T]

OnlyTrashed returns a query that filters down to soft-deleted rows (deleted_at IS NOT NULL) so callers can list, restore, or hard-delete the trash. A no-op when the model has no deleted_at column.

func (*Query[T]) Or

func (q *Query[T]) Or(fn func(*Query[T]) *Query[T]) *Query[T]

Or adds an OR condition group. The callback receives a fresh Query to build conditions. All conditions within the callback are grouped with AND and joined to the outer query with OR.

Example:

quark.For[User](ctx, client).
    Where("active", "=", true).
    Or(func(q *Query[User]) *Query[User] {
        return q.Where("role", "=", "admin").Where("role", "=", "superadmin")
    }).List()

Generates: WHERE "active" = $1 OR ("role" = $2 AND "role" = $3)

The scopes Quark adds — the soft-delete filter on reads, the tenant predicate of RowLevelSecurityClient — are ANDed with the caller's conditions as a whole, Or groups included: on a soft-delete model the example above reads WHERE "deleted_at" IS NULL AND ("active" = $1 OR (…)). Under RowLevelSecurityClient the OR group also carries the tenant predicate itself.

func (*Query[T]) OrderBy

func (q *Query[T]) OrderBy(column string, direction string) *Query[T]

OrderBy adds an ORDER BY clause.

direction must be "ASC" or "DESC" (case-insensitive); the empty string keeps its historical meaning of ASC. Any other value stashes ErrInvalidQuery on the query and surfaces at execution — the same contract the operator whitelist enforces. Before this validation, anything that wasn't exactly "DESC"/"desc" (including "Desc") silently ordered ascending (AQ-06).

func (*Query[T]) Paginate

func (q *Query[T]) Paginate(pageSize, page int) (*Page[T], error)

Paginate executes the query with pagination. Returns current page, total count, and error.

Example:

page, err := quark.For[User](ctx, client).Paginate(100, 0) // 100 per page, page 0
page, err := quark.For[User](ctx, client).Where("active", "=", true).Paginate(50, 2)

func (*Query[T]) PaginateAfter added in v1.15.0

func (q *Query[T]) PaginateAfter(pageSize int, after string) (*KeysetPage[T], error)

PaginateAfter returns the first pageSize rows after the position named by after — "" for the first page — in the query's ORDER BY, and the token of the last row returned. The ordering decides the seek: for ORDER BY a ASC, b DESC the continuation is `a > ? OR (a = ? AND b < ?)`, expanded rather than written as a row comparison because SQL Server and Oracle have none. The primary key is appended to the ordering when it is not already there, so the order is total and no row is skipped or repeated between pages.

Every ordering column has to be a column of the model: the token is built from the last row's values, and an expression has none to read back. The token is opaque and versioned by shape — a token minted under a different ORDER BY is refused with ErrInvalidQuery rather than seeking to the wrong place.

func (*Query[T]) Preload

func (q *Query[T]) Preload(relations ...string) *Query[T]

Preload specifies relations to load automatically.

func (*Query[T]) PreloadWhere added in v1.14.0

func (q *Query[T]) PreloadWhere(relation, column, operator string, value any) *Query[T]

PreloadWhere eager-loads a relation and filters the related rows.

Preload is variadic over relation NAMES, which makes a filter impossible to express and easy to get wrong: Preload("Orders", "status = ?") compiles, reads the second argument as another relation, and fails with "relation not found" — but only once the parent query returns rows, so an empty table hides the mistake entirely.

// users, each carrying only their paid orders
quark.For[User](ctx, client).
    PreloadWhere("Orders", "status", "=", "paid").
    List()

The condition applies to the relation's own query, so it narrows which children load — it does not drop parents that end up with none. Repeated calls on the same relation AND together. The relation is registered for preloading, so PreloadWhere alone is enough; there is no need to also call Preload for it.

column and operator go through the same identifier and operator barriers as Where.

func (*Query[T]) Restore added in v0.3.0

func (q *Query[T]) Restore(entity *T) (int64, error)

Restore clears the deleted_at column on the row identified by entity's primary key, "untrashing" it. Returns the number of rows affected.

Restore implicitly scopes to currently-trashed rows (deleted_at IS NOT NULL): a Restore on a row that was never deleted is a 0-row no-op rather than a corrupting NULL-write. Useful as the inverse of Delete in admin flows. The query's conditions — its Where calls, and the tenant predicate under RowLevelSecurityClient — are ANDed with the key (QK-40).

Phase-1 F1-5. Returns ErrInvalidModel when the model has no deleted_at.

func (*Query[T]) RightJoin

func (q *Query[T]) RightJoin(table string) *JoinBuilder[T]

RightJoin opens a RIGHT JOIN. See Join for ON-clause grammar.

func (*Query[T]) Select

func (q *Query[T]) Select(columns ...string) *Query[T]

Select specifies columns to select. If empty, all columns are selected.

func (*Query[T]) SelectExpr added in v0.4.0

func (q *Query[T]) SelectExpr(alias string, e Expr) *Query[T]

SelectExpr adds an AST projection to the SELECT list, aliased as `alias`. Use it for window functions, scalar subqueries, or any expression the plain `Select(cols...)` API can't model:

q := quark.For[Order](ctx, client).
    SelectExpr("rank", quark.Over(quark.Rank(),
        quark.NewWindow().
            PartitionBy(quark.Col("status")).
            OrderBy(quark.Col("amount"), true))).
    SelectExpr("running_total", quark.Over(
        quark.Func("SUM", quark.Col("amount")),
        quark.NewWindow().OrderBy(quark.Col("id"), false)))

The expression is rendered against a `qmark`-emitting dialect at SelectExpr time, so the inner '?' markers are reindexed to the outer dialect's placeholder syntax when buildSelect runs. The args land in the args slice between any CTE args and the WHERE args — matching the SQL-surface order of the SELECT projection.

Composing SelectExpr with the plain Select(cols...) is allowed: the regular columns render first, the AST projections after, comma- separated. If neither is set, the SELECT defaults to '*'.

func (*Query[T]) SkipLocked added in v0.4.0

func (q *Query[T]) SkipLocked() *Query[T]

SkipLocked tells the database to skip rows that are already locked by another transaction instead of blocking on them. Combine with ForUpdate / ForShare. Implementation-defined per dialect.

func (*Query[T]) Sum

func (q *Query[T]) Sum(column string) (float64, error)

Sum returns the sum of the given column across all matching rows.

func (*Query[T]) Track added in v0.3.0

func (q *Query[T]) Track() *TrackedQuery[T]

Track returns a TrackedQuery whose Find/First/List yield *Tracked[T] values carrying a column-value snapshot. Call Save on the result to emit an UPDATE that touches only the columns whose values changed since load — the permanent fix for the zero-value trap (P0-4).

Track is opt-in. Existing Find/First/List remain unchanged.

func (*Query[T]) Union added in v0.4.0

func (q *Query[T]) Union(other *Query[T]) *Query[T]

Union appends a UNION (DISTINCT) operand to the query. The combined statement renders flat — `SELECT ... UNION SELECT ...` — without parentheses around the operands, since SQLite's compound-select grammar rejects parenthesised operands. The flat form is the portable shape across all six target dialects.

Identifier validation runs eagerly on the operand so a malformed other-query surfaces at attach time, not at the outer query's exec time. Outer-query `OrderBy` / `Limit` apply to the combined result (the SQL standard binding); the operand cannot have its own ORDER BY / LIMIT (rejected with `ErrUnsupportedFeature`). See attachSetOp for the full operand restriction list.

func (*Query[T]) UnionAll added in v0.4.0

func (q *Query[T]) UnionAll(other *Query[T]) *Query[T]

UnionAll is the multiset variant: `UNION ALL` keeps duplicate rows.

func (*Query[T]) Unscoped

func (q *Query[T]) Unscoped() *Query[T]

Unscoped ignores soft-delete filters for the query, returning both trashed and non-trashed rows. Equivalent to WithTrashed; kept for backward compatibility.

func (*Query[T]) Update

func (q *Query[T]) Update(entity *T) (int64, error)

Update updates the entity by its primary key with partial-update semantics: only fields whose value is non-zero for their type are written.

CAUTION — zero-value trap: because zero values are skipped, calling Update cannot write false to a bool, 0 to an integer, "" to a string, or nil to a pointer/slice/map. This is the documented semantics of Update on the v1 line and is not scheduled to change within it. To write a zero value explicitly, use UpdateFields or UpdateMap; to write exactly the fields that changed since a read, load the entity with Query.Track and call Tracked.Save, which diffs against the snapshot and writes zero values. When Update skips a scalar zero (false / 0 / ""), it logs a WARN so callers notice the silent skip; skipped nil pointers/slices/maps are the expected "absent" case and do not warn.

The query's conditions — its Where calls, and the tenant predicate under RowLevelSecurityClient — are ANDed with the key, so they can only narrow which row is written (QK-40): Where("tenant_id", "=", t).Update(&e) writes e's row only when it belongs to t. When no row with the key satisfies them, Update writes nothing, returns (0, nil), and runs no AfterUpdate hook, audit entry or event. On a model with a version column, ErrStaleEntity means the row satisfies the conditions and its version moved. An entity whose key is zero is inserted, as before; the conditions do not apply to an insert. Returns the number of rows affected.

CAUTION — recursive association save (AQ-03): Update recursively saves every loaded association, exactly like Create. An entity read with Find + Preload carries its children in memory, so updating one scalar field ALSO re-writes every loaded child from that in-memory snapshot — silently overwriting concurrent changes to those children, and without the extra writes showing in the returned rows-affected count. When associations are about to be written, Update logs a WARN naming them. Call Query.WithoutAssociations to write only the entity's own row.

func (*Query[T]) UpdateBatch

func (q *Query[T]) UpdateBatch(entities []*T) error

UpdateBatch updates multiple records by their primary keys within a single transaction. Each entity undergoes a partial update: zero-value fields are skipped (same semantics as Update). A transaction is used to guarantee atomicity across all rows.

On a query bound to a transaction (ForTx) the batch runs in that transaction, inside a savepoint: a failing row undoes the rows before it and leaves the caller's transaction usable, and the caller's rollback undoes the whole batch. It used to open a transaction of its own on another connection even there, which waited for the caller's locks — until the query timeout on SQLite — or committed apart from the caller (QK-57).

The query's conditions are ANDed with each entity's key, as in Update (QK-40): an entity whose row does not satisfy them is not written, the way an entity whose key does not exist is not written. UpdateBatch returns no count, so a caller that needs to know which rows were written calls Update per entity.

On a model with a quark:"version" field each UPDATE carries the version predicate, but by default a row it does not match is skipped in silence: the call returns nil and no in-memory version is bumped. Chain Query.CheckVersions to have the batch rolled back with ErrStaleEntity for every such row instead, and the versions of the written rows bumped (QK-45). That becomes the default in Quark 2.0 (DEP-2026-003).

Example:

err := quark.For[User](ctx, client).UpdateBatch(users)

func (*Query[T]) UpdateFields added in v0.3.0

func (q *Query[T]) UpdateFields(entity *T, fields ...string) (int64, error)

UpdateFields updates only the named fields on the entity, bypassing the zero-value filter that Update applies. This is the recommended API when you need to write false / 0 / "" / nil to a column — values that Update would silently skip.

fields are matched against struct field db tags only — the same identifier resolution as Update and Find. Listing a struct field name without a db tag returns ErrInvalidQuery: there is one canonical name per column and we don't accept aliases here, to keep the resolution unambiguous.

The primary key is never overwritten; listing a PK column returns an error. If the client is configured with the RowLevelSecurityClient tenant strategy, the tenant column is injected before the SET clause is built; callers do not need to (and should not) list it explicitly.

Example:

user := User{ID: 42, Active: false}
rows, err := quark.For[User](ctx, client).UpdateFields(&user, "active")
// emitted: UPDATE "users" SET "active" = $1 WHERE "id" = $2  args=[false, 42]

The query's conditions are ANDed with the key, as in Update: when no row with the key satisfies them, UpdateFields writes nothing, returns (0, nil) and runs no AfterUpdate hook, audit entry or event (QK-40).

Returns the number of rows affected.

func (*Query[T]) UpdateMap

func (q *Query[T]) UpdateMap(data map[string]any) (int64, error)

UpdateMap updates fields using a map (for partial updates without full entity). Requires Where clause for safety.

Under RowLevelSecurityClient a map that names the tenant column writes the resolved tenant into it, whatever value the map holds (QK-44); a different value is logged (event quark.tenant.foreign_value_replaced), as Create and the updates by entity do. The caller's map is not modified. Returns the number of rows affected.

func (*Query[T]) Upsert

func (q *Query[T]) Upsert(entity *T, conflictCols []string, updateCols []string) error

Upsert inserts or updates a record depending on whether a conflict occurs on conflictCols. updateCols specifies which columns to update on conflict.

An empty updateCols does not mean the same on every engine (QK-48). On PostgreSQL, SQLite, MySQL and MariaDB the conflicting row is left as it was — insert-or-ignore — and Upsert returns nil; on PostgreSQL and SQLite the entity's key is then not written back. On SQL Server and Oracle the MERGE updates every column the insert writes except the conflict columns, the primary key and created_at, which keeps the row's creation time (QK-62). Pass updateCols for behaviour that does not depend on the engine.

The conflict columns name the key the update branch is for on PostgreSQL, SQLite, SQL Server and Oracle. MySQL and MariaDB have no conflict target: ON DUPLICATE KEY UPDATE fires on a duplicate of any unique key, the primary key included, so with updateCols the row that holds the duplicate is updated whichever key it is; without updateCols it is left as it was (QK-61). On PostgreSQL and SQLite a duplicate of a key other than the conflict columns fails with ErrConstraintViolation.

Under RowLevelSecurityClient the update branch only touches a row of the resolved tenant (QK-43). When the conflicting row belongs to another tenant, nothing is written and Upsert returns an error wrapping ErrConstraintViolation — what a Create of the same key returns.

The key written into the entity (QK-63, QK-64). When Upsert returns nil and the model's key is one integer column, the entity's key is the key of the row the statement inserted or updated, read from that statement, whether the entity carried a key or not: RETURNING on PostgreSQL, SQLite and MariaDB; the statement's own insert id on MySQL, whose clause ends with `<pk> = LAST_INSERT_ID(<pk>)` so that the update branch reports the row it met; MERGE … OUTPUT on SQL Server and MERGE … RETURNING on Oracle, where an inserted row has the key the identity assigned, not the one the entity carried. When nothing is written on a conflict — DO NOTHING on PostgreSQL and SQLite, a MERGE with no update branch — the entity keeps its key; MySQL and MariaDB report the conflicting row's. A row of another tenant gives no key. The entity is never given the key of another row.

Example:

quark.For[User](ctx, client).Upsert(&user, []string{"email"}, []string{"name", "updated_at"})

func (*Query[T]) UpsertBatch

func (q *Query[T]) UpsertBatch(entities []*T, conflictCols []string, updateCols []string) error

UpsertBatch inserts or updates multiple records in a single batch operation. conflictCols defines uniqueness (e.g. primary key or unique index columns). updateCols defines which columns to update on conflict. An empty updateCols leaves a conflicting row as it was on PostgreSQL, SQLite, MySQL and MariaDB, and updates every non-conflict column but the primary key and created_at on SQL Server and Oracle, as for Query.Upsert.

On SQL Server and Oracle each entity whose key is one integer column gets the key of the row it inserted or updated, as with Query.Upsert (QK-64). The multi-row INSERT of PostgreSQL, SQLite, MySQL and MariaDB reports no key per row, and the entities keep the keys they carried; under RowLevelSecurityClient MySQL and MariaDB upsert one row at a time and write each key as Upsert does.

Dialect strategies:

  • Postgres / SQLite / MySQL / MariaDB: multi-row INSERT … ON CONFLICT / ON DUPLICATE KEY
  • MSSQL: single MERGE … USING (VALUES …) AS src(…)
  • Oracle: N individual MERGE statements (Oracle IDENTITY restriction prevents bulk MERGE)

Example:

err := quark.For[User](ctx, client).UpsertBatch(users, []string{"email"}, []string{"name"})

func (*Query[T]) Where

func (q *Query[T]) Where(column string, operator string, value any) *Query[T]

Where adds a WHERE condition with AND logic.

func (*Query[T]) WhereBetween

func (q *Query[T]) WhereBetween(column string, start, end any) *Query[T]

WhereBetween adds a WHERE ... BETWEEN condition.

func (*Query[T]) WhereContains added in v1.15.0

func (q *Query[T]) WhereContains(column, text string) *Query[T]

WhereContains adds a case-sensitive "contains" search for text the user typed: the text is escaped with EscapeLike, wrapped in `%`, and bound to a LIKE that declares its escape character. A `%` or `_` in the text is matched as that character, on every engine.

func (*Query[T]) WhereEndsWith added in v1.15.0

func (q *Query[T]) WhereEndsWith(column, text string) *Query[T]

WhereEndsWith adds a suffix search for text the user typed; see Query.WhereContains for what happens to wildcards in it.

func (*Query[T]) WhereExpr added in v0.4.0

func (q *Query[T]) WhereExpr(e Expr) *Query[T]

WhereExpr adds a WHERE condition built from a composable Expr AST.

The AST is rendered against the active dialect at call time, producing a fragment with '?' bind markers plus the args. Storage and execution reuse the existing raw-fragment slot in condition: buildWhereClause substitutes each '?' for the dialect placeholder at the correct argIndex, so the AST stays dialect-agnostic at construction time and integrates cleanly with WhereJSON, Or, and the rest of the builder.

Errors raised during ToSQL — unknown function names, invalid identifiers, invalid operators, empty IN lists — are stashed on the query and surface at execution time wrapping ErrInvalidQuery.

Example:

q := quark.For[User](ctx, client).WhereExpr(
    quark.Or(
        quark.Eq(quark.Col("role"), quark.Lit("admin")),
        quark.And(
            quark.Gt(quark.Col("logins"), quark.Lit(10)),
            quark.Eq(quark.Col("verified"), quark.Lit(true)),
        ),
    ),
)

func (*Query[T]) WhereIn

func (q *Query[T]) WhereIn(column string, values []any) *Query[T]

WhereIn adds a WHERE ... IN condition.

func (*Query[T]) WhereJSON

func (q *Query[T]) WhereJSON(column, path, operator string, value any) *Query[T]

WhereJSON adds a WHERE condition for a JSON field. column is the JSON column name, path is a dotted key path within the JSON object (e.g. "user.name"). The path is always validated by guard.ValidateJSONPath and, on every dialect except Oracle, bound as a parameter rather than interpolated. Oracle's JSON_VALUE rejects a bound path (ORA-40454), so there the validated path is inlined as a literal; the [A-Za-z0-9_.] grammar keeps it injection-safe. See guard.ValidateJSONPath for the accepted grammar.

On invalid path the error is stashed on the query and surfaces at execution time (List, First, etc.), wrapping ErrInvalidJSONPath.

func (*Query[T]) WhereLike added in v1.15.0

func (q *Query[T]) WhereLike(column, pattern string) *Query[T]

WhereLike adds `column LIKE pattern` with the escape character declared on the statement (`ESCAPE '\'`), so the pattern means the same thing on every engine: `%` and `_` are wildcards, and a backslash makes the next character literal. Compose the pattern from user text with EscapeLike.

It differs from Where(column, "LIKE", pattern) in exactly that: the plain form hands the engine an opaque pattern and lets the engine's own default decide what a backslash means, which is not the same on SQLite, SQL Server and Oracle as it is on PostgreSQL and MySQL (QK-25). The plain form is unchanged — its meaning is published — and this one is the portable form.

A pattern that ends in a dangling escape character is refused with ErrInvalidQuery before it reaches the engine: PostgreSQL rejects it, other engines match nothing or match the backslash, and none of those is what the caller meant.

func (*Query[T]) WhereNot

func (q *Query[T]) WhereNot(column string, operator string, value any) *Query[T]

WhereNot adds a WHERE NOT condition with AND logic.

Example:

quark.For[User](ctx, client).WhereNot("active", "=", false).List()

Generates: WHERE NOT ("active" = $1)

func (*Query[T]) WhereNotLike added in v1.15.0

func (q *Query[T]) WhereNotLike(column, pattern string) *Query[T]

WhereNotLike is the negation of Query.WhereLike, with the same escape character declared.

func (*Query[T]) WhereP added in v0.12.0

func (q *Query[T]) WhereP(preds ...Predicate) *Query[T]

WhereP appends one or more typed predicates (built from generated column accessors) to the query, combined with AND. It is the compile-time-safe counterpart to Query.Where; the two may be mixed freely on the same query. Returns a clone, like every other builder method.

func (*Query[T]) WhereStartsWith added in v1.15.0

func (q *Query[T]) WhereStartsWith(column, text string) *Query[T]

WhereStartsWith adds a prefix search for text the user typed; see Query.WhereContains for what happens to wildcards in it.

func (*Query[T]) WhereSubquery

func (q *Query[T]) WhereSubquery(column, operator, subquery string) *Query[T]

WhereSubquery adds a WHERE column operator (subquery) condition. The subquery is a raw SQL string. Use this only when AllowRawQueries is enabled.

Example:

sub := "SELECT MAX(id) FROM orders WHERE status = 'open'"
quark.For[User](ctx, client).WhereSubquery("id", "IN", sub).List()

func (*Query[T]) With added in v0.4.0

func (q *Query[T]) With(name string, sub *Subquery) *Query[T]

With attaches a non-recursive CTE to the query. The CTE renders as `WITH <name> AS (<inner>)` before the outer SELECT, and the outer query can reference the CTE by name in JOIN clauses.

Example:

topOrders, _ := quark.For[Order](ctx, client).
    Where("amount", ">", 100).
    Select("user_id", "amount").
    AsSubquery()

users, err := quark.For[User](ctx, client).
    With("top_orders", topOrders).
    Join("top_orders").On("users.id", "=", "top_orders.user_id").
    Limit(50).
    List()

Multiple With calls compose: the entries render comma-separated in the order they were added. If any entry is recursive, the prefix becomes `WITH RECURSIVE ...`.

func (*Query[T]) WithRecursive added in v0.4.0

func (q *Query[T]) WithRecursive(name string, sub *Subquery) *Query[T]

WithRecursive is the recursive form. The prefix becomes recursive when at least one entry is recursive; the RECURSIVE keyword itself is dialect-aware (see recursiveCTEKeyword): PostgreSQL/MySQL/MariaDB/SQLite emit `WITH RECURSIVE`, while Oracle and SQL Server infer recursion structurally and reject the keyword, so Quark emits a plain `WITH` there. The inner Subquery is responsible for shaping the recursive body — typically a `UNION ALL` between a base case and a step that references the CTE name. Composing that body is Query.UnionAll plus Query.AsSubquery:

anchor := quark.For[Category](ctx, client).Where("id", "=", rootID)
step := quark.For[Category](ctx, client).
    Join("tree").On("categories.parent_id", "=", "tree.id")
body, _ := anchor.UnionAll(step).AsSubquery()

rows, err := quark.For[Category](ctx, client).
    WithRecursive("tree", body).FromCTE("tree").List()

This comment used to say the typed Subquery surface could not model UNION, so recursive use was limited to single-SELECT bodies. That stopped being true when set operators landed, and the stale note outlived it — long enough that A4's own measurement session read it, wrote the case the way the comment implied, and recorded the capability as missing. It is not.

(Oracle also requires a column-alias list on the CTE name for a genuinely recursive body; that must live in the Subquery's own SQL.)

func (*Query[T]) WithTrashed added in v0.3.0

func (q *Query[T]) WithTrashed() *Query[T]

WithTrashed returns a query that includes both soft-deleted (trashed) and live rows — the same effect as Unscoped, named for parity with the scope-driven idiom that other ORMs use. Only meaningful when the model carries a deleted_at column.

func (*Query[T]) WithoutAssociations added in v1.8.0

func (q *Query[T]) WithoutAssociations() *Query[T]

WithoutAssociations disables the recursive association save for this query's Create/Update: only the entity's own row is written; whatever the relation fields hold in memory is NOT persisted.

This is the opt-out for the AQ-03 lost-update trap: an entity read with Find + Preload carries its children loaded, so a later Update of just one scalar field also re-wrote every loaded child with the in-memory snapshot — silently overwriting any concurrent change to those children (the extra UPDATEs are not reflected in the returned rows-affected).

got, _ := quark.For[User](ctx, client).Preload("Posts").Find(1)
got.Name = "renamed"
// writes ONLY users; the loaded got.Posts stay untouched in the DB
rows, err := quark.For[User](ctx, client).WithoutAssociations().Update(&got)

Without this call, Update keeps its historical recursive-save behaviour (and logs a WARN naming the associations it is about to write).

type QueryEvent

type QueryEvent struct {
	SQL      string
	Args     []any
	Duration time.Duration
	Rows     int64
	Error    error
	Table    string
	// Operation is how the statement was sent: "SELECT", "SELECT (stream)",
	// "SELECT (cursor)" and "PRELOAD" for the query builder's reads,
	// "QUERY_ROW", "EXEC" and "QUERY" for a statement sent through
	// QueryRowContext, ExecContext or QueryContext, and "RAW_QUERY" and
	// "RAW_EXEC" for Client.RawQuery and Client.Exec.
	Operation string
	// Kind is what the statement is for: query, exec, ddl, introspection,
	// savepoint or raw.
	Kind StatementKind
}

QueryEvent describes one statement Quark sent to the engine.

type QueryFunc

type QueryFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (*sql.Rows, error)

QueryFunc is the signature for SQL query functions used by middleware.

type QueryObserver

type QueryObserver interface {
	ObserveQuery(event QueryEvent)
}

QueryObserver is called after each statement Quark sends to the engine — the query builder's, schema work, introspection, savepoints and raw SQL alike (see StatementKind). Use it for logging, metrics, auditing, etc.

type QueryRowFunc

type QueryRowFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) *sql.Row

QueryRowFunc is the signature for SQL single-row query functions used by middleware.

type Range added in v1.15.0

type Range[T any] struct {
	Lower  T
	Upper  T
	Bounds string
	Empty  bool
}

Range is a bounded interval of T, the shape of PostgreSQL's range types. Bounds is the two-character bound spec — "[)" (the zero value, and PostgreSQL's canonical form), "[]", "(]" or "()" — and Empty marks the empty range.

On PostgreSQL a Range[time.Time] column is TSTZRANGE, Range[int64] is INT8RANGE, Range[int32] INT4RANGE and Range[float64] NUMRANGE, and the value travels in the engine's range literal; elsewhere the column is the dialect's JSON type and the value a JSON object. Containment is asked with the `@>` / `<@` operators, on PostgreSQL only.

func (Range[T]) PGLiteral added in v1.15.0

func (r Range[T]) PGLiteral() (string, error)

PGLiteral renders the PostgreSQL range literal: `[lower,upper)`.

func (*Range[T]) Scan added in v1.15.0

func (r *Range[T]) Scan(src any) error

Scan accepts the JSON object the non-PostgreSQL engines store and the range literal PostgreSQL returns.

func (Range[T]) Value added in v1.15.0

func (r Range[T]) Value() (driver.Value, error)

Value implements driver.Valuer as JSON, the form every engine but PostgreSQL stores; the bind path substitutes the range literal there.

type RelationMeta

type RelationMeta = schema.RelationMeta

RelationMeta is the metadata for a model relation.

type ReplicaStrategy added in v1.0.0

type ReplicaStrategy int

ReplicaStrategy selects which healthy read replica serves a routed read when more than one is configured (F6-5, ADR-0015). Set it with WithReplicaStrategy; the zero value is ReplicaRoundRobin. Every strategy honours the F6-6 health cooldown — a replica taken out of rotation by [Client.markReplicaDown] is never chosen until its cooldown expires.

const (
	// ReplicaRoundRobin spreads reads evenly by advancing an atomic cursor one
	// slot per read. Default (zero value); the most predictable distribution
	// under steady concurrency.
	ReplicaRoundRobin ReplicaStrategy = iota
	// ReplicaRandom picks a replica at random per read — uniform across all
	// replicas when they are healthy, and a replica in cooldown is skipped.
	// Needs no shared cursor, so it avoids the single contended atomic of
	// round-robin at the cost of a less even short-term distribution.
	ReplicaRandom
	// ReplicaLeastConn picks the healthy replica with the fewest in-use pool
	// connections (sql.DB.Stats().InUse) at selection time. Best when replica
	// query latencies are uneven, since it steers new reads toward the least
	// busy pool; it reads each replica's Stats per call (a cheap mutex-guarded
	// snapshot).
	ReplicaLeastConn
)

type Result added in v0.6.0

type Result = quarkdriver.Result

Result mirrors database/sql.Result for the lock implementations.

type Routine

type Routine[T any] struct {
	// contains filtered or unexported fields
}

Routine is a builder for executing database functions and stored procedures that return results (table-valued functions or scalar functions).

func NewRoutine

func NewRoutine[T any](ctx context.Context, provider ClientProvider, routine string, args ...any) *Routine[T]

NewRoutine creates a new Routine builder for the given procedure/function.

func (*Routine[T]) First

func (r *Routine[T]) First() (T, error)

First executes the routine and returns the first row.

func (*Routine[T]) List

func (r *Routine[T]) List() ([]T, error)

List executes the routine and maps the resulting rows to a slice of T.

func (*Routine[T]) Scalar

func (r *Routine[T]) Scalar() (T, error)

Scalar executes a routine and returns a single scalar value. It is a convenient alias for First() when T is a primitive type.

type Row added in v0.6.0

type Row = quarkdriver.Row

Row mirrors *database/sql.Row for the lock implementations (Scan only).

type SQLGuard

type SQLGuard = guard.SQLGuard

SQLGuard re-exports the internal guard.SQLGuard. It provides SQL injection prevention utilities for Quark ORM.

func NewSQLGuard

func NewSQLGuard() *SQLGuard

NewSQLGuard creates a new SQLGuard with default settings.

type SQLiteDialect

type SQLiteDialect struct {
	// contains filtered or unexported fields
}

SQLiteDialect implements the SQLite dialect.

func (*SQLiteDialect) AlterTableAddColumn

func (s *SQLiteDialect) AlterTableAddColumn(table, column, dataType string) string

func (*SQLiteDialect) AlterTableAlterColumn

func (s *SQLiteDialect) AlterTableAlterColumn(table, column, newDataType string) string

func (*SQLiteDialect) AlterTableDropColumn

func (s *SQLiteDialect) AlterTableDropColumn(table, column string) string

func (*SQLiteDialect) AutoIncrementColumn added in v1.16.0

func (*SQLiteDialect) AutoIncrementColumn() (string, string)

AutoIncrementColumn is INTEGER PRIMARY KEY AUTOINCREMENT, whose catalog type is INTEGER: only INTEGER PRIMARY KEY aliases SQLite's rowid.

func (*SQLiteDialect) BoolLiteral added in v1.16.0

func (*SQLiteDialect) BoolLiteral(v bool) string

BoolLiteral is 1 or 0.

func (*SQLiteDialect) BuildProcedureCall

func (s *SQLiteDialect) BuildProcedureCall(procedure string, argCount int) string

func (*SQLiteDialect) BuildRoutineQuery

func (s *SQLiteDialect) BuildRoutineQuery(routine string, argCount int) string

func (*SQLiteDialect) ColumnType added in v1.16.0

func (*SQLiteDialect) ColumnType(s quarkdriver.ColumnSpec) string

ColumnType is SQLite's type for a column of the given kind.

func (*SQLiteDialect) CurrentTimestamp

func (s *SQLiteDialect) CurrentTimestamp() string

func (*SQLiteDialect) IntrospectSchema added in v0.6.0

func (d *SQLiteDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)

IntrospectSchema reads the SQLite schema using `sqlite_master` for the table list and `PRAGMA table_info(<table>)` for the column metadata of each table. This avoids parsing the CREATE TABLE DDL, which would be brittle.

SQLite caveats handled here:

  • System tables (`sqlite_*`) and quark internal tables (`quark_*`) are filtered out. The diff layer doesn't need to reason about them.
  • SQLite's PRAGMA returns columns in declaration order. We preserve that order (Tables is sorted alphabetically; Columns aren't re-sorted within a table).
  • The `dflt_value` column from PRAGMA table_info comes back as a literal SQL fragment (`'draft'`, `0`, `CURRENT_TIMESTAMP`); we pass it through unchanged in `Column.Default`.

func (*SQLiteDialect) JSONExtract

func (s *SQLiteDialect) JSONExtract(column, path string) (string, []any, error)

func (*SQLiteDialect) LastInsertIDQuery

func (s *SQLiteDialect) LastInsertIDQuery(table, pkColumn string) string

func (*SQLiteDialect) LimitOffset

func (s *SQLiteDialect) LimitOffset(limit, offset int) string

func (*SQLiteDialect) LockSuffix added in v0.4.0

func (s *SQLiteDialect) LockSuffix(opts LockOptions) (string, string, error)

SQLite has no row-level pessimistic-lock primitive — locking is transaction-scoped via BEGIN IMMEDIATE / EXCLUSIVE. Return ErrUnsupportedFeature so callers can branch by dialect or fall back.

func (*SQLiteDialect) Name

func (d *SQLiteDialect) Name() string

func (*SQLiteDialect) Placeholder

func (s *SQLiteDialect) Placeholder(index int) string

func (*SQLiteDialect) Placeholders

func (s *SQLiteDialect) Placeholders(n int) []string

func (*SQLiteDialect) Quote

func (s *SQLiteDialect) Quote(identifier string) string

func (*SQLiteDialect) RebuildsTables added in v1.16.0

func (*SQLiteDialect) RebuildsTables() bool

RebuildsTables reports true: SQLite has no ALTER COLUMN, no ADD CONSTRAINT and no DROP CONSTRAINT, so ApplyPlan changes a column and adds or drops a foreign key or a CHECK by rebuilding the table (migrate_alter.go).

func (*SQLiteDialect) RenameColumn

func (s *SQLiteDialect) RenameColumn(table, oldName, newName string) string

func (*SQLiteDialect) RenameTable

func (s *SQLiteDialect) RenameTable(oldName, newName string) string

func (*SQLiteDialect) Returning

func (s *SQLiteDialect) Returning(columns ...string) string

func (*SQLiteDialect) SupportsLastInsertID

func (s *SQLiteDialect) SupportsLastInsertID() bool

func (*SQLiteDialect) SupportsReturning

func (s *SQLiteDialect) SupportsReturning() bool

func (*SQLiteDialect) SupportsTransactionalDDL

func (s *SQLiteDialect) SupportsTransactionalDDL() bool

func (*SQLiteDialect) UpsertSQL

func (s *SQLiteDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string

UpsertSQL for SQLite: ON CONFLICT (cols) DO UPDATE SET col = excluded.col

type SavepointDialect added in v1.1.0

type SavepointDialect = quarkdriver.SavepointDialect

SavepointDialect is the optional Dialect extension for engines whose savepoint statements diverge from the ANSI form (SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT); the transaction layer (Tx.Savepoint, Tx.RollbackTo, Tx.ReleaseSavepoint) asserts it on the dialect. It is declared, and documented, in quarkdriver (ADR-0026); this name is an alias of the same type.

type ScatterMerge added in v1.2.0

type ScatterMerge[T any] struct {
	// Less, if set, merges the per-shard rows into a globally ordered slice
	// (reports whether a sorts before b). Apply the SAME OrderBy in the per-shard
	// query (build) so each shard returns its own rows sorted; Less then orders
	// across shards.
	Less func(a, b T) bool
	// Limit, if > 0, caps the merged result to the first Limit rows AFTER
	// ordering — the global top-N. For a correct global top-N also Limit(N) the
	// per-shard query in build, so each shard returns its own N candidates and
	// the merge keeps the N best overall.
	Limit int
}

ScatterMerge controls how ScatterGather merges the per-shard results. The zero value (no Less, Limit 0) concatenates every shard's rows in shard-name order with no global cap.

type Schema added in v0.6.0

type Schema = quarkdriver.Schema

Schema is the dialect-neutral representation of a database schema. Tables are sorted by Name for deterministic ordering.

type SchemaIntrospector added in v0.6.0

type SchemaIntrospector = quarkdriver.SchemaIntrospector

SchemaIntrospector is the optional Dialect interface for retrieving the current schema from the database; Client.IntrospectSchema, Client.PlanMigration and Client.Sync assert it on the dialect.

type Scope

type Scope[T any] func(*Query[T]) *Query[T]

Scope is a reusable query modifier — a function that receives and returns a *Query[T]. Scopes can be composed via Apply().

type ShardFunc added in v1.0.0

type ShardFunc func(shardKey string) string

ShardFunc maps a shard-key value to the name of the shard that owns it. It is the partitioning policy and the resharding seam — pluggable: hash-mod (HashShardFunc, the default helper), range, geo, or a lookup table.

func HashShardFunc added in v1.0.0

func HashShardFunc(shardNames []string) ShardFunc

HashShardFunc returns a ShardFunc that maps a key to one of shardNames by FNV-1a hash modulo the shard count — a stable, uniform default assignment. The names are copied, so later mutation of the caller's slice does not change routing. With no names it maps everything to "" (GetClient then errors).

type ShardKeyer added in v1.2.0

type ShardKeyer interface {
	// ShardKey returns the shard-key value for this entity. An empty string is
	// "no key": routing then fails like a missing [WithShardKey], rather than
	// silently fanning out across shards.
	ShardKey() string
}

ShardKeyer is implemented by a model that owns its shard key. It is the model hook ADR-0016 anticipated for entity-based routing: the entity returns the value its partition is keyed on (its tenant id, user id, region, …), so the key-deriving logic lives on the model instead of being repeated at every call site. Reads, which carry no entity, keep using WithShardKey directly — the context stays the uniform routing mechanism; this only populates it from an entity. Implement it on the value or pointer you pass to WithShardKeyOf:

func (u User) ShardKey() string { return u.TenantID }

type ShardResolver added in v1.0.0

type ShardResolver func(ctx context.Context) string

ShardResolver extracts the shard key from a context, returning "" when none is present. Use DefaultShardResolver with WithShardKey, or supply your own (e.g. reading an existing request value).

type ShardRouter added in v1.0.0

type ShardRouter struct {
	// contains filtered or unexported fields
}

ShardRouter routes each query to the Client of the shard that owns the query's shard key (resolved from context). A query without a shard key in context is an error — there is no implicit cross-shard fan-out.

func NewShardRouter added in v1.0.0

func NewShardRouter(shards map[string]*Client, resolve ShardResolver, shardFor ShardFunc) (*ShardRouter, error)

NewShardRouter builds a router over a fixed set of shards (name -> Client). resolve extracts the shard key from context; shardFor maps that key to a shard name. It errors if shards is empty or either function is nil — those are setup mistakes worth catching at construction.

func (*ShardRouter) GetClient added in v1.0.0

func (r *ShardRouter) GetClient(ctx context.Context) (*Client, error)

GetClient implements ClientProvider: it resolves the shard key from ctx, maps it to a shard, and returns that shard's Client. It errors when no shard key is in context (no implicit fan-out) or when the mapped shard is unknown.

func (*ShardRouter) ShardNames added in v1.0.0

func (r *ShardRouter) ShardNames() []string

ShardNames returns the registered shard names (unspecified order). Useful for migrating/onboarding every shard. The Clients themselves are not exposed — route through For[T] so the shard-key discipline is preserved.

type StatementKind added in v1.17.0

type StatementKind string

StatementKind says what a statement Quark sends to the engine is for. Every QueryEvent carries one, and a Middleware reads it with StatementKindFromContext, so a consumer can tell the query builder's reads and writes from schema work, savepoints and SQL the caller wrote.

Query and exec are told apart by the database/sql method the statement goes through, the same line Middleware's WrapQuery/WrapQueryRow and WrapExec draw; the other four by what the statement is for, whatever the method.

const (
	// StatementQuery is a statement the query builder, a [Routine] or
	// [Client.Backfill] sends through QueryContext or QueryRowContext: a
	// SELECT, a Count or an aggregate, a preload, the check a guarded write
	// reads first, and a write that reads its result back — INSERT …
	// RETURNING, SQL Server's OUTPUT, the key a Create reads after its
	// INSERT.
	StatementQuery StatementKind = "query"
	// StatementExec is a statement the query builder sends through
	// ExecContext — INSERT, UPDATE, DELETE, an upsert, a batch — and a
	// stored-procedure [Call], [Notify], an audit row, and the tenant
	// variable a RowLevelSecurityNative transaction sets when it opens.
	StatementExec StatementKind = "exec"
	// StatementDDL is schema work: the CREATE, ALTER and DROP of
	// [Client.Migrate], [Client.Sync], [Client.ApplyPlan],
	// [Client.CreateIndex] and [Client.AddForeignKey], and the statements
	// that serve them — the migration lock, the writes to Quark's
	// bookkeeping tables (quark_migration_state, quark_backfill_state,
	// quark_migrations), and the PRAGMAs and row copy of SQLite's table
	// rebuild. The RLS policies quarktenant installs are DDL too.
	StatementDDL StatementKind = "ddl"
	// StatementIntrospection is a read of what the schema is: the catalog
	// queries of a dialect's [SchemaIntrospector] under
	// [Client.IntrospectSchema], [Client.PlanMigration] and [Client.Sync],
	// the catalog reads of ApplyPlan and of the RowLevelSecurityNative
	// policy checks, the reads of Quark's bookkeeping tables, and the server
	// version a client on the mysql driver reads to tell MariaDB apart.
	StatementIntrospection StatementKind = "introspection"
	// StatementSavepoint is a SAVEPOINT, ROLLBACK TO SAVEPOINT or RELEASE
	// SAVEPOINT: [Tx.Savepoint], [Tx.RollbackTo], [Tx.ReleaseSavepoint] and
	// the nested [Tx.Tx].
	StatementSavepoint StatementKind = "savepoint"
	// StatementRaw is SQL the caller wrote: [Client.Exec] and
	// [Client.RawQuery].
	StatementRaw StatementKind = "raw"
)

func StatementKindFromContext added in v1.17.0

func StatementKindFromContext(ctx context.Context) StatementKind

StatementKindFromContext returns the kind of the statement a Middleware is wrapping: Quark puts it on the context it hands the chain. It returns "" for a context that did not come through Quark's chain — a middleware function called directly from a test, for instance.

type StrictReadsMode added in v1.4.0

type StrictReadsMode int

StrictReadsMode selects how much enforcement WithStrictReads applies to unbounded read entrypoints (#247). The zero value is StrictReadsOff.

const (
	// StrictReadsOff disables strict-read enforcement — the default and
	// the historical behaviour. The only cost the feature leaves on the
	// read path in this mode is one integer comparison per call: no
	// allocation, no context lookup.
	StrictReadsOff StrictReadsMode = iota

	// StrictReadsWarn logs a structured WARN — through the same client
	// logger that already announces List() without Limit() — when Iter()
	// or Cursor() run without an explicit Limit() and without
	// [Query.AllowUnbounded]. It also enables N+1 detection inside
	// contexts prepared with [TrackReads].
	StrictReadsWarn

	// StrictReadsReject upgrades the unbounded Iter()/Cursor() WARN to an
	// [ErrInvalidQuery] error. N+1 detection stays WARN-only: it is a
	// heuristic (a threshold over a per-context counter), and a heuristic
	// must never fail an otherwise valid query.
	StrictReadsReject
)

type Subquery added in v0.4.0

type Subquery struct {
	// contains filtered or unexported fields
}

Subquery is a rendered SELECT that can be embedded inside another query through the Expr AST (Sub, Exists, NotExists, InSub, NotInSub).

A subquery is built like any other query — `For[T](...).Where(...)` — and then captured with `AsSubquery()`. The capture eagerly renders the SELECT using the active dialect's identifier quoting but with `?` as the bind marker, so the outer query's `buildWhereClause` can swap each `?` for the dialect's placeholder syntax at the correct arg index when the AST is rendered.

This contract matches the rest of the AST: leaves emit `?`, `substitutePathMarkers` does the placeholder rewrite, args are threaded through `condition.extraArgs`. So a subquery is just another Expr leaf.

func (*Subquery) SQL added in v0.4.0

func (s *Subquery) SQL() (string, []any)

SQL returns the captured SELECT fragment with '?' bind markers and the args slice. Exposed for test introspection — production code should use the Expr wrappers (Sub, Exists, etc.) which compose through the AST.

type SyncOptions

type SyncOptions struct {
	DryRun        bool // If true, logs the SQL but doesn't execute it.
	NoTransaction bool // If true, doesn't wrap the sync in a transaction.
}

SyncOptions configures the behavior of the Sync operation.

type Table added in v0.6.0

type Table = quarkdriver.Table

Table represents one table in the schema.

type TableNamer added in v1.17.0

type TableNamer = schema.TableNamer

TableNamer is implemented by a model that names its own table:

TableName() string

Without it, a model's table is its type name in snake case, pluralised (User → users). Quark asks once per model type, on a zero *T, and caches the answer in the model's metadata, which is where Migrate and the query builder read the table from: the method may have a value or a pointer receiver, and must not depend on the value's fields.

The type is the one Quark asserts against, not a copy of it, so a model can check that it satisfies the convention at compile time:

var _ quark.TableNamer = User{}

type TenantConfig

type TenantConfig struct {
	Strategy       TenantStrategy
	MaxCachedPools int     // Maximum number of DB connection pools to keep open (for DatabasePerTenant)
	BaseClient     *Client // Used for SchemaPerTenant, RowLevelSecurityClient and RowLevelSecurityNative
	TenantColumn   string  // Column name for RowLevelSecurityClient, default is "tenant_id"

	// SkipPolicyVerification turns off the check a RowLevelSecurityNative
	// router runs the first time it touches each table: that row-level
	// security is enabled on it and at least one policy exists (A8 S8). Off
	// by default, the router refuses with ErrRLSNotEnforced instead of
	// serving every tenant's rows over a table whose policies were never
	// installed. Set it when policies are managed outside Quark's catalog
	// view — a test that measures something else, a schema the connecting
	// role cannot inspect — and run quarktenant.VerifyRLSPolicies yourself.
	SkipPolicyVerification bool

	// NativeRLSVar is the PostgreSQL session variable name used by
	// RowLevelSecurityNative to carry the resolved tenant ID. Each
	// query under a Native router is wrapped in a transaction that
	// calls `set_config(NativeRLSVar, <tenantID>, true)` before
	// executing; the `CREATE POLICY` clauses installed by
	// `quark tenant install-rls-policies` (F5-3) reference the same
	// variable.
	//
	// Defaults to "app.tenant_id". Must be a valid PostgreSQL
	// configuration parameter name (lowercase, dot-namespaced).
	// Ignored when Strategy is not RowLevelSecurityNative.
	NativeRLSVar string
}

TenantConfig configures the TenantRouter.

func DefaultTenantConfig

func DefaultTenantConfig() TenantConfig

DefaultTenantConfig provides sensible defaults.

type TenantRouter

type TenantRouter struct {
	// contains filtered or unexported fields
}

TenantRouter manages dynamic database connections or queries for different tenants.

func NewTenantRouter

func NewTenantRouter(
	config TenantConfig,
	resolver func(ctx context.Context) string,
	factory func(tenantID string) (*Client, error),
) *TenantRouter

NewTenantRouter creates a new router for multi-tenant database access.

func (*TenantRouter) ActiveTenants

func (r *TenantRouter) ActiveTenants() []string

ActiveTenants returns a list of active tenant connections in the cache.

func (*TenantRouter) GetClient

func (r *TenantRouter) GetClient(ctx context.Context) (*Client, error)

GetClient resolves the tenant ID from the context and returns the corresponding Client. It implements the ClientProvider interface so it can be used with For[T].

func (*TenantRouter) ResolveTenant

func (r *TenantRouter) ResolveTenant(ctx context.Context) (string, error)

ResolveTenant returns the tenant ID for the context.

func (*TenantRouter) Tx added in v0.9.0

func (r *TenantRouter) Tx(ctx context.Context, fn func(tx *Tx) error) error

Tx opens a single transaction for the tenant the context resolves, confines it, and invokes fn(tx). On nil return from fn the transaction commits; on error it rolls back.

Under RowLevelSecurityNative the transaction is opened on the BaseClient and `set_config('<NativeRLSVar>', <resolvedTenantID>, true)` runs as its first statement, so the engine's policies filter everything fn runs. This is the recommended entry point under Native — it avoids the per-query implicit-tx overhead and the connection-hold semantics described on nativeRLSExecutor. Under the other strategies (DatabasePerTenant / SchemaPerTenant / RowLevelSecurityClient) no variable is emitted: the transaction is opened on the tenant's client and the confinement travels with the *Tx.

Every strategy: the *Tx handed to fn carries its tenant, and a query built with ForTx inside fn is confined to it — the schema prefix, the predicate, or the engine's policy — the same way For confines one built from the router. The transaction's tenant rules: a query context inside fn that resolves a different tenant fails with ErrTenantMismatch, and one that resolves none inherits the transaction's (ADR-0025). Before that, fn got a bare *Tx for every strategy but Native, and a query inside it read every tenant's rows (QK-26).

Returns ErrUnsupportedFeature wrapped with the dialect name when the BaseClient is not PostgreSQL under RowLevelSecurityNative.

type TenantStrategy

type TenantStrategy int

TenantStrategy defines how multi-tenancy is handled.

const (
	// DatabasePerTenant uses a separate database connection pool per tenant.
	// This requires an LRU cache to prevent connection exhaustion.
	DatabasePerTenant TenantStrategy = iota
	// SchemaPerTenant uses a single database connection pool but prefixes
	// the table name with the tenant ID (e.g. "tenant_acme.users").
	SchemaPerTenant
	// RowLevelSecurityClient uses a single database connection pool and
	// injects a "WHERE tenant_id = ?" predicate into every query the
	// builder constructs. This is **client-side tenant scoping**, not
	// engine-enforced Row-Level Security: `client.Raw()` and `client.Exec()`
	// bypass the predicate. See ADR-0012 and `docs/playbooks/tenant.md` for
	// the limitations. On PostgreSQL, prefer RowLevelSecurityNative for
	// engine-enforced isolation. On other engines this remains the only
	// row-level option.
	RowLevelSecurityClient
	// RowLevelSecurityNative delegates row-level isolation to the
	// database engine via PostgreSQL row-level security policies. Each
	// query is wrapped in a transaction that first calls
	// `set_config('app.tenant_id', <tenantID>, true)` (i.e., SET LOCAL);
	// `CREATE POLICY` clauses on each tenant-scoped table reference that
	// session variable to filter rows. Unlike RowLevelSecurityClient,
	// `client.Raw()` / `client.Exec()` are still filtered: the policy
	// runs server-side and returns zero rows when `app.tenant_id` is not
	// set on the current transaction — there is no client-side bypass.
	//
	// A structured warning for Raw/Exec callers under a Native router
	// context is deferred to a follow-up (TASKS.md F5-2 closure block).
	// The engine enforcement is the security boundary; the warning would
	// be a developer-experience cue, not a safety net.
	//
	// Native is PostgreSQL-only. Constructing a Query[T] under a Native
	// router with a non-PostgreSQL dialect returns ErrUnsupportedFeature.
	//
	// See ADR-0012 §"Cómo se ejecuta SET LOCAL por query" for the
	// rationale and the F5-3 CLI for the DDL generator.
	RowLevelSecurityNative
)

type Tracked added in v0.3.0

type Tracked[T any] struct {
	// Entity is the loaded value. Mutate fields on it directly; Save will
	// detect the changes and write only those columns.
	Entity *T
	// contains filtered or unexported fields
}

Tracked wraps a loaded entity with a snapshot of its column values, so a later Save can emit an UPDATE limited to the fields that actually changed.

Active Record + dirty tracking ligero (Phase 1): the snapshot lives on the wrapper, not in the Client, so there is no shared map to grow or evict. Each Tracked carries the metadata it needs (client, table, dialect, pk, meta) to run Save without the caller threading state.

Tracked is the permanent fix for P0-4: a bool / int / string / pointer can be set to its zero value and Save will write it because the diff is taken against the snapshot, not against "is this field non-zero?".

func (*Tracked[T]) Changed added in v0.3.0

func (t *Tracked[T]) Changed() []string

Changed reports the names (db tags) of columns whose value differs between Entity now and the snapshot taken at load time. Useful for tests and observability; Save uses the same comparison internally.

func (*Tracked[T]) Save added in v0.3.0

func (t *Tracked[T]) Save(ctx context.Context) (int64, error)

Save updates the row identified by Entity's primary key, writing only the columns whose value differs from the load-time snapshot. If nothing changed, Save returns (0, nil) without touching the database.

Returns the number of rows affected. Refreshes the internal snapshot on success so subsequent Save calls diff against the new state.

type TrackedQuery added in v0.3.0

type TrackedQuery[T any] struct {
	// contains filtered or unexported fields
}

TrackedQuery is the lightweight wrapper Query[T].Track() returns. It re-issues Find/First/List on the underlying query and wraps each loaded entity with a snapshot for later dirty-tracked Save.

func (*TrackedQuery[T]) Find added in v0.3.0

func (tq *TrackedQuery[T]) Find(id any) (*Tracked[T], error)

Find loads a single entity by primary key and wraps it in a Tracked.

func (*TrackedQuery[T]) First added in v0.3.0

func (tq *TrackedQuery[T]) First() (*Tracked[T], error)

First returns the first matching entity wrapped in a Tracked.

func (*TrackedQuery[T]) List added in v0.3.0

func (tq *TrackedQuery[T]) List() ([]*Tracked[T], error)

List loads all matching entities wrapped in Tracked values.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx wraps *sql.Tx and provides transactional query execution. It shares dialect, guard, observers, and limits from the parent Client.

func TxFromContext added in v0.9.0

func TxFromContext(ctx context.Context) *Tx

TxFromContext returns the *Tx the context is currently scoped to, or nil when the context was not enriched by a ForTx call. Note that this includes the Client.Tx callback body itself: that callback receives the *Tx as a parameter, and the bare ctx it captures is NOT enriched until it flows through ForTx[T]. The helper is therefore meant for lifecycle hooks (which only get a context), not for the Tx callback body (which already has the tx).

The primary use is inside a lifecycle hook: the hook interfaces (BeforeCreateHook, AfterUpdateHook, …) receive only a context, so a hook that needs to register a commit/rollback side-effect reaches the transaction through this helper:

func (o *Order) AfterCreate(ctx context.Context) error {
    if tx := quark.TxFromContext(ctx); tx != nil {
        tx.OnCommit(func(ctx context.Context) error {
            return bus.Publish(ctx, OrderCreated{ID: o.ID})
        })
    }
    return nil
}

Returning nil is the normal case for non-transactional CRUD; the caller must nil-check before use.

func (*Tx) Commit

func (t *Tx) Commit() error

Commit commits the transaction.

When commit succeeds, any model After* hooks queued during the transaction lifetime are drained in FIFO order. A hook returning an error is logged via the Client's slog logger (event: `quark.hook.after_post_commit_error`) but does NOT block the remaining hooks or fail the Commit — once the database confirmed the commit, no application-level handler can undo it (ADR-0013 Regla 2). Commit failures surface the underlying database error and the queued hooks are discarded.

With a cache store (WithCacheStore), a successful commit drops again, in one call and before the hooks run, the cache tags the transaction's writes dropped when they ran: a read from outside the transaction between a write and the commit caches what was committed then, and would otherwise outlive the commit.

The context each After* hook receives is the context the originating Query[T] captured at construction time (the ctx passed to ForTx). If the caller installed a deadline on that context via context.WithTimeout inside the Client.Tx callback, the hook may observe an already-expired ctx after Commit returns. Hooks that need a fresh context can derive one from context.Background inside their implementation.

func (*Tx) OnCommit added in v0.9.0

func (t *Tx) OnCommit(fn func(context.Context) error)

OnCommit registers a callback to run after the transaction commits successfully. Callbacks fire in FIFO registration order, after the model After* hooks, with the transaction's context. A callback returning an error is logged via the Client's slog logger (event `quark.hook.on_commit_error`) but does NOT block the remaining callbacks — once the database has confirmed the commit, no application-level handler can undo it (ADR-0013 Regla 3).

If the transaction rolls back instead, registered OnCommit callbacks are discarded without firing.

Registering an OnCommit callback from inside another OnCommit callback (i.e. during the drain) is a no-op for the current commit: the queue was already lifted before the drain began, so the newly-registered fn will not fire. This prevents unbounded re-entrancy.

Example:

err := client.Tx(ctx, func(tx *quark.Tx) error {
    if err := quark.ForTx[Order](ctx, tx).Create(o); err != nil {
        return err
    }
    tx.OnCommit(func(ctx context.Context) error {
        return bus.Publish(ctx, OrderCreated{ID: o.ID})
    })
    return nil
})

func (*Tx) OnRollback added in v0.9.0

func (t *Tx) OnRollback(fn func(context.Context) error)

OnRollback registers a callback to run after the transaction rolls back. Callbacks fire in FIFO registration order with the transaction's context. A callback returning an error is logged via the Client's slog logger (event `quark.hook.on_rollback_error`) but does NOT block the remaining callbacks.

If the transaction commits instead, registered OnRollback callbacks are discarded without firing.

Example:

tx.OnRollback(func(ctx context.Context) error {
    log.Warn("order create rolled back", "id", o.ID)
    return nil
})

func (*Tx) ReleaseSavepoint

func (t *Tx) ReleaseSavepoint(name string) error

ReleaseSavepoint releases the named savepoint. Hooks queued since it was set stay on the transaction queues — released work is part of the surrounding transaction and its side-effects fire with it.

func (*Tx) Rollback

func (t *Tx) Rollback() error

Rollback aborts the transaction.

All model After* hooks and OnCommit callbacks queued during the transaction lifetime are discarded without firing — rolled-back work never triggers the side-effects that would have followed it (ADR-0013 Regla 2). OnRollback callbacks DO fire, in FIFO order, after the underlying rollback is attempted, so callers can react to the abort (release a reservation, emit a "cancelled" event, etc.). An OnRollback callback returning an error is logged but does not block the rest of the chain.

func (*Tx) RollbackTo

func (t *Tx) RollbackTo(name string) error

RollbackTo rolls back to the named savepoint. Beyond undoing the SQL written since the savepoint, it discards the After*/OnCommit/ OnRollback hooks queued in that window so the rolled-back work does not trigger its side-effects on the eventual commit (ADR-0013 Regla 2). Reactions to the partial rollback flow through the error the nested scope returns, not through OnRollback (which fires only on a whole-transaction rollback).

func (*Tx) Savepoint

func (t *Tx) Savepoint(name string) error

Savepoint creates a savepoint with the given name. Hooks queued by CRUD run between this call and a matching Tx.RollbackTo are unwound by that rollback (see Tx.RollbackTo).

func (*Tx) Tx

func (t *Tx) Tx(ctx context.Context, fn func(tx *Tx) error) error

type TypeMapper added in v0.3.0

type TypeMapper = migrate.TypeMapper

TypeMapper produces a dialect-specific SQL type for a Go type. The caller supplies the dialect name (lower-case: "postgres", "mysql", "mariadb", "sqlite", "mssql", "oracle") and the sizing hints from the field's tag.

type TypeOptions added in v0.3.0

type TypeOptions = migrate.TypeOptions

TypeOptions carries the SQL-type sizing hints parsed from a struct's db tag — `size=N`, `precision=N`, `scale=N` — plus a flag indicating whether the column is the primary key. A zero value for any field means "use the dialect default for the Go type".

type TypedBinder added in v0.11.0

type TypedBinder func(entity any, mode BindMode) (cols []string, args []any, err error)

TypedBinder returns the column names and bind arguments for entity — a non-nil pointer to the model value (*T) — for the given mode, without reflection. Implemented by generated code and registered via RegisterTypedBinder. Not intended for hand use.

type TypedColumn added in v0.12.0

type TypedColumn[T any] struct {
	// contains filtered or unexported fields
}

TypedColumn is a typed handle to one model column, parameterised by the Go type of the underlying field. Generated code constructs it via NewTypedColumn; its methods produce [Predicate]s whose value argument is typed T, so passing a value of the wrong type for the column is a compile error.

func NewTypedColumn added in v0.12.0

func NewTypedColumn[T any](name string) TypedColumn[T]

NewTypedColumn builds a typed column handle for the given SQL column name. It is intended to be called only from generated code, where name is the model's `db` tag column.

func (TypedColumn[T]) Between added in v0.12.0

func (c TypedColumn[T]) Between(lo, hi T) Predicate

Between builds `column BETWEEN lo AND hi`.

func (TypedColumn[T]) Eq added in v0.12.0

func (c TypedColumn[T]) Eq(v T) Predicate

Eq builds `column = value`.

func (TypedColumn[T]) Gt added in v0.12.0

func (c TypedColumn[T]) Gt(v T) Predicate

Gt builds `column > value`.

func (TypedColumn[T]) Gte added in v0.12.0

func (c TypedColumn[T]) Gte(v T) Predicate

Gte builds `column >= value`.

func (TypedColumn[T]) In added in v0.12.0

func (c TypedColumn[T]) In(values ...T) Predicate

In builds `column IN (values...)`. Pass at least one value: with none it lowers to the same empty-IN condition as the string WhereIn — which SQLite treats as matching nothing but PostgreSQL, Oracle, and SQL Server reject as invalid SQL. (WhereP is a faithful lowering of the string API, so it does not paper over that engine difference.)

func (TypedColumn[T]) IsNotNull added in v0.12.0

func (c TypedColumn[T]) IsNotNull() Predicate

IsNotNull builds `column IS NOT NULL`.

func (TypedColumn[T]) IsNull added in v0.12.0

func (c TypedColumn[T]) IsNull() Predicate

IsNull builds `column IS NULL`.

func (TypedColumn[T]) Lt added in v0.12.0

func (c TypedColumn[T]) Lt(v T) Predicate

Lt builds `column < value`.

func (TypedColumn[T]) Lte added in v0.12.0

func (c TypedColumn[T]) Lte(v T) Predicate

Lte builds `column <= value`.

func (TypedColumn[T]) Name added in v0.12.0

func (c TypedColumn[T]) Name() string

Name returns the SQL column name the handle refers to.

func (TypedColumn[T]) Neq added in v0.12.0

func (c TypedColumn[T]) Neq(v T) Predicate

Neq builds `column != value`.

func (TypedColumn[T]) NotIn added in v0.12.0

func (c TypedColumn[T]) NotIn(values ...T) Predicate

NotIn builds `column NOT IN (values...)`. Pass at least one value — see In for the empty-list caveat.

type TypedScanner added in v0.11.0

type TypedScanner func(rows *sql.Rows, dest any) error

TypedScanner scans the current row of rows into dest — a non-nil pointer to the model value (*T) — without reflection. It is implemented by generated code and registered via RegisterTypedScanner. Not intended for hand use.

type TypedStringColumn added in v0.12.0

type TypedStringColumn struct {
	TypedColumn[string]
}

TypedStringColumn is a TypedColumn[string] that additionally offers the text-only operators LIKE / NOT LIKE. The generator uses it for string-typed fields so pattern matching is offered only where it is meaningful, while every TypedColumn[string] method (Eq, In, …) remains available via embedding.

func NewTypedStringColumn added in v0.12.0

func NewTypedStringColumn(name string) TypedStringColumn

NewTypedStringColumn builds a typed string-column handle. Intended to be called only from generated code.

func (TypedStringColumn) Contains added in v1.15.0

func (c TypedStringColumn) Contains(text string) Predicate

Contains builds a "contains" search for text the user typed, escaped with EscapeLike — the typed counterpart of Query.WhereContains. This is the accessor a search box should call: a `%` the user typed is matched as a percent sign, not as a wildcard.

func (TypedStringColumn) EndsWith added in v1.15.0

func (c TypedStringColumn) EndsWith(text string) Predicate

EndsWith builds a suffix search for text the user typed; see TypedStringColumn.Contains.

func (TypedStringColumn) Like added in v0.12.0

func (c TypedStringColumn) Like(pattern string) Predicate

Like builds `column LIKE pattern`.

func (TypedStringColumn) LikeEscaped added in v1.15.0

func (c TypedStringColumn) LikeEscaped(pattern string) Predicate

LikeEscaped builds `column LIKE pattern` with the escape character declared on the statement — the typed counterpart of Query.WhereLike. TypedStringColumn.Like keeps the plain, engine-default form.

func (TypedStringColumn) NotLike added in v0.12.0

func (c TypedStringColumn) NotLike(pattern string) Predicate

NotLike builds `column NOT LIKE pattern`.

func (TypedStringColumn) NotLikeEscaped added in v1.15.0

func (c TypedStringColumn) NotLikeEscaped(pattern string) Predicate

NotLikeEscaped is the negation of TypedStringColumn.LikeEscaped.

func (TypedStringColumn) StartsWith added in v1.15.0

func (c TypedStringColumn) StartsWith(text string) Predicate

StartsWith builds a prefix search for text the user typed; see TypedStringColumn.Contains.

type Validator added in v1.17.0

type Validator interface {
	Validate(ctx context.Context) error
}

Validator is implemented by a model that checks itself before Quark writes it:

Validate(ctx context.Context) error

Client.Validate calls it first, with the context of the write, and then applies the `validate:"…"` struct tags. Query.Create, Query.CreateBatch, Query.Upsert and Query.UpsertBatch call Client.Validate on each entity before any SQL runs; a non-nil error aborts the write, wrapped as "validation failed: %w", so errors.Is and errors.As still find it. Update does not validate. Implement it on the pointer receiver, since Quark hands it the *T it is about to write.

Quark asserts against this type, so a model can check that it satisfies the convention at compile time:

var _ quark.Validator = (*Member)(nil)

type Window added in v0.4.0

type Window struct {
	// contains filtered or unexported fields
}

Window models the `OVER (...)` clause for a window-function expression. Build with NewWindow() and chain PartitionBy / OrderBy. The chain is immutable: each method returns a fresh copy so a Window definition can be reused across multiple Over() calls without aliasing.

func NewWindow added in v0.4.0

func NewWindow() *Window

NewWindow returns an empty Window. An empty Window renders as `OVER ()` — sometimes legitimate (e.g. `COUNT(*) OVER ()` for a running grand total), so it's not an error.

func (*Window) OrderBy added in v0.4.0

func (w *Window) OrderBy(col Expr, desc bool) *Window

OrderBy adds an order entry. Set desc=true for descending; the second argument is the conventional bool toggle to keep the API tight (no "ASC"/"DESC" stringly-typed argument).

func (*Window) PartitionBy added in v0.4.0

func (w *Window) PartitionBy(cols ...Expr) *Window

PartitionBy adds one or more partition expressions. Identifiers go through SQLGuard at render time — pass `Col("status")`, not the raw column string.

func (*Window) Range added in v1.14.0

func (w *Window) Range(start, end FrameBound) *Window

Range sets a RANGE frame. It counts in VALUES of the ORDER BY expression rather than in rows, so peer rows — those comparing equal — enter or leave the frame together.

func (*Window) Rows added in v1.14.0

func (w *Window) Rows(start, end FrameBound) *Window

Rows sets a ROWS frame: which rows of the partition the window function actually sees, counted in physical rows.

Without a frame, a window with an ORDER BY defaults to every row from the start of the partition through the current one. That default is what makes SUM() OVER (ORDER BY …) a running total — and it is also why a "moving average" written without a frame is silently a running average instead.

// three-row moving average
quark.NewWindow().OrderBy(quark.Col("day"), false).Rows(quark.Preceding(2), quark.CurrentRow())

Bounds come from Preceding, Following, CurrentRow, UnboundedPreceding and UnboundedFollowing. The offsets render as literals, not bind parameters: every engine requires a constant there.

Directories

Path Synopsis
cache
cmd
quark module
drivers
mssql module
mysql module
oracle module
postgres module
sqlite module
internal
db
guard
Package guard provides SQL injection prevention utilities for Quark ORM.
Package guard provides SQL injection prevention utilities for Quark ORM.
migrate
Package migrate provides internal utilities for database schema migrations.
Package migrate provides internal utilities for database schema migrations.
observe
Package observe lets the packages of this module that send statements on a *quark.Client's behalf, but do not live in package quark — the migrate package's ledger, quarktenant's policy install and check — send them through the client's execution seam, so they pass its middleware chain and reach its query observers like every statement package quark sends.
Package observe lets the packages of this module that send statements on a *quark.Client's behalf, but do not live in package quark — the migrate package's ledger, quarktenant's policy install and check — send them through the client's execution seam, so they pass its middleware chain and reach its query observers like every statement package quark sends.
schema
Package schema provides struct reflection and model metadata caching for Quark ORM.
Package schema provides struct reflection and model metadata caching for Quark ORM.
Package otel provides a Quark Middleware that emits OpenTelemetry tracing spans and metrics for every statement Quark sends to the engine — the query builder's, schema work, introspection, savepoints and raw SQL.
Package otel provides a Quark Middleware that emits OpenTelemetry tracing spans and metrics for every statement Quark sends to the engine — the query builder's, schema work, introspection, savepoints and raw SQL.
Package quarkdriver is the contract a database driver module implements to plug into Quark.
Package quarkdriver is the contract a database driver module implements to plug into Quark.
drivertest
Package drivertest is the conformance kit for Quark driver modules.
Package drivertest is the conformance kit for Quark driver modules.
drivertest/suite
Package suite is the engine conformance suite: what Quark checks on each of the six engines it ships, in a form a driver module outside this repository runs against its own engine.
Package suite is the engine conformance suite: what Quark checks on each of the six engines it ships, in a form a driver module outside this repository runs against its own engine.
Package quarkmigrate is the thin CLI wrapper that turns a quark.Client + a set of Go model values into a plan/verify/apply workflow.
Package quarkmigrate is the thin CLI wrapper that turns a quark.Client + a set of Go model values into a plan/verify/apply workflow.
Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow.
Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow.
Package quarktest is the minimal test kit for applications built on quark: a ready SQLite client with cleanup, one-line schema setup, and the transaction-per-test isolation pattern.
Package quarktest is the minimal test kit for applications built on quark: a ready SQLite client with cleanup, one-line schema setup, and the transaction-per-test isolation pattern.
Package seed is the registration point for database seeders — the seeding counterpart to package migrate.
Package seed is the registration point for database seeders — the seeding counterpart to package migrate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL