where

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Apr 21, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package where provides query building options for store operations.

Field-based options (WithFilter*, WithOrder) require a field whitelist configured via store.WithQueryFields. Unrecognized fields return an error.

Index

Constants

View Source
const (
	QueryPage  = "page"
	QuerySize  = "size"
	QueryOrder = "order"
)

Reserved query parameter names.

View Source
const DefaultPageSize = 20

DefaultPageSize is used when "size" is absent from the query.

View Source
const MaxInList = 500

MaxInList is the maximum number of values accepted by WithFilterIn. Above this, databases start rejecting queries: SQLite ~999, MySQL limited by max_allowed_packet, PostgreSQL ~65535 bound parameters. Callers with more values should chunk the IN list manually.

View Source
const MaxPageSize = 10_000

MaxPageSize is the hard upper bound on page size accepted by WithPage and WithLimit. Requests above this value are rejected with ErrInvalidParam. Individual Stores may tighten further via store.WithMaxPageSize. Set deliberately below math.MaxInt32 to leave headroom for offset arithmetic (page * size) on 32-bit systems.

Variables

View Source
var (
	// ErrInvalidParam indicates a client-provided query parameter is invalid
	// (e.g. page < 1). Distinguished from config/field errors which are server bugs.
	ErrInvalidParam = errors.New("where: invalid parameter")

	// ErrUnknownField indicates a field name not present in the query whitelist.
	// Typically caused by client input (sort/filter on a non-queryable field).
	ErrUnknownField = errors.New("where: unknown field")

	// ErrFieldNotConfigured indicates WithQueryFields was not called on the Store.
	// This is a server-side configuration error (programming bug), not client input.
	ErrFieldNotConfigured = errors.New("where: fields not configured")
)

Functions

func ApplyFiltersOnly

func ApplyFiltersOnly(db *gorm.DB, fieldMap map[string]string, opts []Option) (*gorm.DB, error)

ApplyFiltersOnly applies only filter options (skips pagination, ordering, count). Used by Store.List for the COUNT query so that LIMIT/OFFSET do not affect the total.

Types

type Config

type Config struct {
	Count     bool // true if WithCount() was applied
	HasFilter bool // true if any WHERE condition was applied
	HasPage   bool // true if pagination (WithPage/WithOffset/WithLimit) was applied
	HasCursor bool // true if cursor-based pagination (WithCursor) was applied
	// DegenerateFilter is set when a filter option collapsed to a
	// guaranteed-empty result (currently only WithFilterIn over an empty
	// slice, which renders WHERE 1=0). HasFilter is still set so callers
	// that gate on "any filter present" see the filter, but locators that
	// want to reject "filter that matches nothing" (e.g. Update/Delete)
	// can inspect this flag to refuse the operation.
	DegenerateFilter bool
	MaxPageSize      int // when > 0, LIMIT is clamped to this value
	// contains filtered or unexported fields
}

Config holds query metadata extracted from options.

func Apply

func Apply(db *gorm.DB, fieldMap map[string]string, opts []Option) (*gorm.DB, *Config, error)

Apply applies all options to the given GORM DB and returns the modified DB and config. Used internally by Store.

type CursorDirection

type CursorDirection string

CursorDirection specifies the scan direction for cursor-based pagination.

const (
	// CursorAfter fetches rows AFTER the cursor value (ascending keyset).
	CursorAfter CursorDirection = "after"
	// CursorBefore fetches rows BEFORE the cursor value (descending keyset).
	CursorBefore CursorDirection = "before"
)

type Op

type Op string

Op is a comparison operator.

const (
	Eq  Op = "="
	Ne  Op = "<>"
	Gt  Op = ">"
	Gte Op = ">="
	Lt  Op = "<"
	Lte Op = "<="
)

type Option

type Option func(db *gorm.DB, cfg *Config, fieldMap map[string]string) (*gorm.DB, error)

Option modifies a GORM query and/or query config. fieldMap is provided by Store at apply-time.

func FromQuery

func FromQuery(params url.Values, allowedFields map[string]string, defaultSize ...int) ([]Option, error)

FromQuery parses URL query parameters into []Option.

Supported parameters:

  • page: page number (default 1)
  • size: items per page (default DefaultPageSize, overridden by defaultSize if > 0)
  • order: "field:desc" or "field:asc" (field must be in allowedFields)
  • Any key in allowedFields: equality filter (WHERE field = value)

Unknown parameters are silently ignored. Use FromQueryStrict to reject them instead. Always includes WithCount().

func FromQueryStrict

func FromQueryStrict(params url.Values, allowedFields map[string]string, defaultSize ...int) ([]Option, error)

FromQueryStrict is like FromQuery but rejects unknown query parameters with ErrInvalidParam. Used by Store.ListFromQuery when the Store was constructed with WithStrict.

func WithCount

func WithCount() Option

WithCount instructs List to execute a COUNT query and return actual total. Without this, List returns total = 0 and skips COUNT.

func WithCursor

func WithCursor(field string, direction CursorDirection, cursor any, size int) Option

