Search with Filters — FindMany<Model>
A generated model’s list route already takes where/sort query
parameters, but that’s REST-only, untyped (a string grammar), and tied
to a plain CRUD list. FindMany<Model> gives a procedure the same
filtering and sorting capability as a real, typed argument — usable
over REST or RPC, and composable with a procedure’s own business logic
before or after the query runs.
Schema syntax
FindMany<T>is valid only in procedure-argument position — a model field, atypeblock field, or a procedure return type can’t use it.Tmust be a declaredmodel, not atypeblock. Filtering needs a real table’s columns; atypehas none.
What gets generated
For every model in the schema (unconditionally — same asCreate<Model>Input/Update<Model>Input, whether or not any procedure
actually declares a FindMany<Model> argument), the server composer
generates four things:
<Model>Where— one optional filter per filterable scalar field.PostWhere { id: Option<FieldFilterInput<i64>>, title: Option<FieldFilterInput<String>>, ... }<Model>SortField— an enum with one variant per scalar field (every scalar field is sortable; unlike filtering, ordering has no type restriction).<Model>OrderByClause—{ field: <Model>SortField, direction: SortDirection }.<Model>FindManyInput—{ where: Option<<Model>Where>, orderBy: Option<Vec<<Model>OrderByClause>> }, what theFindMany<Model>argument actually decodes into, plus abuild_<model>_query_from_find_many(db, input)function that turns a decoded input into a ready-to-run query builder.
<Model>Where::to_filters() calls straight
into the same FieldRef field accessors (super::post::title(),
super::post::published(), …) the REST ?where= route and every other
typed query already use. A field that’s invalid to filter on is a
compile error (it’s a struct field, not a string a caller could
mistype), not a runtime 400.
In your procedure implementation:
Filter operators
The six named filter types below (StringFilter, NumberFilter, etc.) are
what the TypeScript and Dart generated clients expose — one interface/class
per scalar family, each with only the operators that make sense for it. The
Rust side is different: every <Model>Where field, regardless of scalar
type, is the same single generic FieldFilterInput<V> (as shown above,
PostWhere { id: Option<FieldFilterInput<i64>>, title: Option<FieldFilterInput<String>>, ... })
— there’s one Rust type, not six, and it carries every operator field
(eq, ne, in, lt, lte, gt, gte, contains, starts_with, is_null)
regardless of V, whether or not they’re meaningful for that particular V.
The table below reflects which operators the TypeScript/Dart clients
actually surface per scalar family — it does not describe distinct Rust
types.
Every generated TypeScript/Dart <Model>Where field is one of six shared filter shapes, matching
whichever operators actually make sense for that scalar type:
Notes:
contains/startsWithexist only onStringFilter— the only two typesFieldRef’s own.contains()/.starts_with()are implemented for.isNullonly makes sense (and is only offered by the generated client types) for a field declared?in the schema — a required field is never null, so there’s nothing to test.Json,Bytes, enum, and customtypefields aren’t filterable at all —<Model>Wheresimply has no field for them, matching the untyped REST?where=route’s own coverage.- Relation fields aren’t filterable either —
PostWhereonly ever coversPost’s own scalar columns, notauthor.name.
Wire format
Structured JSON, not a string grammar — aFindMany<Post> argument
serializes as:
null) — the
caller only sends what it’s actually filtering on. Multiple operators
on the same field combine with AND ({ "gte": 10, "lt": 100 } means
“between 10 and 100”); multiple filtered fields also combine with AND.
orderBy is a list of { field, direction } clauses, not a
field-keyed object. A JSON object’s key order isn’t guaranteed to
survive every parser — serde_json::Map alphabetizes keys by default,
and other languages’ map/dict implementations make no ordering promise
either. A list is the only shape that reliably preserves “sort by
published first, then title” instead of silently becoming “sort by
title first.”
This is identical over REST and RPC: on transport rpc, query is
just another key in the unary call’s input object
({"op": "procedure.searchPosts", "input": {"query": {...}}}) — see
RPC transport for the general request/response
envelope this composes with.
Composing with pagination
FindMany<Model> and PageInput
are separate, orthogonal arguments — filtering/sorting is one concern,
pagination is another:
FindMany<'a, M, PK>’s .limit()/.offset() slice the result set, but computing
totalCount for the Page<T> envelope is a second, separate query,
same as a generated @@paged list route’s own handler does it. Reuse
<Model>Where::to_filters() against both the list query and a
.aggregate().count() query so the same predicate applies to both:
Page<T>/PageInfo
contract this mirrors.
Generated clients
Rust (the same generated types the server uses —include_client_schema!
generates the identical <Model>Where/<Model>SortField/
<Model>OrderByClause/<Model>FindManyInput structs, just without the
DB-backed build_<model>_query_from_find_many function, which needs a
live Cratestack handle a pure HTTP client doesn’t have):
PostWhere/PostFindMany interfaces, backed by
shared StringFilter/NumberFilter/BooleanFilter/UuidFilter/
DateTimeFilter/DecimalFilter interfaces (themselves built on shared
EqualityFilter<V>/ComparableFilter<V> base shapes) — hardcoded once
per package, the same way Page/PageInfo/PageInput are:
riverpod presets both emit the same per-model
PostWhere/PostSortField/PostOrderByClause/PostFindMany classes;
under riverpod, every one of them — plus the shared filter classes —
is @MappableClass()-annotated, so a PostFindMany passed as a
@riverpod family-provider argument gets real structural equality
instead of comparing by identity):
What this is not
- not relation-aware —
PostWhereonly coversPost’s own scalar fields. Filtering by a related model’s field (author.name) isn’t supported; build that inside your procedure’s own logic instead. - not
.select/.include—FindMany<Model>coverswhere/orderByonly. Typed field-selection/relation-inclusion builders for procedure arguments are a documented future direction, not implemented today. - not a replacement for the REST list route’s
?where=/?sort=— that untyped string grammar still exists, unchanged, for plain CRUD list routes.FindMany<Model>is specifically for procedures that want the same capability as a typed argument. - not free of the same validation the list route already runs — a
filter that references a field outside
allowed_fields()(a@server_onlyfield, for instance) simply isn’t representable:<Model>Wherehas no field for it, so there’s nothing to reject at runtime.
Read Next
- Pagination —
PageInput/Page<T>, the argument/return-type pairFindMany<Model>composes with - RPC Transport — the request/response envelope
FindMany<Model>arguments travel in over RPC - TypeScript Client Generation and Dart Client Generation — full per-language client codegen coverage