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

query

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package query is the filter model of filterable resources: declared fields, the filter document (and, or, not, per-type operators, search, relative times), its validation, canonical form and hash, and the compilation to SQL text with arguments. It depends on the standard library only.

Index

Examples

Constants

View Source
const (
	MaximumDepth         = 8
	MaximumNodes         = 200
	MaximumListValues    = 500
	MaximumStringBytes   = 1024
	MaximumDocumentBytes = 16 * 1024
)

The limits of one document (D12). A document beyond any of them is refused with invalid_filter.

Variables

This section is empty.

Functions

func Canonical

func Canonical(document *Document) ([]byte, error)

Canonical is the document's canonical JSON: keys sorted, no insignificant whitespace, numbers in their shortest form, times in UTC, uuids in lower case, relative times as seconds ("-7d" and "-1w" both become "-604800s"). Two documents that Parse into the same values have the same canonical form, so its Hash deduplicates stored filters (D13). A nil document is the empty document `{}`.

func Hash

func Hash(document *Document) (string, error)

Hash is the SHA-256 of the canonical form in lower-case hex.

func NonFiniteSpelling

func NonFiniteSpelling(value float64) string

NonFiniteSpelling is PostgreSQL's spelling of a non-finite float8 (NaN, Infinity, -Infinity), the form a cursor carries it in; postgresadapter writes the same in JSON bodies.

Types

type Builder

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

Builder writes one PostgreSQL statement: its text and the numbered arguments the placeholders $1, $2, ... bind. The zero value is ready to use. Identifiers and expressions are written as they are; values go through Argument.

func (*Builder) Argument

func (b *Builder) Argument(value any) *Builder

Argument binds value and writes its placeholder. A nil value writes NULL and binds nothing.

func (*Builder) Query

func (b *Builder) Query() (string, []any)

Query returns the statement text and its arguments in placeholder order.

func (*Builder) WriteString

func (b *Builder) WriteString(text string) *Builder

WriteString appends text as it is.

type Code

type Code string

Code names what a client got wrong. Every code answers 400.

const (
	CodeInvalidFilter Code = "invalid_filter"
	CodeInvalidSort   Code = "invalid_sort"
	CodeInvalidPage   Code = "invalid_page"
	CodeInvalidCursor Code = "invalid_cursor"
)

The codes an Error carries.

func (Code) Message

func (c Code) Message() string

Message is the fixed, client-safe text of the code; "" for a code this package does not define.

type Cursor

type Cursor struct {
	Sort   []SortTerm
	Values []any
	ID     string
}

Cursor marks the last row of a page in cursor mode: the sort it was taken under, the row's value of every sort term (nil for NULL) and its id, a uuid. ListQuery.NextCursor builds one from the page query's columns; Encode makes it opaque for the client.

func ParseCursor

func ParseCursor(fields Fields, encoded string) (*Cursor, error)

