qx

package module
v0.2.1 Latest Latest
Warning

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

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

README

qx

GoDoc License

qx is a small, focused package for describing query expressions in a structured, type-agnostic form.
Its primary purpose is to act as a minimal query / filter AST (expressions, ordering, pagination) that can be consumed by other packages (databases, indexes, storage engines). It does not support projections, joins, or aggregation functions as its main purpose is filtering. User-defined functions may be implemented in the future, but currently there are no plans for that.

There is no built-in query normalization (NNF, De Morgan) because many engines implement their own optimization strategies based on their internal layout.

As a secondary feature, it also provides a high-performance in-memory matcher: a fast evaluator that applies expressions to Go structs using reflection and unsafe optimizations.
This is intended for cases where filtering happens in memory without an index.

You can use the expression layer without ever touching the matcher.

Query Expression

The expression language is intentionally simple and explicit.
It represents filters as a tree of expressions combined with logical operators.

Expressions

An Expr represents either:

  • a comparison on a field (EQ, GT, IN, HAS, ...)
  • a logical combination of other expressions (AND, OR)
type Expr struct {
    Op       Op
    Not      bool
    Field    string
    Value    any
    Operands []Expr
}
Supported operations

The semantics are intentionally close to what most query engines provide:

  • Scalar comparisons:
    • EQ, NE (NOTEQ) GT, GTE, LT, LTE
  • String operations (case-sensitive):
    • PREFIX — field value starts with the provided string
    • SUFFIX — field value ends with the provided string
    • CONTAINS — field value contains the provided substring
  • Set / slice operations:
    • IN — scalar field value is contained in the provided slice
    • NOTIN — scalar field value is not contained in the provided slice *
    • HAS — slice field contains all provided values
    • HASANY — slice field contains at least one provided value
    • HASNOT — slice field does not contain all provided values *
    • HASNONE — slice field contains none of the provided values *
  • Logical operations:
    • AND, OR, NOT
  • Syntactic sugar:
    • NE, NOTEQ - an alias for NOT(EQ)
    • NOTIN - an alias for NOT(IN)
    • HASNOT - an alias for NOT(HAS)
    • HASNONE - an alias for NOT(HASANY)

Syntactic sugar operations exist only as helper functions and do not have separate opcodes.

Expressions can be constructed using helper functions with the same names:

qx.EQ("age", 30)
qx.GT("score", 100)
qx.IN("status", []string{"active", "pending"})

qx.AND(
    qx.EQ("country", "US"),
    qx.GTE("age", 20),
)

qx.NE("deleted", true)
Queries

QX combines:

  • a root expression
  • optional ordering rules
  • pagination (offset / limit)
  • an optional cache key
type QX struct {
    Key    string
    Expr   Expr
    Order  []Order
    Offset uint64
    Limit  uint64
}

This makes it suitable as a transport or intermediate representation between systems.

q := qx.Query(
    qx.EQ("status", "active"),
    qx.GTE("age", 21),
).
By("created_at", qx.DESC).
Page(1, 50)

See the GoDoc for a complete API reference.

In-Memory Matcher

qx also provides an optional in-memory matcher that can evaluate expressions against Go structs.

This is useful when:

  • there is no index
  • data is already in memory
  • expressions must be evaluated many times
m := qx.MatcherFor[User]()

The matcher:

  • inspects the struct type once
  • caches reflection metadata
  • supports field lookup by Go name and struct tags (json, db, ...)
One-shot matching

For occasional checks:

match, err := qx.Match(user, EQ("age", 30))

or, using a matcher instance:

m, err := qx.MatcherFor[User]()
// ...
match, err := m.Match(user, EQ("age", 30))

For repeated evaluations of the same expression:

check, err := m.Compile(
    qx.AND(
        GTE("age", 18),
        EQ("active", true),
    )
)
// ...
for _, v := range values {
    match, err := check(v)
    // ...
}

Compiled predicates:

  • avoid repeated expression traversal
  • use fast paths when possible
  • can be safely reused across calls
Value handling

Matcher methods accept:

  • struct values (T)
  • pointers to structs (*T)
  • interfaces wrapping either

Passing a pointer enables additional fast paths based on unsafe offsets.
Passing nil always results in (false, nil).

Equality semantics

Equality is defined in terms of data, not pointer identity:

  • pointers are compared by the values they point to
  • interfaces are unwrapped
  • structs are compared field-by-field
  • slices use element-wise comparison where applicable

This makes the matcher suitable for “document-style” filtering.

Performance characteristics

The matcher is designed with performance in mind:

  • No allocations on the fast path for scalar comparisons
  • Optional unsafe field access when values are passed as *T
  • Reflection metadata cached per type
  • Expression compilation amortizes cost over repeated runs

Typical performance characteristics:

  • Simple scalar predicates: tens of nanoseconds
  • Mixed predicates: tens to hundreds of nanoseconds
  • Complex structures (slices, nested structs): higher cost, still allocation-aware

Documentation

Index

Constants

This section is empty.

Variables

View Source
var QueryOpString = map[Op]string{
	OpNOOP:     "NOOP",
	OpAND:      "AND",
	OpOR:       "OR",
	OpEQ:       "EQ",
	OpGT:       "GT",
	OpGTE:      "GTE",
	OpLT:       "LT",
	OpLTE:      "LTE",
	OpIN:       "IN",
	OpHAS:      "HAS",
	OpHASANY:   "HASANY",
	OpPREFIX:   "PREFIX",
	OpSUFFIX:   "SUFFIX",
	OpCONTAINS: "CONTAINS",
}
View Source
var QueryOpValue = map[string]Op{
	"NOOP":     OpNOOP,
	"AND":      OpAND,
	"OR":       OpOR,
	"EQ":       OpEQ,
	"GT":       OpGT,
	"GTE":      OpGTE,
	"LT":       OpLT,
	"LTE":      OpLTE,
	"IN":       OpIN,
	"HAS":      OpHAS,
	"HASANY":   OpHASANY,
	"PREFIX":   OpPREFIX,
	"SUFFIX":   OpSUFFIX,
	"CONTAINS": OpCONTAINS,
}

Functions

func Match

func Match(v any, exp Expr) (bool, error)

Match evaluates the expression exp against v in one shot. v may be provided either as a struct value (T) or as a pointer to a struct (*T); interfaces wrapping T or *T are also supported. Nil values never match and return (false, nil). For repeated evaluations over the same type, prefer creating a Matcher via MatcherFor and reusing it.

Types

type Expr

type Expr struct {
	Op       Op
	Not      bool
	Field    string
	Value    any
	Operands []Expr
}

Expr represents a single filter expression or a logical group of expressions. Depending on Op, an Expr may represent:

  • a scalar comparison (EQ, GT, LT, etc.),
  • a slice operation (IN, HAS, HASANY),
  • or a logical operation (AND, OR) combining sub-expressions.

For logical operations, Operands contains the nested expressions. For non-logical operations, Field and Value describe the comparison. If Not is set, the result of the expression is logically negated.

func AND

func AND(exprs ...Expr) Expr

AND builds a conjunction expression combining all provided expressions.

func CONTAINS

func CONTAINS(f string, v any) Expr

CONTAINS builds an expression that matches when the string field contains the provided substring.

func EQ

func EQ(f string, v any) Expr

EQ builds an equality expression: field == value.

func GT

func GT(f string, v any) Expr

GT builds a greater-than expression: field > value.

func GTE

func GTE(f string, v any) Expr

GTE builds a greater-than-or-equal expression: field >= value.

func HAS

func HAS(f string, v any) Expr

HAS builds a slice containment expression for slice fields: the field slice must contain all elements from the provided value slice.

func HASANY

func HASANY(f string, v any) Expr

HASANY builds a slice intersection expression for slice fields: the field slice must share at least one element with the provided value slice.

func HASNONE

func HASNONE(f string, v any) Expr

HASNONE builds an expression that matches when the slice field contains none of the provided values. It evaluates to true only if the intersection between the field slice and the provided values is empty. HASNONE is the logical equivalent of NOT(HASANY(...)).

func HASNOT

func HASNOT(f string, v any) Expr

HASNOT builds a negated slice containment expression for slice fields. It matches when the field slice does not contain all elements from the provided value slice (i.e., at least one of the provided values is missing). HASNOT is the logical equivalent of NOT(HAS(...)).

func IN

func IN(f string, v any) Expr

IN builds a set-membership expression: field IN valueSlice. The provided value must be a slice; the field is compared against each element of the slice.

func LT

func LT(f string, v any) Expr

LT builds a less-than expression: field < value.

func LTE

func LTE(f string, v any) Expr

LTE builds a less-than-or-equal expression: field <= value.

func NE

func NE(f string, v any) Expr

NE builds a negated equality expression: field != value.

func NOT

func NOT(exp Expr) Expr

NOT negates the provided expression by setting Not flag.

func NOTEQ

func NOTEQ(f string, v any) Expr

NOTEQ is an alias of NE and builds a negated equality expression: field != value.

func NOTIN

func NOTIN(f string, v any) Expr

NOTIN builds a negated set-membership expression: field NOT IN valueSlice. The provided value must be a slice; the field is compared against each element of the slice.

func OR

func OR(exprs ...Expr) Expr

OR builds a disjunction expression combining all provided expressions.

func PREFIX

func PREFIX(f string, v any) Expr

PREFIX builds an expression that matches when the string field starts with the provided prefix.

func SUFFIX

func SUFFIX(f string, v any) Expr

SUFFIX builds an expression that matches when the string field ends with the provided suffix.

func (Expr) UsedFields added in v0.1.1

func (expr Expr) UsedFields() []string

UsedFields returns a de-duplicated list of field names referenced by the expression. The returned slice includes fields from nested expressions.

type MatchFunc

type MatchFunc func(v any) (bool, error)

func CompileFor

func CompileFor[T any](expr Expr) (MatchFunc, error)

CompileFor is a convenience helper equivalent to MatcherFor[T]().Compile(expr). The resulting predicate accepts both T and *T values (including via interfaces).

type Matcher

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

func MatcherFor

func MatcherFor[T any]() (*Matcher, error)

MatcherFor creates a Matcher for the struct type T. T may be a struct or a pointer to a struct (any pointer depth is allowed). Reflection data for the type is computed once and cached for reuse. The matcher supports addressing fields by Go name and by selected struct tag aliases.

func NewMatcher

func NewMatcher(v any) (*Matcher, error)

NewMatcher creates a matcher for the type of the provided value. Pointer types allow faster matching.

func (*Matcher) Compile

func (m *Matcher) Compile(expr Expr) (MatchFunc, error)

Compile compiles expr into an efficient predicate function. The returned function may be called with either a struct value (T) or a pointer to a struct (*T); interfaces wrapping T or *T are also supported. Passing nil to the predicate returns (false, nil). The compiled predicate can be reused safely for repeated evaluations.

func (*Matcher) DiffFields

func (m *Matcher) DiffFields(values ...any) ([]string, error)

DiffFields returns the names of exported fields whose values differ between the provided values. Each value may be provided either as T or *T; interfaces wrapping T or *T are also supported. At least two values must be provided; if fewer are provided, it returns nil, nil. Values must be of the matcher's source type or convertible to it.

func (*Matcher) DiffFieldsTag

func (m *Matcher) DiffFieldsTag(tag string, values ...any) ([]string, error)

DiffFieldsTag is like DiffFields but returns field names using the provided struct tag. Each value may be provided either as T or *T; interfaces wrapping T or *T are also supported. For each differing field, the first component of the tag (before a comma) is used; if the tag is missing or "-", it falls back to the Go field name. If tag is empty, DiffFieldsTag behaves the same as DiffFields.