WithCursor adds keyset-based (cursor) pagination. Instead of OFFSET, it uses WHERE field > cursor ORDER BY field LIMIT size, which is O(1) regardless of how deep the page is. Ideal for infinite-scroll APIs.

field is validated against the query whitelist. direction controls whether to scan forward (CursorAfter) or backward (CursorBefore). cursor is the last-seen value of the sort field from the previous page. size is the maximum number of items to return.

**Uniqueness requirement**: field MUST be a strictly unique column (typically `id` or `rid`). If multiple rows can share the same value (e.g. `created_at` at second resolution), rows on the boundary will be silently skipped because `> cursor` excludes equal values. Use WithCursorBy for a composite (field, id) cursor that handles ties.

Typical usage:

where.WithCursor("id", where.CursorAfter, lastID, 20)

func WithCursorBy

func WithCursorBy(field string, direction CursorDirection, fieldCursor any, idCursor uint, size int) Option

WithCursorBy is the composite-cursor variant of WithCursor. It uses (field, id) as the keyset so rows sharing the same field value are still deterministically ordered and never skipped at page boundaries. Use this for non-unique sort columns like `created_at`.

cursor encodes the last row's (field, id) pair. When both are nil, the first page is fetched (no cursor WHERE). The SQL is:

WHERE (field, id) > (?, ?) ORDER BY field ASC, id ASC LIMIT size   // CursorAfter
WHERE (field, id) < (?, ?) ORDER BY field DESC, id DESC LIMIT size // CursorBefore

This relies on row-value comparison support, which all major SQL engines (MySQL 8+, PostgreSQL, SQLite 3.15+) implement.

func WithFilter

func WithFilter(field string, value any) Option

WithFilter adds WHERE field = value. A nil value is treated as degenerate (SQL's three-valued logic makes `col = NULL` always false) and flagged via Config.DegenerateFilter so locators can reject "filter present but matches nothing" on Update/Delete. Use explicit IS NULL semantics via a custom option if that is the intended query.

func WithFilterContains

func WithFilterContains(field string, value string) Option

WithFilterContains adds WHERE field LIKE '%<escaped value>%'. Wildcards in the caller-supplied value are escaped, so user input cannot expand the match set.

func WithFilterEndsWith

func WithFilterEndsWith(field string, value string) Option

WithFilterEndsWith adds WHERE field LIKE '%<escaped value>'.

func WithFilterIn

func WithFilterIn(field string, values ...any) Option

WithFilterIn adds WHERE field IN (...). When called with a single slice argument (e.g. WithFilterIn("id", mySlice)), the slice is unwrapped so GORM receives the flat values instead of a nested []any{[]T{...}}. A nil or empty slice produces no-match (WHERE 1=0), rather than driver-dependent behaviour from a nil/empty IN list. Lists larger than MaxInList are rejected with ErrInvalidParam.

func WithFilterLike

func WithFilterLike(field string, pattern string) Option

WithFilterLike adds WHERE field LIKE pattern. The pattern is treated as a literal substring of arbitrary length — `%` and `_` in the input are escaped so user-supplied values cannot widen the match set. Use when you need a positional wildcard but want to keep the rest of the input safe (the helper does NOT inject leading/trailing `%`; pair it with WithFilterContains / StartsWith / EndsWith for those shapes).

For the rare case where the caller genuinely wants the raw LIKE grammar (e.g. internal admin tooling that builds patterns server-side from a trusted source), use WithFilterLikeRaw and own the escaping.

func WithFilterLikeRaw

func WithFilterLikeRaw(field string, pattern string) Option

WithFilterLikeRaw adds WHERE field LIKE pattern with the pattern passed through verbatim — `%` and `_` retain their wildcard meaning. Reserved for trusted callers that build patterns server-side; passing untrusted input here lets attackers expand the match set.

func WithFilterOp

func WithFilterOp(field string, op Op, value any) Option

WithFilterOp adds WHERE field op value. op must be one of the predefined constants (Eq, Ne, Gt, Gte, Lt, Lte). A nil value is treated as degenerate for the same reason as WithFilter.

func WithFilterStartsWith

func WithFilterStartsWith(field string, value string) Option

WithFilterStartsWith adds WHERE field LIKE '<escaped value>%'.

func WithLimit

func WithLimit(limit int) Option

WithLimit sets a raw limit. Rejects limit < 1 and limit > MaxPageSize.

func WithMaxPageSize

func WithMaxPageSize(max int) Option

WithMaxPageSize clamps any subsequent LIMIT to at most max. This is a server-side safety measure that prevents clients from requesting unbounded result sets. Applied silently (no error) because this is a policy constraint, not a user input error.

func WithOffset

func WithOffset(offset int) Option

WithOffset sets a raw offset. Negative offsets are rejected.

func WithOrder

func WithOrder(field string, desc ...bool) Option

WithOrder adds ORDER BY field [DESC]. desc defaults to false (ASC).

func WithPage

func WithPage(page, size int) Option

WithPage sets page-based pagination. Returns ErrInvalidParam if page < 1, size < 1, size > MaxPageSize, or the implied offset (page-1)*size would overflow int32. Using int64 math here lets us detect overflow even on 32-bit platforms.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL