rent
ReferenceRust API

Query builders

Start a query from the singular entity method:

let users = client
    .user()
    .active_eq(true)
    .age_ge(18)
    .order_by(user::fields::CREATED_AT)
    .limit(50)
    .all()
    .await?;

Each additional filter is joined with AND. Generated scalar filters include eq, ne, gt, ge, lt, and le; nullable fields also receive is_null and is_not_null. Type-specific filters add operations such as string, collection, and JSON predicates.

Compose model-safe filters

For alternatives or negation, combine standalone filters. A filter retains the model it belongs to:

use generated::user_predicate as user_filter;

let users = client
    .user()
    .where_(user_filter::id_eq(1).or(user_filter::id_eq(2)))
    .all()
    .await?;

Use .and(other), .or(other), and !filter for boolean composition. The generated UserPredicate::all(filters) and UserPredicate::any(filters) combine collections; an empty all matches every row and an empty any matches none. The same filters work with generated updates and deletes. Passing a post filter to a user operation, or mixing models inside a boolean expression, is a compile error.

Advanced SQL expressions use where_raw(expression) explicitly. This bypasses model/column checking, not runtime policies or interceptors. Prefer generated filters for ordinary application queries.

Cardinality

  • all() returns every matching row.
  • only_or_none() returns zero or one row and still rejects duplicates.
  • only() requires exactly one row and detects both absence and duplicates.
  • count() returns the number of matches.
  • exist() checks for a matching row with a database-side one-row limit. Like count(), it applies filters, not pagination: an earlier limit(0) or offset(...) does not change its answer. It does not load entities or eager relations. Middleware receives QueryOperation::Exists and QueryOutcome::Exists(bool).
  • sum(field), average(field), minimum(field), maximum(field), aggregate(...), and group_count::<K>(field) compute database-side aggregates over the filtered rows.
  • order_by(field), order_by_desc(field), order_by_expression(...), order_by_expression_desc(...), limit(n), and offset(n) shape the result window before any of the terminals above run.

String collection filters accept arrays, slices, owned strings, Cow<str>, and iterators:

let names = ["Ada", "Grace"];

let users = client
    .user()
    .name_in(&names)
    .all()
    .await?;

The same inputs work for name_not_in, standalone predicates, update/delete filters, and relation closures. Owned strings move into the query; borrowed text is copied once, so the query can outlive the input collection. An empty in filter matches nothing; an empty not_in filter imposes no membership restriction.

Relationships and eager loading

let posts = client
    .post()
    .with_author(|author| author)
    .with_comments(|comments| {
        comments
            .order_by(comment::fields::CREATED_AT)
            .with_author(|author| author)
    })
    .all()
    .await?;

for post in posts {
    let author = post.author()?;
    let comments = post.comments()?;
    // Use the already-loaded values without another database request.
}

Calling a relationship accessor that was not loaded returns RelationNotLoaded. This makes accidental N+1 query patterns visible instead of issuing hidden requests.

Relationship predicates and traversals are generated beside eager loading. has_author() keeps rows whose target exists. where_comments(|comments| comments.body_contains("typo")) filters by a predicate over the related rows. query_comments() traverses from the current result set to a CommentQuery over the related rows. select_title() projects one column into a typed single-field query that supports all(), only(), only_or_none(), and to_sql().

Rent chooses the relation loading plan automatically. A direct to-one relationship can use one joined query, while collections and nested relationships use bounded select-in queries that avoid row multiplication. Override the plan when profiling a particular workload:

use rent::runtime::RelationLoadStrategy;

let joined = client
    .post()
    .relation_load_strategy(RelationLoadStrategy::Join)
    .with_author(|author| author)
    .all()
    .await?;

let selected = client
    .post()
    .relation_load_strategy(RelationLoadStrategy::SelectIn)
    .with_author(|author| author)
    .all()
    .await?;

Join is a preference, not permission to change query semantics. Rent falls back to select-in loading for nested, paginated, ordered collection, and many-to-many shapes that cannot be joined safely.

Inspect SQL and query plans

to_sql() applies configured policies and interceptors, renders backend-native placeholders, and returns redacted bind kinds instead of secrets:

let inspection = client
    .user()
    .email_eq("ada@example.com")
    .to_sql()?;

assert!(!inspection.sql.contains("ada@example.com"));
assert_eq!(inspection.bindings, ["string"]);

Use explain() for a backend-native plan statement and explain_analyze() when runtime measurements are needed. The latter executes a query when the rendered SQL is submitted to PostgreSQL or MySQL; generated builders only inspect SELECT statements.

Projection

let identities = client
    .user()
    .select((user::fields::ID, user::fields::EMAIL))
    .all()
    .await?;

Single fields and typed tuples return only the selected columns. Use complete models when relationships will be loaded.

Generated projections decode supported built-in values directly from database rows; SQLite uses compact JSON for wider tuples to reduce transfer overhead. Nullable fields return Option<T>, and tuple values follow the order of the selected fields. Registered field codecs, interceptors, custom Rust types, and SQL storage overrides use the dynamic decoding path so their behavior remains intact. Policies apply to both paths, including inside transactions.

Call to_sql() on a projection to inspect the actual selected columns. Use only() when exactly one row is required, or only_or_none() for zero or one; both reject multiple matches rather than silently taking the first.

Pagination and streaming

Offset pagination is useful for small administrative lists. Keyset pagination is stable for feeds:

let page = client
    .post()
    .page_by_id(50, after)
    .await?;

let next = page.next;

page_by_id orders by ascending ID and its cursor contains that ID. Use the corresponding generated compound cursor when ordering by multiple fields. A conflicting sort or nonzero offset returns InvalidCursorOrdering; this also applies when ordering is changed after after_id or before_id. Filters compose normally with cursor pagination. before_id uses descending ID order.

For a large export, use a bounded stream so memory does not grow with the result:

use rent::futures_util::TryStreamExt as _;

let mut rows = client
    .post()
    .order_by(post::fields::ID)
    .stream(500);

while let Some(post) = rows.try_next().await? {
    export(post).await?;
}

Reusable expressions

Compact filters are preferred for ordinary code. Predicate modules are useful when an expression must be stored or combined dynamically:

let visible = PostPredicate::any([
    post_predicate::published_eq(true),
    post_predicate::author_id_eq(viewer_id),
]);

let posts = client
    .post()
    .where_(visible)
    .all()
    .await?;

On this page