func (*Matcher) Match

func (m *Matcher) Match(v any, expr Expr) (bool, error)

Match evaluates the expression expr against v using this Matcher. v may be provided either as a struct value (T) or as a pointer to a struct (*T); interfaces wrapping T or *T are also supported. This method is intended for occasional, one-off evaluations. For repeated matching with the same expression, use Compile to obtain a reusable predicate.

type Op

type Op byte
const (
	OpNOOP Op = iota

	OpAND
	OpOR

	OpEQ
	OpGT
	OpGTE
	OpLT
	OpLTE
	OpIN

	OpHAS    // for array fields - contains at least all the provided values
	OpHASANY // for array fields - has intersection with the provided values

	OpPREFIX   // has prefix
	OpSUFFIX   // has suffix
	OpCONTAINS // contains substring
)

func (Op) MarshalJSON

func (q Op) MarshalJSON() ([]byte, error)

func (Op) String

func (q Op) String() string

func (*Op) UnmarshalJSON

func (q *Op) UnmarshalJSON(bytes []byte) error

type Order

type Order struct {
	Field string
	Type  OrderType
	Data  any
	Desc  bool
}

Order describes a single ordering rule applied to query results. The meaning of Data depends on the ordering Type. If Desc is true, the ordering is descending; otherwise ascending.

type OrderDirection

type OrderDirection byte
const (
	ASC  OrderDirection = 0
	DESC OrderDirection = 1
)

type OrderType

type OrderType byte
const (
	OrderBasic OrderType = iota
	OrderByArrayPos
	OrderByArrayCount
)

type QX

type QX struct {
	Key    string // optional cache key
	Expr   Expr
	Order  []Order
	Offset uint64
	Limit  uint64
}

QX represents a compiled query description. It combines a filter expression, optional ordering rules, offset and limit.

func Query

func Query(expressions ...Expr) *QX

Query creates a new QX and sets the provided expressions as part of the root AND group. With zero expressions it returns an empty query. With one expression it becomes the root.

func (*QX) AND added in v0.2.1

func (qx *QX) AND(exprs ...Expr) *QX

AND combines the current expression with exprs using logical AND.

func (*QX) By

func (qx *QX) By(field string, dir OrderDirection) *QX

By adds a basic sort order by field; dir may be ASC or DESC.

func (*QX) ByArrayCount

func (qx *QX) ByArrayCount(field string, dir OrderDirection) *QX

ByArrayCount adds an ordering rule by the number of items in a slice field; dir may be ASC or DESC.

func (*QX) ByArrayPos

func (qx *QX) ByArrayPos(field string, slice any, dir OrderDirection) *QX

ByArrayPos adds an ordering rule by the position of field's value within the provided slice; dir may be ASC or DESC.

func (*QX) CacheKey

func (qx *QX) CacheKey(key string) *QX

CacheKey sets an optional cache key associated with the query.

func (*QX) Max

func (qx *QX) Max(limit int) *QX

Max sets the query limit (maximum number of items to return). It panics if limit is negative.

func (*QX) OR added in v0.2.1

func (qx *QX) OR(exprs ...Expr) *QX

OR combines the current expression with exprs using logical OR.

func (*QX) Page

func (qx *QX) Page(pageNum, perPage int) *QX

Page sets the offset and limit based on 1-based pageNum and perPage. It panics if pageNum or perPage are not positive.

func (*QX) Skip

func (qx *QX) Skip(offset int) *QX

Skip sets the query offset (number of items to skip). It panics if offset is negative.

func (*QX) UsedFields

func (qx *QX) UsedFields() []string

UsedFields returns a de-duplicated list of field names referenced by the query expression and sort orders. The returned slice includes fields from nested expressions.

Jump to

Keyboard shortcuts

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