Quarry Docs
GitHub
Example

Search and Filter Examples

This is the shape you reach for when a page has half a dozen optional inputs: text search, facet filters, safe sort keys, and pagination that should not change the query into a string puzzle.

Quarry logo

Directory search

type EmployeeSearch struct {
    TenantID   int
    Query      string
    Departments []string
    Status     *string
    Sort       string
    Page       int
    PerPage    int
}

func anySlice[T any](values []T) []any {
    out := make([]any, 0, len(values))
    for _, v := range values {
        out = append(out, v)
    }
    return out
}

func BuildEmployeeSearch(qq *quarry.Quarry, req EmployeeSearch) quarry.SQLer {
    return qq.Select(
        "id", "email", "department", "status", "created_at",
    ).
        From("employees").
        Where(
            quarry.Eq("tenant_id", req.TenantID),
            quarry.Or(
                quarry.OptionalILike("email", req.Query),
                quarry.OptionalILike("name", req.Query),
            ),
            quarry.OptionalEq("status", req.Status),
            quarry.OptionalIn("department", anySlice(req.Departments)...),
        ).
        OrderBySafeDefault(req.Sort, quarry.SortMap{
            "newest": "created_at DESC",
            "email":  "email ASC",
            "name":   "name ASC",
        }, "newest").
        Page(req.Page, req.PerPage)
}
Why this shape works Empty strings, nil pointers, and empty slices disappear. The query stays stable, and the SQL text only changes where the user actually supplied something.

Facet filters and ranges

type TicketSearch struct {
    TenantID   int
    Owners     []int
    CreatedMin  *time.Time
    CreatedMax  *time.Time
    Escalated  *bool
}

q := qq.Select("id", "subject", "owner_id", "created_at").
    From("tickets").
    Where(
        quarry.Eq("tenant_id", req.TenantID),
        quarry.OptionalIn("owner_id", anySlice(req.Owners)...),
        quarry.WhereIf(req.CreatedMin != nil && req.CreatedMax != nil,
            quarry.Between("created_at", *req.CreatedMin, *req.CreatedMax),
        ),
        quarry.OptionalEq("escalated", req.Escalated),
    )
SELECT id, subject, owner_id, created_at
FROM tickets
WHERE tenant_id = $1
  AND owner_id IN ($2, $3, $4)
  AND created_at BETWEEN $5 AND $6

Composite matching

When a search UI is really looking for a set of known combinations, TupleIn keeps the intent obvious.

q := qq.Select("account_id", "user_id", "role").
    From("account_memberships").
    Where(quarry.TupleIn([]any{
        quarry.C("account_id"),
        quarry.C("user_id"),
    }, [][]any{
        {42, 9},
        {42, 11},
        {42, 19},
    }))
Use it for sets, not everything Tuple predicates are handy when the combinations are known and finite. If the filter is really free-form, keep using the simpler optional predicates.

What the SQL looks like

SELECT id, email, department, status, created_at
FROM employees
WHERE tenant_id = $1
  AND (LOWER(email) LIKE LOWER($2) OR LOWER(name) LIKE LOWER($3))
  AND status = $4
ORDER BY created_at DESC
LIMIT 50 OFFSET 0

The important part is not the exact values. It is that each optional clause appears or disappears without changing the rest of the builder.