Skip to main content

Query Builder

VEF query building is centered around typed search structs, search tags, and CRUD find options. The goal is to keep query rules close to the fields they belong to instead of scattering stringly typed conditions across handlers.

Search Struct Model

The usual shape is:

type UserSearch struct {
api.P

ID string `json:"id" search:"eq"`
Keyword string `json:"keyword" search:"contains,column=username|email"`
IsActive *bool `json:"isActive" search:"eq,column=is_active"`
}

The search tag describes how a field becomes one or more SQL conditions.

Default Behavior Without A search Tag

If a field has no search tag at all:

  • the framework still includes it in the parsed search schema
  • the default operator is eq
  • the default column name is the snake_case form of the field name

That means this field:

Age int

behaves like:

Age int `search:"eq,column=age"`

Search Tag Grammar

The search tag supports these patterns:

PatternMeaning
search:"eq"operator only
`search:"contains,column=usernameemail"`
search:"operator=gte,column=price"fully explicit key/value form
`search:"operator=in,params=delimiter:,type:int"`
search:"dive"recurse into nested struct fields
search:"-"ignore this field completely

Supported tag attributes:

AttributeMeaning
default value or operatorquery operator
columnone or more target columns, separated by `
aliastable alias used when qualifying columns
paramsextra operator parameters
diverecurse into nested struct fields

Supported Operators

The framework currently supports all of these operators:

Comparison operators

OperatorMeaning
eqequals
neqnot equals
gtgreater than
gtegreater than or equal
ltless than
lteless than or equal

Range operators

OperatorMeaning
betweeninclusive range
notBetweenoutside range

Set operators

OperatorMeaning
invalue is in a set
notInvalue is not in a set

Null operators

OperatorMeaning
isNullapplies IS NULL
isNotNullapplies IS NOT NULL

String matching operators

OperatorMeaning
containscontains substring
notContainsdoes not contain substring
startsWithstarts with prefix
notStartsWithdoes not start with prefix
endsWithends with suffix
notEndsWithdoes not end with suffix

Case-insensitive string operators

OperatorMeaning
iContainscase-insensitive contains
iNotContainscase-insensitive not contains
iStartsWithcase-insensitive starts with
iNotStartsWithcase-insensitive not starts with
iEndsWithcase-insensitive ends with
iNotEndsWithcase-insensitive not ends with

One field can target multiple columns by separating column names with |.

Example:

Keyword string `search:"contains,column=username|email|mobile"`

This is useful for keyword search against multiple text fields.

Nested Search With dive

dive is not a query operator. It is a parser directive telling the framework to recurse into nested structs.

Example:

type UserSearch struct {
Name string `search:"column=user_name,operator=contains"`
}

type OrderSearch struct {
api.P

User UserSearch `search:"dive"`
}

Aliases

Use alias when the query should qualify columns with a table alias:

Name string `search:"alias=u,column=name,operator=contains"`

This is especially useful for joined queries.

Operator Parameters

Some operators support extra parameters through the params=... section.

Currently relevant parameter keys:

Param keyMeaning
delimitercustom delimiter for parsing string-based sets or ranges
typeexplicit parsing type such as int, dec, date, datetime, or time

between Input Forms

between and notBetween support multiple input shapes:

Input shapeExample
monad.Range[T] style structmonad.Range[int]{Start: 1, End: 10}
two-item slice[]int{1, 10}
delimited string"1,10"

For string input, parsing can be controlled through params.

Examples:

Price string `search:"operator=between,column=price,params=type:int,delimiter:,"`
DateRange string `search:"operator=between,column=created_at,params=type:date,delimiter:|"`

in / notIn Input Forms

Set operators support:

Input shapeExample
slice field[]string{"a", "b"}
delimited string"a,b,c"
delimited string with custom delimiter`"1

Sorting

Sorting is usually handled through metadata using crud.Sortable:

type QueryMeta struct {
api.M
crud.Sortable
}

crud.Sortable shape:

FieldMeaning
Sort []sortx.OrderSpeclist of sort specifications

Each sortx.OrderSpec can express:

PropertyMeaning
Columntarget column
Directionascending or descending
NullsOrdernull ordering

CRUD find builders can apply these sort specs automatically.

Pagination

Paging uses page.Pageable:

type QueryMeta struct {
api.M
page.Pageable
}

FindPage normalizes page and size, applies limits, and returns page.Page[T].

Important detail:

  • page.Pageable is decoded from meta
  • for REST handlers, ?page=1&size=20 lands in raw params; it does not automatically populate typed page.Pageable

Data Permissions

Many read builders automatically apply request-scoped data-permission filtering through the query layer.

That means:

  • search tags and custom conditions are not the only filters in play
  • data permission may add additional conditions transparently
  • if your query must bypass this behavior, the relevant CRUD builder has to disable it explicitly

Query Escape Hatches

When search tags are not expressive enough, CRUD find builders support these extension points:

MethodUse for
WithCondition(...)additional WHERE conditions
WithRelation(...)relation joins
WithDefaultSort(...)fallback sorting
WithQueryApplier(...)arbitrary typed query customization
WithSelect(...) / WithSelectAs(...)explicit select-list shaping

For tree APIs, these escape hatches can also be targeted at different query parts such as QueryBase, QueryRecursive, and QueryRoot.

Practical Patterns

type UserSearch struct {
api.P

ID string `json:"id" search:"eq"`
Keyword string `json:"keyword" search:"contains,column=username|email"`
}

Range and set filtering

type ProductSearch struct {
api.P

PriceRange string `json:"priceRange" search:"operator=between,column=price,params=type:int,delimiter:,"`
Statuses string `json:"statuses" search:"operator=in,column=status,params=delimiter:|"`
}
type UserSearch struct {
Name string `search:"column=user_name,operator=contains"`
}

type OrderSearch struct {
api.P

User UserSearch `search:"dive"`
}

Practical Advice

  • use a dedicated search struct per resource
  • use search tags for normal filtering and keep query rules next to the field definition
  • prefer explicit multi-column tags for keyword search instead of hidden custom SQL
  • use metadata for sorting and pagination
  • reach for WithQueryApplier(...) only when tag-based configuration is no longer expressive enough
  • keep the query contract visible in the type definition instead of burying it in handler code

Next Step

Read Hooks if your queries or mutations also need lifecycle-aware behavior around CRUD operations.