ParseCursor decodes an encoded cursor against the resource's fields: the sort it carries must parse (ParseSort), every value must be of its field's type or null, and the id must be a uuid. Values are column contents, not document values, so the D12 limits do not apply to them: any string length, any int64, the non-finite floats and infinite times of Encode, and any enum text (a retired value is still a row's value; a forged value on a native PostgreSQL enum column fails in the database with SQLSTATE 22P02, which the loader maps to CodeInvalidCursor in cursor mode). Compile then refuses the cursor if the request's sort differs. Every refusal is an *Error with CodeInvalidCursor at path "after".

func (*Cursor) Encode

func (c *Cursor) Encode() (string, error)

Encode returns the cursor as URL-safe base64 of its JSON form. Values are the row's values as a driver returns them: a time.Time, an integer or float of any width, a string, a bool, or a uuid as [16]byte, which is written in its hyphenated form. A float32 is widened exactly, a non-finite float is written as the string PostgreSQL spells it (NaN, Infinity, -Infinity), a timestamptz infinity is the string "infinity" or "-infinity" (NextCursor). A time outside the years 0 to 9999 in UTC has no RFC 3339 form and is refused. The error of a value encoding/json cannot write is reported by message only; it is not part of the API.

type DerivedColumn

type DerivedColumn struct {
	Name string
	SQL  string
}

DerivedColumn is a value computed per row by the page query that the loader needs for hydration or the client for display, named by its API name.

type Document

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

Document is a parsed and validated filter document of one resource: `{"where": <node>, "search": "..."}`, both parts optional. Parse is the only way to build one, so every document compiles. The empty document matches every row.

func Parse

func Parse(fields Fields, raw []byte) (*Document, error)

Parse validates raw against the resource's fields and returns the document. A condition may only name a field declared Filter, with an operator of the field's type (OperatorsOf) and a value of that type; search needs a field declared Search. Every refusal is an *Error with CodeInvalidFilter naming the path of the offending part.

Example

ExampleParse reads a filter document of the todo example's fields: conditions combined with and, or and not, a relative time, and a search over the searchable fields.

package main

import (
	"fmt"

	"github.com/govengers/livewire/query"
)

func main() {
	fields := query.Fields{
		{Name: "title", Type: query.TypeText, SQL: "todos.title", Filter: true, Sort: true, Search: true},
		{Name: "priority", Type: query.TypeEnum, SQL: "todos.priority", Enum: []string{"low", "normal", "high"}, Filter: true},
		{Name: "done", Type: query.TypeBoolean, SQL: "todos.done", Filter: true},
		{Name: "dueAt", Type: query.TypeTime, SQL: "todos.due_at", Filter: true, Nullable: true},
	}
	// snippet:begin filter-document
	document, err := query.Parse(fields, []byte(`{
		"where": {"and": [
			{"field": "done", "op": "eq", "value": false},
			{"or": [
				{"field": "priority", "op": "in", "value": ["high", "normal"]},
				{"field": "dueAt", "op": "lt", "value": {"relative": "+2d"}}
			]},
			{"not": {"field": "title", "op": "startsWith", "value": "draft"}}
		]},
		"search": "milk"
	}`))
	// snippet:end filter-document
	if err != nil {
		fmt.Println(err)
		return
	}
	canonical, err := query.Canonical(document)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(canonical), document.IsTimeDependent())
}
Output:
{"search":"milk","where":{"and":[{"field":"done","op":"eq","value":false},{"or":[{"field":"priority","op":"in","value":["high","normal"]},{"field":"dueAt","op":"lt","value":{"relative":"172800s"}}]},{"not":{"field":"title","op":"startsWith","value":"draft"}}]}} true

func (*Document) FieldNames

func (d *Document) FieldNames() []string

FieldNames returns the distinct fields the conditions name, sorted. The root package evaluates the Read checks of D39 on them.

func (*Document) IsTimeDependent

func (d *Document) IsTimeDependent() bool

IsTimeDependent reports whether the document carries a relative time, which makes its result move with the clock without a write; the engine re-runs such subscriptions on a tick.

type Error

type Error struct {
	Code    Code
	Message string
	Fields  map[string]string
}

Error is a client mistake in a filter document, a sort, a page or a cursor. Fields maps the path of the offending part (`where.and[1].field`, `sort[0]`, `limit`, `after`) to what is wrong with it; one error names one fault. Match it with errors.As; the root package answers it with 400 and the Code.

func (*Error) Error

func (e *Error) Error() string

Error names the code and its message only: a field's path may be a key the client chose and its text may quote the client's value, both stay out of logs and wide events; the client gets them through the Fields.

type Field

type Field struct {
	Name string
	Type FieldType
	// SQL is the expression over the resource's FROM, written into the statement as it is.
	SQL string
	// Nullable must be true whenever SQL can yield NULL. A nullable term keeps the rows
	// without a value last on a descending sort too, where PostgreSQL would put them first,
	// and cursor mode walks them. A term declared non-nullable over a column with NULLs is
	// wrong in cursor mode: ascending it skips the NULL rows, descending a page that ends
	// among the leading NULLs ends the walk and loses every row after them. Only a
	// non-nullable leading sort term gives a cursor page the index range bound; a Nullable
	// leading term walks correctly but without it.
	Nullable bool
	// Enum lists the values of a TypeEnum field; a filter refuses any other value.
	Enum   []string
	Filter bool
	Sort   bool
	Search bool
	Read   func(context.Context) error
	Write  func(context.Context) error
}

Field is one field of a filterable resource: its API name (the JSON name of the item), its type and the SQL expression that reads it over the resource's FROM. Filter, Sort and Search say what a client may do with it. Read and Write are the column checks of D39: nil passes; this package stores them and evaluates Read in Fields.Visible only, the root package decides what a refusal means.

type FieldType

type FieldType string

FieldType is the type of a field as the API exposes it. It decides which operators a filter may use on the field (OperatorsOf) and how a value is decoded.

const (
	TypeString      FieldType = "string"
	TypeText        FieldType = "text"
	TypeInteger     FieldType = "int"
	TypeFloat       FieldType = "float"
	TypeBoolean     FieldType = "bool"
	TypeTime        FieldType = "time"
	TypeUUID        FieldType = "uuid"
	TypeEnum        FieldType = "enum"
	TypeStringArray FieldType = "stringArray"
	TypeJSON        FieldType = "json"
)

