Skip to main content
Version: 1.17.0

Query Builder API Reference

Query[T] is Quark's immutable fluent builder for model T. Builder methods return a cloned query; execution methods are covered in Querying and write methods in CRUD.

q := quark.For[User](ctx, client)

Core Builder Methods​

MethodPurpose
Where(column, operator string, value any)Adds an AND predicate.
WhereIn(column string, values []any)Adds column IN (...).
WhereInOf[T, V](q *Query[T], column string, values []V)Package-level typed-slice form of WhereIn (no []any conversion loop).
WhereBetween(column string, start, end any)Adds column BETWEEN ? AND ?.
PaginateAfter(pageSize int, after string)Keyset (seek) pagination: the rows after the token's position in the query's ORDER BY, plus the next token.
WhereLike(column, pattern string) / WhereNotLikeAdds column LIKE ? ESCAPE '\': a pattern whose backslash escapes the next character on every engine.
WhereContains(column, text string) / WhereStartsWith / WhereEndsWithSearches for text the user typed, escaped with EscapeLike and wrapped in the wildcards named.
WhereNot(column, operator string, value any)Adds NOT (column operator value).
Or(fn func(*Query[T]) *Query[T])Adds a grouped OR branch.
WhereJSON(column, path, operator string, value any)Adds a dialect JSON extraction predicate.
WhereSubquery(column, operator, subquery string)Adds a raw subquery predicate when raw SQL is enabled.
OrderBy(column, direction string)Adds ORDER BY. Direction is ASC/DESC (case-insensitive, "" = ASC); anything else fails with ErrInvalidQuery.
Limit(n int)Sets result limit.
Offset(n int)Sets offset.
Select(columns ...string)Selects model-table columns.
Distinct()Adds SELECT DISTINCT.
GroupBy(columns ...string)Adds GROUP BY.
Having(column, operator string, value any)Adds HAVING.
Join(table string) *JoinBuilder[T]Inner join; complete with .On(left, op, right) or .OnRaw(clause).
LeftJoin(table string) *JoinBuilder[T]Left join; same completion.
RightJoin(table string) *JoinBuilder[T]Right join; same completion.
Preload(relations ...string)Eager-loads relations by Go field name.
Apply(scopes ...Scope[T])Applies reusable scope functions.
Cache(ttl time.Duration, tags ...string)Enables per-query result caching.
Unscoped()Includes soft-deleted rows.

Where​

users, err := quark.For[User](ctx, client).
Where("active", "=", true).
Where("age", ">=", 18).
Limit(100).
List()

Supported operators:

OperatorNotes
=, !=, <>Equality and inequality.
<, <=, >, >=Comparisons.
LIKE, NOT LIKEPattern matching.
IN, NOT INThe value is any slice or array ([]any, []string, []int64, a named slice type…). Usually created through WhereIn.
BETWEEN, NOT BETWEENThe value is a slice or array whose first two elements are the bounds. Usually created through WhereBetween.
IS NULL, IS NOT NULLNo bound value is emitted.

Column identifiers and operators are validated by SQLGuard. Values are passed as driver parameters.

For IN, NOT IN, BETWEEN and NOT BETWEEN, a value that is not a list — a scalar, nil, a byte slice such as []byte or net.IP, a bare UUID — and a BETWEEN with fewer than two values return ErrInvalidQuery before any SQL runs. Byte slices and UUIDs are single values to database/sql; wrap one in []any{v} to use it as a one-element list.

When the query has at least one join, Where / OrderBy / GroupBy / Select / Col also accept a one-level qualified name (table.column); each segment is validated and quoted separately. See Qualified column names under a join.

WhereIn​

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

Avoid empty slices. Most SQL engines reject IN (). The same holds for an empty slice of any type passed to Where(column, "IN", values).

For a typed slice, WhereInOf performs the []any conversion internally (a package-level function — Go methods cannot add a second type parameter):

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

WhereBetween​

orders, err := quark.For[Order](ctx, client).
WhereBetween("created_at", start, end).
List()

WhereLike and the text searches​

Where(col, "LIKE", pattern) emits the pattern as is, under the engine's default escape rules. The escaped forms declare the character on the statement:

quark.For[Doc](ctx, client).WhereContains("title", "100%").List()
// "title" LIKE ? ESCAPE '\' bound to %100\%%
quark.For[Doc](ctx, client).WhereLike("ref", "INV-"+quark.EscapeLike(id)+"-%").List()

