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

query

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package query turns a tree of conditions into parameterized SQL.

Callers never build these types by hand. They fill in a generated query struct — q.Params.Equals.Status, q.Params.In.FixtureID — and the generated repository translates it into the tree this package renders. Keeping the translation in generated code rather than doing it by reflection is what lets the caller's side stay fully typed: asking for "created_at contains 3" is not expressible, rather than merely rejected at runtime.

Column names are never taken from user input. They come from generated constants, so this package concatenates them into SQL directly while every value goes through a placeholder.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func JoinSQL

func JoinSQL(joins []Join, args *Args) string

JoinSQL renders the joins, returning empty when there are none.

It has to be rendered before the statement's WHERE clause, because Postgres numbers placeholders in the order they appear in the text and a join stands earlier in the statement than the conditions do.

func OrderSQL

func OrderSQL(terms []Order) string

OrderSQL renders an ordering, returning empty when there is none.

Nulls are left where Postgres puts them: last when ascending, first when descending. A row ordered by a column of a related row it does not have sorts to one end rather than disappearing, which is the whole point of reaching it through a left join.

Types

type Args

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

Args accumulates placeholder values while SQL is rendered.

Postgres numbers its placeholders, so they have to be allocated in the order the fragments are written. Threading one of these through the rendering is simpler than counting after the fact and getting it wrong once.

func NewArgs

func NewArgs() *Args

NewArgs starts an empty argument list.

func (*Args) Len

func (a *Args) Len() int

Len is the number of arguments so far.

func (*Args) Next

func (a *Args) Next(v any) string

Next records a value and returns its placeholder.

func (*Args) Values

func (a *Args) Values() []any

Values returns the collected arguments.

type Cond

type Cond struct {
	// Table qualifies the column. It is empty at the top level, where only one
	// table is in scope, and set inside a subquery, where two are: an
	// unqualified name there resolves to whichever scope happens to have it,
	// which is a silent correlation to the wrong row rather than an error.
	Table  string
	Column string
	Op     Op
	// Value is the operand. It is nil for the null checks, and a slice for the
	// set-membership ones.
	Value any

	// Exists is a condition on a related table. When it is set, the column and
	// the operator are not used.
	Exists *Exists
}

Cond is one comparison against one column, or one correlated subquery.

func Eq

func Eq(column string, v any) Cond

Eq builds an equality condition.

func Gt

func Gt(column string, v any) Cond

Gt builds a greater-than condition.

func Gte

func Gte(column string, v any) Cond

Gte builds a greater-or-equal condition.

func In

func In(column string, v any) Cond

In builds a set-membership condition.

func IsNull

func IsNull(column string) Cond

IsNull builds a presence condition.

func Like

func Like(column string, v any) Cond

Like builds a case-insensitive pattern condition.

func Lt

func Lt(column string, v any) Cond

Lt builds a less-than condition.

func Lte

func Lte(column string, v any) Cond

Lte builds a less-or-equal condition.

func Ne

func Ne(column string, v any) Cond

Ne builds an inequality condition.

func NotIn

func NotIn(column string, v any) Cond

NotIn builds a set-exclusion condition.

func NotLike

func NotLike(column string, v any) Cond

NotLike builds a negated pattern condition.

func NotNull

func NotNull(column string) Cond

NotNull builds an absence condition.

func Related(e Exists) Cond

Related builds a condition on rows of another table.

func (Cond) SQL

func (c Cond) SQL(args *Args) string

SQL renders one condition.

type Exists

type Exists struct {
	// From is the table and alias, or a join chain for a many-to-many. It is
	// generated, never built from anything a client sent.
	From string
	// On correlates the subquery to the outer row, and is generated the same way.
	On string
	// Where is the caller's condition on the related table, its columns
	// qualified with the alias From declares.
	Where Group
	// Not inverts it: no such row rather than at least one.
	//
	// Which is not the same as negating a condition inside Where, and the
	// difference is the one callers get wrong. A negated comparison still needs
	// the row to be there — "there is a related row whose name differs" — so a
	// row with no related row fails it. This asks whether a matching row exists
	// at all, so a row with none satisfies it.
	Not bool
}

Exists is a condition on rows of another table, correlated to this one.

EXISTS (SELECT 1 FROM <From> WHERE <On> AND <Where>)

A subquery rather than a join, because a join to the far side of a has-many multiplies rows: the count would be wrong, the page would be wrong, and putting DISTINCT over the top would make the ordering wrong too. EXISTS asks the only question a filter is asking — is there such a row — and leaves the shape of the result alone.

func (Exists) SQL

func (e Exists) SQL(args *Args) string

SQL renders the subquery.

type Group

type Group struct {
	// Or joins this group's members with OR instead of AND. It applies to the
	// group, not to a member, which is why mixing the two needs a nested group
	// rather than a flag per condition.
	Or     bool
	Conds  []Cond
	Groups []Group
}

Group is a set of conditions combined with AND or OR, plus nested groups. Nesting is what lets the two be mixed to any depth.

Example

A generated repository builds one of these from the caller's typed query struct and renders it into the WHERE clause. Every value becomes a numbered placeholder; only the column names, which come from generated constants, are written into the SQL.

