Filtering with Expressions
The filter_by_* methods generated for indexed fields cover simple equality
lookups. For anything else — comparisons, combining conditions with AND/OR,
checking for null — use Model::filter() with field expressions.
| Expression | Description | Database equivalent |
|---|---|---|
.eq(value) | Equal | = value |
.ne(value) | Not equal | != value |
.gt(value) | Greater than | > value |
.ge(value) | Greater than or equal | >= value |
.lt(value) | Less than | < value |
.le(value) | Less than or equal | <= value |
.in_list([...]) | Value in list | IN (...) |
.is_none() | Null check (Option fields) | IS NULL |
.is_some() | Not-null check (Option fields) | IS NOT NULL |
.starts_with(prefix) | Prefix match | begins_with(field, prefix) / LIKE 'prefix%' |
.like(pattern) | Pattern match, behavior per backend | LIKE pattern |
.ilike(pattern) | Case-insensitive pattern match, PostgreSQL only | ILIKE pattern |
.and(expr) | Both conditions true | AND |
.or(expr) | Either condition true | OR |
.not() / !expr | Negate condition | NOT |
.any(expr) | Any related record matches (HasMany) | IN (SELECT ...) |
.all(expr) | Every related record matches (HasMany) | NOT IN (SELECT ... WHERE NOT ...) |
Field paths
Every model has a fields() method that returns typed accessors for each field.
These accessors produce field paths that you pass to comparison methods:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[index]
country: String,
}
fn __example() {
// User::fields() returns a struct with one method per field
let name_path = User::fields().name();
let country_path = User::fields().country();
}
}
Field paths are the building blocks for filter expressions. Call a comparison
method on a path to get an Expr<bool>, then pass that expression to
Model::filter().
Equality and inequality
.eq() tests whether a field equals a value. .ne() tests whether it does not:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[index]
country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users named "Alice"
let users = User::filter(User::fields().name().eq("Alice"))
.exec(&mut db)
.await?;
// Find users not from the US
let users = User::filter(User::fields().country().ne("US"))
.exec(&mut db)
.await?;
Ok(())
}
}
Ordering comparisons
Four methods compare field values by order:
| Method | Meaning |
|---|---|
.gt(value) | Greater than |
.ge(value) | Greater than or equal |
.lt(value) | Less than |
.le(value) | Less than or equal |
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
#[key]
#[auto]
id: u64,
kind: String,
timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Events after timestamp 1000
let events = Event::filter(Event::fields().timestamp().gt(1000))
.exec(&mut db)
.await?;
// Events at or before timestamp 500
let events = Event::filter(Event::fields().timestamp().le(500))
.exec(&mut db)
.await?;
Ok(())
}
}
Membership with in_list
.in_list() tests whether a field’s value is in a given list, equivalent to
SQL’s IN clause:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[index]
country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = User::filter(
User::fields().country().in_list(["US", "CA", "MX"]),
)
.exec(&mut db)
.await?;
Ok(())
}
}
Null checks
For Option<T> fields, use .is_none() and .is_some() to filter by whether
the value is null:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
bio: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users who have not set a bio
let users = User::filter(User::fields().bio().is_none())
.exec(&mut db)
.await?;
// Users who have set a bio
let users = User::filter(User::fields().bio().is_some())
.exec(&mut db)
.await?;
Ok(())
}
}
These methods are only available on paths to Option<T> fields. Calling
.is_none() on a non-optional field is a compile error.
Combining with AND
.and() combines two expressions so both must be true:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
#[key]
#[auto]
id: u64,
kind: String,
timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let events = Event::filter(
Event::fields()
.kind()
.eq("info")
.and(Event::fields().timestamp().gt(1000)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
Chain multiple .and() calls to add more conditions:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
#[key]
#[auto]
id: u64,
kind: String,
timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let events = Event::filter(
Event::fields()
.kind()
.eq("info")
.and(Event::fields().timestamp().gt(1000))
.and(Event::fields().timestamp().lt(2000)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
You can also add AND conditions by chaining .filter() on a query. Each
.filter() call adds another AND condition:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
#[key]
#[auto]
id: u64,
kind: String,
timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Equivalent to the previous example
let events = Event::filter(Event::fields().kind().eq("info"))
.filter(Event::fields().timestamp().gt(1000))
.filter(Event::fields().timestamp().lt(2000))
.exec(&mut db)
.await?;
Ok(())
}
}
Combining with OR
.or() combines two expressions so at least one must be true:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users named "Alice" or aged 35
let users = User::filter(
User::fields()
.name()
.eq("Alice")
.or(User::fields().age().eq(35)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
Expressions evaluate left to right through method chaining. Each method wraps
everything before it. a.or(b).and(c) produces (a OR b) AND c:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
age: i64,
active: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// (name = "Alice" OR age = 35) AND active = true
let users = User::filter(
User::fields()
.name()
.eq("Alice")
.or(User::fields().age().eq(35))
.and(User::fields().active().eq(true)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
To group differently, build sub-expressions and pass them as arguments. Here,
a.or(b.and(c)) produces a OR (b AND c):
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
age: i64,
active: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// name = "Alice" OR (age = 35 AND active = true)
let users = User::filter(
User::fields().name().eq("Alice").or(User::fields()
.age()
.eq(35)
.and(User::fields().active().eq(true))),
)
.exec(&mut db)
.await?;
Ok(())
}
}
Negation with NOT
.not() negates an expression. The ! operator works too:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users not named "Alice"
let users = User::filter(User::fields().name().eq("Alice").not())
.exec(&mut db)
.await?;
// Same thing with the ! operator
let users = User::filter(!User::fields().name().eq("Alice"))
.exec(&mut db)
.await?;
Ok(())
}
}
NOT works on compound expressions too:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// NOT (name = "Alice" OR name = "Bob")
let users = User::filter(
!(User::fields()
.name()
.eq("Alice")
.or(User::fields().name().eq("Bob"))),
)
.exec(&mut db)
.await?;
Ok(())
}
}
String pattern matching
.like() and .ilike() map directly onto the database’s own LIKE and ILIKE
operators. Toasty passes them through and keeps each backend’s behavior rather
than normalizing it, so .like()’s case sensitivity varies by backend and
.ilike() is only supported where the database has ILIKE — see the sections
below.
starts_with
.starts_with() tests whether a string field starts with the given prefix. It
works on all supported databases — SQL drivers translate it to LIKE 'prefix%',
and DynamoDB uses its native begins_with condition expression:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users whose name starts with "Al"
let users = User::filter(User::fields().name().starts_with("Al"))
.exec(&mut db)
.await?;
Ok(())
}
}
like
.like() tests a string field against a SQL LIKE pattern. % matches any
sequence of characters; _ matches any single character. SQL only —
DynamoDB has no LIKE operator, so calling .like() against it panics in the
driver. Use .starts_with() for prefix matching on DynamoDB.
.like() is a pass-through to the database’s own LIKE, whose case sensitivity
differs between backends:
| Backend | Case behavior of .like() |
|---|---|
| PostgreSQL | Case-sensitive. |
| MySQL | Set by the column’s collation — _ci collations match case-insensitively, _bin and binary collations case-sensitively. |
| SQLite | Case-insensitive for ASCII; case-sensitive for non-ASCII characters. |
For a case-insensitive match on PostgreSQL, use .ilike().
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users whose name matches a pattern
let users = User::filter(User::fields().name().like("Al%"))
.exec(&mut db)
.await?;
Ok(())
}
}
Prefer .starts_with() over .like("prefix%") when you only need a prefix
match — it works across all drivers.
ilike
.ilike() is the case-insensitive form of .like() and maps to PostgreSQL’s
ILIKE operator. PostgreSQL only. Toasty does not emulate ILIKE on
backends that lack it: on MySQL, SQLite, and DynamoDB the query is rejected with
an unsupported_feature error.
PostgreSQL’s LIKE is case-sensitive, so PostgreSQL provides ILIKE for
case-insensitive matching. The other backends have no operator with matching
semantics for .ilike() to pass through to — SQLite’s LIKE already folds ASCII
case, and MySQL’s case behavior is set by the column collation. For
case-insensitive matching there, rely on SQLite’s ASCII folding or pick a
case-folding collation on MySQL (see .like()).
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// On PostgreSQL, matches "Alice", "ALICIA", "alfred", and so on.
let users = User::filter(User::fields().name().ilike("al%".to_string()))
.exec(&mut db)
.await?;
Ok(())
}
}
Filtering on associations
A field path can traverse a relation. The path User::fields().profile()
refers to the user’s HasOne Profile; chaining .score() produces a
path to score on the related profile. Comparison methods on such a
path generate a subquery that filters the parent by the value of a child
field. Same syntax works through BelongsTo and through chains of
HasOne/BelongsTo (e.g., A::fields().b().c().name()). SQL-only.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[has_one]
profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
#[key]
#[auto]
id: u64,
score: i64,
#[unique]
user_id: Option<u64>,
#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users whose profile has a score above 50
let users = User::filter(User::fields().profile().score().gt(50))
.exec(&mut db)
.await?;
Ok(())
}
}
any — at least one match
For HasMany relations, .any() tests whether at least one related record
matches a condition. This generates a subquery:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[has_many]
todos: toasty::HasMany<Todo>,
}
#[derive(Debug, toasty::Model)]
struct Todo {
#[key]
#[auto]
id: u64,
#[index]
user_id: u64,
#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,
title: String,
complete: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users who have at least one incomplete todo
let users = User::filter(
User::fields()
.todos()
.any(Todo::fields().complete().eq(false)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
The path User::fields().todos() refers to the HasMany relation. Calling
.any() on it takes a filter expression on the child model (Todo) and
produces a filter expression on the parent (User).
all — every related record matches
.all() on a HasMany path is the universal counterpart to .any():
it tests whether every related record matches the filter. SQL-only.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto]
id: u64,
name: String,
#[has_many]
todos: toasty::HasMany<Todo>,
}
#[derive(Debug, toasty::Model)]
struct Todo {
#[key]
#[auto]
id: u64,
#[index]
user_id: u64,
#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,
complete: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users whose todos are all complete
let users = User::filter(
User::fields()
.todos()
.all(Todo::fields().complete().eq(true)),
)
.exec(&mut db)
.await?;
Ok(())
}
}
.all() is vacuously true for a parent with no related records — a
user with no todos matches todos().all(...) for any filter. This
mirrors Rust’s [].iter().all(...) semantics.