rent
Learn by example

5. Querying collections

Generated field methods cover comparisons while standard query methods shape the result:

let page = client
    .user()
    .id_ge(2)
    .order_by(user::fields::ID)
    .offset(1)
    .limit(2)
    .all()
    .await?;

let names = client
    .user()
    .order_by(user::fields::ID)
    .select_name()
    .all()
    .await?;

let count = client.user().count().await?;

For alternatives, compose model-safe filters. Ordinary chained filters use AND; this example selects user 1 or 3:

use generated::user_predicate as user_filter;

let selected = client
    .user()
    .where_(user_filter::id_eq(1).or(user_filter::id_eq(3)))
    .order_by(user::fields::ID)
    .all()
    .await?;

Use .and(...) to combine conditions and !filter to negate one. These filters also work for updates and deletes. Mixing a post filter into a user operation is a compile error. Advanced untyped SQL goes through where_raw(expression) and still honors policies.

Inspect the exact policy-aware SQL without executing it. Bind values stay redacted, so the result is safe to attach to logs and bug reports:

let inspection = client
    .user()
    .name_eq("Grace")
    .to_sql()?;

assert!(!inspection.sql.contains("Grace"));
assert_eq!(inspection.bindings, ["string"]);

explain() renders the backend-native query-plan statement. explain_analyze() adds runtime analysis where the database supports it; on SQLite it uses EXPLAIN QUERY PLAN.

let plan = client
    .user()
    .name_eq("Grace")
    .explain()?;

println!("{}", plan.sql);

Inspection describes the root statement. Eager-loaded relationships are separate planned statements when Rent selects the batched loading strategy.

Use a unique field as a keyset cursor instead of making the database skip every earlier row:

let next_page = client
    .user()
    .after_id(2)
    .limit(2)
    .all()
    .await?;

For API boundaries, prefer an opaque cursor. Rent performs a one-row lookahead, returns has_more, and gives you the next typed cursor without exposing its encoding:

let first = client.user().page_by_id(20, None).await?;

let second = client
    .user()
    .page_by_id(20, first.next.as_ref())
    .await?;

Unique compound indexes generate compound cursor methods with lexicographic predicates. Non-unique indexes append the entity ID as a stable tie-breaker.

Select several fields as a typed tuple. Rent fetches only those columns:

let identities: Vec<(i64, String)> = client
    .user()
    .order_by(user::fields::ID)
    .select((user::fields::ID, user::fields::NAME))
    .all()
    .await?;

Large reads can be consumed without collecting the complete result set. Portable generated models stream rows directly from the database. Queries with interceptors, codecs, eager loading, or dynamic decoding transparently use bounded pages, so every query still honors its configured behavior:

use rent::futures_util::TryStreamExt as _;

let users = client
    .user()
    .order_by(user::fields::ID)
    .stream(500)
    .try_collect::<Vec<_>>()
    .await?;

Aggregates remain typed:

let maximum = client
    .user()
    .maximum(user::fields::ID)
    .await?;

let by_name = client
    .user()
    .group_count(user::fields::NAME)
    .await?;

This lesson deliberately returns to one model; see crates/rent/examples/tutorial_04_querying/schema.rsl. Run cargo run -p rent --example tutorial_04_querying. The same query workflow runs under nextest.