A set membership renders as `= ANY` against a single array parameter rather than an expanded placeholder list, so the statement text does not change with the length of the list.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/query"
)

func main() {
	g := query.And(
		query.Eq("status", "active"),
		query.Gte("score", 10),
		query.In("fixture_id", []int{1, 2, 3}),
	)

	args := query.NewArgs()
	fmt.Println(g.SQL(args))
	fmt.Println(args.Values())

}
Output:
status = $1 AND score >= $2 AND fixture_id = ANY($3)
[active 10 [1 2 3]]

func And

func And(conds ...Cond) Group

And builds a conjunction.

func Or

func Or(conds ...Cond) Group

Or builds a disjunction.

func (*Group) Add

func (g *Group) Add(conds ...Cond)

Add appends conditions, skipping any that are zero.

Skipping is what lets a caller add a condition it may or may not have — the tenant predicate of a table with no tenant column, for instance — without asking first. A subquery has no column of its own, so it is judged by whether it is there.

Example

Add skips a zero condition, which is what lets a repository add a predicate it may or may not have — the tenant filter of a table with no tenant column, say — without asking first.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/query"
)

func main() {
	var g query.Group
	g.Add(query.Eq("tenant_id", "t-1"))
	g.Add(query.Cond{}) // a filter this table does not have
	g.Add(query.NotNull("published_at"))

	args := query.NewArgs()
	fmt.Println(g.SQL(args))

}
Output:
tenant_id = $1 AND published_at IS NOT NULL

func (Group) Empty

func (g Group) Empty() bool

Empty reports whether the group constrains anything.

func (*Group) Nest

func (g *Group) Nest(sub Group)

Nest appends a sub-group, ignoring an empty one.

Example

Mixing AND with OR is what nesting is for: the flag belongs to a group, so the two are combined by putting one inside the other rather than by a precedence rule nobody can see at the call site.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/query"
)

func main() {
	g := query.And(query.Eq("tenant_id", "t-1"))
	g.Nest(query.Or(
		query.Eq("status", "draft"),
		query.IsNull("published_at"),
	))

	args := query.NewArgs()
	fmt.Println(g.SQL(args))
	fmt.Println(args.Values())

}
Output:
tenant_id = $1 AND (status = $2 OR published_at IS NULL)
[t-1 draft]

func (Group) SQL

func (g Group) SQL(args *Args) string

SQL renders a group. The result carries no surrounding parentheses; a caller combining it with other predicates adds its own.

type Join

type Join struct {
	// Table is the table and its alias, and On correlates it to the outer row.
	// Both are generated from the schema, never built from anything a client
	// sent.
	Table string
	On    string

	// Where is what the far side is scoped by: the tenant, and whatever
	// lifecycle predicates it has.
	//
	// It renders into the ON clause rather than the statement's WHERE, and that
	// is not a stylistic choice. A predicate on the joined table in WHERE is
	// false for a row that matched nothing, so it would discard exactly the rows
	// the left join was chosen to keep — an inner join with extra steps.
	Where Group
}

Join is a related table brought into a statement so that its columns can be ordered by.

LEFT and never INNER, because the join is here to reach a value rather than to decide anything: an inner join would drop every row whose foreign key is null, silently turning "order these by their team's name" into "and hide the ones with no team". A filter across a relation is a Cond with an Exists, not one of these — see Exists for why that one is a subquery.

type Op

type Op string

Op is a comparison.

const (
	OpEq      Op = "="
	OpNe      Op = "<>"
	OpGt      Op = ">"
	OpGte     Op = ">="
	OpLt      Op = "<"
	OpLte     Op = "<="
	OpIn      Op = "IN"
	OpNotIn   Op = "NOT IN"
	OpLike    Op = "ILIKE"
	OpNotLike Op = "NOT ILIKE"
	OpIsNull  Op = "IS NULL"
	OpNotNull Op = "IS NOT NULL"
)

The operators, spelled the way they go into the SQL. Two are not quite what their names say: OpLike and OpNotLike render as ILIKE, because a filter a person typed into a search box is not a statement about letter case, and the null checks take no value at all.

type Order

type Order struct {
	// Table qualifies the column, and has to whenever the statement joins: two
	// tables with a created_at make an unqualified one ambiguous, and Postgres
	// is right to refuse it.
	Table  string
	Column string
	Desc   bool
}

Order is one term of an ORDER BY.

type Page

type Page struct {
	Limit  int
	Offset int
}

Page bounds a result set.

func (Page) Clamp

func (p Page) Clamp(defaultLimit, maxLimit int) Page

Clamp applies the defaults and the ceiling.

A limit is always applied. An unbounded list is a production incident waiting for the table to grow, and the caller who omitted it did not mean "all of them" — they just did not think about it.

Example

Clamp is why a generated list cannot return an unbounded result set: the caller who omitted a limit did not mean "all of them", and a table that grows would turn that omission into an incident.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/query"
)

func main() {
	fmt.Println(query.Page{}.Clamp(50, 200))
	fmt.Println(query.Page{Limit: 1000, Offset: 20}.Clamp(50, 200))

}
Output:
{50 0}
{200 20}

func (Page) SQL

func (p Page) SQL(args *Args) string

SQL renders the LIMIT and OFFSET.

Jump to

Keyboard shortcuts

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