Skip to main content
Version: 1.11.0

Architecture

This page describes how a Quark call travels from quark.For[T] to a row in your struct. Read it when you want to know where an extension point sits — where middleware runs, when identifiers are validated, what the dialect owns — rather than how to call a specific method.

Everything is built from five primitives:

  • Client owns the database handle, dialect, middleware, observers, cache, and migration helpers.
  • Query[T] is the immutable typed builder for model operations.
  • Dialect isolates database-specific SQL details.
  • Middleware and hooks extend the execution path.
  • TenantRouter injects tenant-aware behavior.

Core principles

PrincipleImplementation
Type safetyFor[T] returns a query bound to a concrete model type.
ImmutabilityQuery methods clone builder state before returning the next query.
Database independenceDialects own placeholders, quoting, upserts, and DDL.
Guarded SQLSQLGuard validates identifiers, operators, and keywords.
Modular executionMiddleware, hooks, observers, and cache stores are injected through the client.

Request lifecycle

The same path in words:

  1. quark.For[T] parses model metadata and initializes a typed builder.
  2. Builder methods add query state by returning cloned builders.
  3. An endpoint such as List, Create, Update, or Delete executes.
  4. Write operations run validation.
  5. Middleware and lifecycle hooks wrap the operation.
  6. SQL is generated through the dialect and SQLGuard.
  7. database/sql executes the statement.
  8. Rows are mapped into typed models.
  9. Preloads resolve associations with secondary queries.
  10. Observers and telemetry receive query events.

Native routines and events

Calls to database functions and stored procedures take a separate path from model queries — they don't go through the model builder at all. A function that returns rows is read through NewRoutine[T], which maps the result set into your struct the same way a List would:

users, err := quark.NewRoutine[User](
ctx,
client,
"get_active_users",
100,
).List()

For a stored procedure with output parameters, use Call:

var processed int
err := quark.Call(
ctx,
client,
"process_billing",
"2026-05",
sql.Out{Dest: &processed},
)

Database-native events are exposed through notification helpers such as quark.Notify.

Schema evolution

DDL follows the same rule as the rest of the SQL: the dialect owns it. Add, drop, alter, rename column, and rename table are all dialect methods. Each dialect also reports whether the engine supports transactional DDL, since that differs by engine and changes what a failed migration leaves behind.