The field types. TypeFloat reads a real or double precision expression and binds its values as float8; a numeric column is declared as `t.amount::float8` and accepts float comparison semantics. TypeStringArray reads a text[] expression and is filterable but not sortable; TypeJSON is neither.

type Fields

type Fields []Field

Fields is the ordered field list of one resource.

func (Fields) Lookup

func (f Fields) Lookup(name string) (Field, bool)

Lookup returns the field with the given API name.

func (Fields) Validate

func (f Fields) Validate() error

Validate checks the declaration: names present and unique, a known type, SQL present, enum values on exactly the enum fields, Search only on string and text, Filter never on json, Sort never on json or stringArray. The error names the first field that is wrong; it is a declaration mistake, not an *Error.

func (Fields) Visible

func (f Fields) Visible(ctx context.Context) Fields

Visible returns the fields whose Read check passes in ctx; a nil check passes. The result is a new list, the receiver is unchanged.

type ListQuery

type ListQuery struct {
	Where Predicate
	Order []OrderTerm
	Limit int
	// Offset is ignored when Cursor is set.
	Offset int
	// CountCap bounds the count: CountSQL reads at most CountCap + 1 rows, so a total above
	// CountCap means "more than CountCap" (D14). Zero counts every row.
	CountCap int
	// Cursor is the position after which PageSQL reads in cursor mode (D38).
	Cursor *Cursor
}

ListQuery is everything a list loader needs: one predicate (scope, filter and search), the order (the id tiebreaker is added by PageSQL), the page and the count cap. A nil Where matches every row.

func Compile

func Compile(fields Fields, document *Document, sort []SortTerm, page Page, scope Predicate) (*ListQuery, error)

Compile turns a parsed document, the sort terms, the page and the resource's scope into one ListQuery. A nil document is no filter, no sort terms means no order beyond the id, a nil scope matches every row. Conditions compile to plain comparisons the planner can serve from an index; a NULL input drops the row in a positive WHERE context, and `not` reads NULL as FALSE before negating, so `not` behaves classically: `ne` and `notIn` include the rows whose value is NULL, exactly like `not eq` and `not in`. A sort term must name a field in fields; a cursor must have been taken under the same sort. The caller sets CountCap.

Example
package main

import (
	"fmt"

	"github.com/govengers/livewire/query"
)

