rent
ReferenceRust API

Mutation builders

Builder setters only collect values. The database request begins at save(), save_one(), or exec().

let post = client
    .create_post()
    .title("A typed database client")
    .body("Built in Rust")
    .connect_author(&author)
    .save()
    .await?;

The setters do not cause multiple round trips. PostgreSQL and SQLite return the model from the insert request; MySQL and MariaDB reload an auto-identified row when the server cannot return its complete shape.

create_post_from(NewPost { .. }) takes the generated typed input for the required fields, so a missing value fails at compile time. Nested writes use create_author(...), create_tags(...), connect_tags_entities(...), set_tags(...), and sync_tags_ids(...). on_conflict(target) turns the create into an upsert; upsert_post(target) is the same builder with the conflict target preset, and the _raw forms of both take raw column names.

Update one row

let post = client
    .update_post_one(&post)
    .title("A faster typed database client")
    .save_one()
    .await?;

An entity-targeted update carries its ID. If the schema has a version field, it also compares the old version and increments it atomically. update_post().one(id) targets one row by ID without an entity value, and expect_version(v) on either form checks a specific version. delete_post_one(&post) and delete_post().one(id) are the matching one-row deletes. Many-to-many links can also be written by ID with add_post_tags(post_id, tag_id), remove_post_tags(...), connect_post_tags(...), and disconnect_post_tags(...).

Use a filtered builder for set-based updates:

let changed = client
    .update_post()
    .author_id_eq(author.id)
    .published_eq(false)
    .published(true)
    .save()
    .await?;

Delete

let removed = client
    .delete_comment()
    .post_id_eq(post.id)
    .exec()
    .await?;

Database foreign keys determine cascade, restrict, and SET NULL behavior.

Upsert

let membership = client
    .upsert_membership((
        membership::fields::USER_ID,
        membership::fields::GROUP_ID,
    ))
    .user_id(user.id)
    .group_id(group.id)
    .role("owner")
    .save()
    .await?;

The typed conflict target can be one field or a tuple of fields from the same entity. It should match a unique index or primary key.

An upsert preserves the existing row's primary key, including every component of a compound key. A supplied or automatically generated candidate ID is used only if the insert creates a new row. This lets you upsert by a unique email or slug without breaking relationships to the existing row. Other incoming values, including evaluated client defaults, update the matching row; omitted database-defaulted fields keep their current values. Fields declared @rent.immutable retain their existing values on conflict, even when a different insert value is supplied. A version field advances atomically on conflict; newly inserted records use their create default.

An upsert is an unconditional insert-or-update, not a stale-write check. Use update_post_one(&post) when you need compare-and-swap behavior. Generated filtered updates also advance version fields for every matched row, so earlier entity snapshots become stale after a bulk edit. Empty updates without an explicit expected version remain no-ops. Raw SQL writers must maintain the version themselves. An exhausted version counter rejects the statement instead of wrapping or silently switching to floating-point storage; database rejection leaves the row unchanged, and a failed set-based statement rolls back all of that statement's row changes.

Bulk create

let created = client
    .create_post_many([
        client.create_post().title("First").connect_author(&user),
        client.create_post().title("Second").connect_author(&user),
    ])
    .save()
    .await?;

Rent groups compatible rows, bounds statement parameters and payload size, and wraps chunked execution in one transaction. A failure in a later chunk rolls back earlier chunks.

If only the count matters, skip returning and decoding the inserted models:

let result = client
    .create_post_many(creates)
    .exec()
    .await?;

assert_eq!(result.inserted, expected);

PostgreSQL ingestion jobs can use the generated native CSV COPY builder:

let result = client
    .copy_user_csv(csv_bytes)
    .header(true)
    .exec()
    .await?;

COPY follows generated column order and is intentionally rejected when field codecs, application policies, or mutation hooks need to inspect individual rows.

Nested relationship writes are also transactional. Use explicit foreign-key setters when no related create is needed; use generated connect_*, create_*, and relationship mutation methods for typed relationship changes.

Relationship mutations

To-one setters accept either a loaded entity or only its ID:

let draft = client
    .create_post()
    .title("Draft")
    .connect_author(&author)
    .save()
    .await?;

let imported = client
    .create_post()
    .title("Imported")
    .connect_author_id(author_id)
    .save()
    .await?;

Collection setters distinguish replacement from incremental changes:

let post = client
    .update_post_one(&post)
    .set_tags([&rust, &databases])
    .save_one()
    .await?;

let post = client
    .update_post_one(&post)
    .connect_tags([new_tag_id])
    .disconnect_tags([old_tag_id])
    .save_one()
    .await?;

set_tags and sync_tags_ids clear and rebuild the join rows inside the same transaction as scalar changes.

On this page