EscapeLike prefixes %, _, [ and \ with a backslash. The tail is spelled ESCAPE '\\' on MySQL and MariaDB, whose parsers read a lone backslash inside a literal as an escape, and ESCAPE '\' everywhere else. A pattern ending in a dangling backslash returns ErrInvalidQuery before any SQL runs. The typed columns offer Contains / StartsWith / EndsWith / LikeEscaped and the AST quark.Like / quark.Contains / quark.StartsWith / quark.EndsWith.

WhereNot​

users, err := quark.For[User](ctx, client).
WhereNot("banned", "=", true).
List()

Or​

users, err := quark.For[User](ctx, client).
Where("active", "=", true).
Or(func(q *quark.Query[User]) *quark.Query[User] {
return q.Where("role", "=", "admin").
Where("verified", "=", true)
}).
List()

The callback receives a blank query used only to collect the OR group. Conditions inside that callback are joined with AND.

WhereJSON​

users, err := quark.For[User](ctx, client).
WhereJSON("metadata", "plan", "=", "enterprise").
List()

The JSON expression is generated by the active dialect:

DialectFunction or operator
PostgreSQLjsonb_extract_path_text
MySQLJSON_EXTRACT
MariaDBJSON_VALUE
SQLiteJSON_EXTRACT
SQL ServerJSON_VALUE
OracleJSON_VALUE

WhereSubquery​

limits := quark.DefaultLimits()
limits.AllowRawQueries = true

client, err := quark.New("postgres", dsn,
quark.WithLimits(limits),
)

users, err := quark.For[User](ctx, client).
WhereSubquery("id", "IN", "SELECT user_id FROM orders WHERE total > 100").
List()

If AllowRawQueries is false, the returned query stores ErrInvalidQuery and execution fails.

Ordering and Pagination​

users, err := quark.For[User](ctx, client).
OrderBy("created_at", "DESC").
OrderBy("id", "ASC").
Limit(20).
Offset(40).
List()

OrderBy treats any direction other than DESC or desc as ascending.

SQL Server and Oracle require an ORDER BY when using offset/fetch. If you use Limit or Offset without an order, Quark adds a fallback order.

Select and Distinct​

users, err := quark.For[User](ctx, client).
Select("id", "email", "name").
Distinct().
Limit(100).
List()

Select validates each argument as a simple SQL identifier. It is not a raw SQL expression API. Use aggregate helpers, views, or RawQuery for expressions, aliases, and dotted identifiers.

GroupBy and Having​

rows, err := quark.For[Order](ctx, client).
Select("status").
GroupBy("status").
Having("status", "!=", "cancelled").
List()

Grouped aggregate projections such as COUNT(*) AS count are not currently accepted by Select because SQLGuard validates simple identifiers. Use a database view or controlled raw SQL for those reports.

Joins​

orders, err := quark.For[Order](ctx, client).
Join("users").On("users.id", "=", "orders.user_id").
Where("status", "=", "paid").
Limit(100).
List()

table is validated and quoted. .On(left, op, right) is the structured form (both identifiers validated by SQLGuard); .OnRaw(clause) is the escape hatch for the identifier-only grammar ValidateJoinOn accepts. Where and Select still accept simple identifiers only; they do not accept users.active or orders.total AS amount.

orders, err := quark.For[Order](ctx, client).
LeftJoin("payments").On("payments.order_id", "=", "orders.id").
List()

RightJoin is available, but SQLite does not support every right-join shape across versions. Prefer LeftJoin when portability matters.

Preload​

users, err := quark.For[User](ctx, client).
Preload("Profile", "Posts", "Roles").
Limit(50).
List()

Use the Go relation field name. Preload("Posts") is correct for a field named Posts; Preload("posts") is not.

Supported relation types:

RelationTag
Has onerel:"has_one"
Has manyrel:"has_many"
Belongs torel:"belongs_to"
Many to manyrel:"many_to_many" m2m:"join_table:this_fk:ref_fk"
Polymorphicrel:"polymorphic" polymorphic:"type_col:type_value" join:"parent_id"

Apply and Scope​

type Scope[T any] func(*quark.Query[T]) *quark.Query[T]
var ActiveUsers = quark.Scope[User](func(q *quark.Query[User]) *quark.Query[User] {
return q.Where("active", "=", true)
})

users, err := quark.For[User](ctx, client).
Apply(ActiveUsers).
Limit(100).
List()

Scopes are applied in the order passed to Apply.

Cache​

users, err := quark.For[User](ctx, client).
Where("active", "=", true).
Cache(5*time.Minute, "users", "users:active").
List()

If no tags are supplied, Quark tags the entry with the table name. If custom tags are supplied, include the table tag yourself when you want writes to invalidate the entry automatically.

Unscoped​

allUsers, err := quark.For[User](ctx, client).
Unscoped().
List()

Unscoped disables the automatic deleted_at IS NULL predicate for models that have a deleted_at column.

Immutability​

base := quark.For[User](ctx, client).Where("active", "=", true)

admins := base.Where("role", "=", "admin")
editors := base.Where("role", "=", "editor")

base remains only active = true. This clone pattern makes query composition predictable and helps avoid state leaks across request paths.