func main() {
	fields := query.Fields{
		{Name: "id", Type: query.TypeUUID, SQL: "parts.id", Filter: true, Sort: true},
		{Name: "name", Type: query.TypeString, SQL: "parts.name", Filter: true, Sort: true, Search: true},
		{Name: "createdAt", Type: query.TypeTime, SQL: "parts.created_at", Filter: true, Sort: true},
	}
	document, err := query.Parse(fields, []byte(`{"where":{"field":"createdAt","op":"gt","value":{"relative":"-7d"}},"search":"bolt"}`))
	if err != nil {
		fmt.Println(err)
		return
	}
	sort, err := query.ParseSort(fields, "-createdAt")
	if err != nil {
		fmt.Println(err)
		return
	}
	page, err := query.ParsePage("20", "", 500)
	if err != nil {
		fmt.Println(err)
		return
	}
	scope := func(builder *query.Builder) { builder.WriteString("parts.tenant_id = ").Argument("tenant-1") }

	listQuery, err := query.Compile(fields, document, sort, page, scope)
	if err != nil {
		fmt.Println(err)
		return
	}
	text, arguments := listQuery.PageSQL(&query.ResourceSQL{From: "parts", IDColumn: "parts.id"})
	fmt.Println(text)
	fmt.Println(arguments)
}
Output:
SELECT parts.id, (parts.created_at) FROM parts WHERE (parts.tenant_id = $1) AND (((parts.created_at) > (now() + make_interval(secs => $2))) AND ((parts.name) ILIKE $3 ESCAPE '\')) ORDER BY (parts.created_at) DESC, parts.id ASC LIMIT $4 OFFSET $5
[tenant-1 -604800 %bolt% 20 0]

func (*ListQuery) CountSQL

func (q *ListQuery) CountSQL(specification *ResourceSQL) (string, []any)

CountSQL counts the rows under WHERE; the cursor does not narrow it. With a CountCap it reads at most CountCap + 1 rows, so the count over a large table stops early.

func (*ListQuery) DistinctSQL

func (q *ListQuery) DistinctSQL(specification *ResourceSQL, field Field, maximum int) (string, []any, error)

DistinctSQL is the distinct read of D15: the values of one filterable scalar field under WHERE with their row counts, most frequent first, at most maximum rows. A field that is not filterable, or an array, is refused with an *Error of CodeInvalidFilter at path "field"; the caller checks that the field is visible.

func (*ListQuery) NextCursor

func (q *ListQuery) NextCursor(orderValues []any, id string) *Cursor

NextCursor builds the cursor of a row from the order values PageSQL selected after the id and derived columns (one per order term, nil for NULL) and the row's id. Values are what the driver returns (see Cursor.Encode); a timestamptz infinity has no time.Time, the loader hands it over as the string "infinity" or "-infinity".

func (*ListQuery) PageSQL

func (q *ListQuery) PageSQL(specification *ResourceSQL) (string, []any)

PageSQL is the page query: it selects the id column, the derived columns and then the value of every order term (so the loader can build the next cursor, NextCursor), under WHERE, ordered by the terms with the id column as the final tiebreaker, and bounded by LIMIT and OFFSET, or by the keyset after Cursor in cursor mode.

type Operator

type Operator string

Operator is a filter operator as it is spelled in the document.

const (
	OperatorEqual          Operator = "eq"
	OperatorNotEqual       Operator = "ne"
	OperatorGreater        Operator = "gt"
	OperatorGreaterOrEqual Operator = "gte"
	OperatorLess           Operator = "lt"
	OperatorLessOrEqual    Operator = "lte"
	OperatorIn             Operator = "in"
	OperatorNotIn          Operator = "notIn"
	OperatorContains       Operator = "contains"
	OperatorStartsWith     Operator = "startsWith"
	OperatorIsNull         Operator = "isNull"
	OperatorHas            Operator = "has"
	OperatorHasAny         Operator = "hasAny"
	OperatorHasAll         Operator = "hasAll"
	OperatorIsEmpty        Operator = "isEmpty"
)

The operators. Which ones a field accepts depends on its type, see OperatorsOf.

func OperatorsOf

func OperatorsOf(fieldType FieldType) []Operator

OperatorsOf lists the operators a filter may apply to a field of the type (D12), in the order the generators emit them. A type that is not filterable (json) has none. The result is the caller's copy.

type OrderTerm

type OrderTerm struct {
	Field      string
	SQL        string
	Descending bool
	Nullable   bool
}

OrderTerm is one ORDER BY term: a field, its SQL expression and its direction. A Nullable term keeps its NULLs last on a descending sort as well.

type Page

type Page struct {
	Limit  int
	Offset int
	After  *Cursor
}

Page is one page of a list. Offset mode pages by Limit and Offset; cursor mode (D38) sets After, the cursor of the last row of the previous page, and ignores Offset.

func ParsePage

func ParsePage(limit, offset string, maximumLimit int) (Page, error)

ParsePage reads the limit and offset parameters. An empty limit means maximumLimit, a larger one is clamped to it (D14); an empty offset means 0. A negative or unparsable value is refused with an *Error of CodeInvalidPage. maximumLimit must be positive.

type Predicate

type Predicate func(*Builder)

Predicate writes one boolean SQL expression into a builder. A nil Predicate is no condition: it matches every row. And, Or and Not parenthesize every operand, so a predicate may be any expression, a bare `a OR b` included. A predicate is meant for a WHERE clause: it is three-valued (a NULL input yields NULL, which drops the row like FALSE) except under Not, which reads NULL as FALSE before negating.

func And

func And(predicates ...Predicate) Predicate

And joins the non-nil predicates with AND, each operand in parentheses. One predicate alone is returned as it is; none returns nil.

func Not

func Not(predicate Predicate) Predicate

Not negates the predicate two-valued: `NOT COALESCE((p), FALSE)`, so a row the predicate cannot decide (NULL) is in the negation. A nil predicate matches every row, so its negation matches none.

func Or

func Or(predicates ...Predicate) Predicate

Or joins the non-nil predicates with OR, each operand in parentheses. One predicate alone is returned as it is; none returns nil.

type ResourceSQL

type ResourceSQL struct {
	From     string
	IDColumn string
	Derived  []DerivedColumn
}

ResourceSQL names where a resource's rows come from: the FROM clause (tables and joins), the id column and the derived columns the page query returns next to the id.

type SortTerm

type SortTerm struct {
	Field      string
	Descending bool
}

SortTerm is one term of a sort: `-createdAt` is {Field: "createdAt", Descending: true}.

func ParseSort

func ParseSort(fields Fields, raw string) ([]SortTerm, error)

ParseSort reads a comma-separated list of fields, each optionally prefixed with "-" for descending: "-createdAt,name". An empty raw value returns no terms, so the caller applies the resource's default sort. Only fields declared Sort may appear, each at most once; the id tiebreaker is added when the SQL is built, not here. Every refusal is an *Error with CodeInvalidSort.

func (SortTerm) String

func (t SortTerm) String() string

Jump to

Keyboard shortcuts

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