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 ¶
- Constants
- func Canonical(document *Document) ([]byte, error)
- func Hash(document *Document) (string, error)
- func NonFiniteSpelling(value float64) string
- type Builder
- type Code
- type Cursor
- type DerivedColumn
- type Document
- type Error
- type Field
- type FieldType
- type Fields
- type ListQuery
- func (q *ListQuery) CountSQL(specification *ResourceSQL) (string, []any)
- func (q *ListQuery) DistinctSQL(specification *ResourceSQL, field Field, maximum int) (string, []any, error)
- func (q *ListQuery) NextCursor(orderValues []any, id string) *Cursor
- func (q *ListQuery) PageSQL(specification *ResourceSQL) (string, []any)
- type Operator
- type OrderTerm
- type Page
- type Predicate
- type ResourceSQL
- type SortTerm
Examples ¶
Constants ¶
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 ¶
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 NonFiniteSpelling ¶
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 ¶
Argument binds value and writes its placeholder. A nil value writes NULL and binds nothing.
func (*Builder) WriteString ¶
WriteString appends text as it is.
type Code ¶
type Code string
Code names what a client got wrong. Every code answers 400.
type Cursor ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
FieldNames returns the distinct fields the conditions name, sorted. The root package evaluates the Read checks of D39 on them.
func (*Document) IsTimeDependent ¶
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 ¶
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.
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) Validate ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
And joins the non-nil predicates with AND, 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 ¶
SortTerm is one term of a sort: `-createdAt` is {Field: "createdAt", Descending: true}.
func ParseSort ¶
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.