rent
Learn by example

21. Bulk writes and performance

Build a collection of generated create builders and send them together:

let creates = (1..=2_501_i64)
    .map(|id| {
        client
            .create_user()
            .id(id)
            .name(format!("User {id}"))
    })
    .collect::<Vec<_>>();

let users = client
    .create_user_many(creates)
    .save()
    .await?;

Use .save() when the application needs the inserted models. Use .exec() when it only needs the affected-row count:

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

assert_eq!(result.inserted, 2_501);

The count-only path avoids returning and decoding every inserted row.

On PostgreSQL and SQLite, homogeneous rows use multi-row INSERT ... RETURNING requests and return typed entities. MySQL and MariaDB batches with explicit IDs use a multi-row insert followed by a reload; Rent binds each ID once and restores the caller's order in memory. Batches with database-generated IDs use the safe fallback because assuming a contiguous ID range is unsafe under concurrency. Mixed shapes and SQL expressions also use the safe fallback.

For PostgreSQL ingestion jobs, generated clients also expose the server's native CSV copy protocol:

let result = client
    .copy_user_csv(b"id,name\n8001,Copy one\n8002,Copy two\n")
    .header(true)
    .exec()
    .await?;

assert_eq!(result.inserted, 2);

The CSV columns follow the generated entity column order. COPY is deliberately unavailable when field codecs, application policies, or mutation hooks must inspect individual rows; use create_user_many(...) in those cases.

You do not need to size chunks yourself. Rent plans them from the active dialect's bind-parameter, row-count, and payload limits. A 10,000-row call remains one atomic SDK operation even when it requires several SQL statements: Rent opens a transaction and rolls every chunk back if a later chunk fails.

When no mutation hooks are registered, Rent sends the atomic insert directly and returns the decoded entities without an extra transaction or serialization pass. When hooks are registered, Rent wraps the write and after-hooks in a transaction so a rejected or rewritten result retains the documented rollback behavior.

The bulk operation is atomic. If one row violates a constraint, no row from that collection remains:

let rejected = client
    .create_user_many([
        client
            .create_user()
            .id(101)
            .name("Would be rolled back"),
        client
            .create_user()
            .id(1)
            .name("Duplicate primary key"),
    ])
    .save()
    .await;

assert!(rejected.is_err());

assert!(
    client
        .user()
        .id_eq(101)
        .only_or_none()
        .await?
        .is_none()
);

Run the executable behavior fixture with:

cargo run -p rent --example tutorial_20_bulk_performance

Run the quick same-process benchmark with:

bash scripts/benchmark-quick.sh
Rent versus direct SQLx on sqlite (Criterion sample averages; not request-tail latency)
workload                  sqlx-median  rent-median  median-over  sample-p95  sample-p99
point_lookup                   20.555       23.150       12.63%    16.11%    16.11%
policy_lookup                  21.782       22.261        2.20%    20.38%    20.38%
encrypted_lookup               19.495       23.734       21.74%    29.04%    29.04%
update_one                     19.843       23.608       18.98%    21.65%    21.65%
optimistic_update              41.859       46.859       11.94%    -3.58%    -3.58%
eager_user_posts_comments     209.292      238.102       13.77%    15.87%    15.87%
eager_high_fanout            1162.874     1402.093       20.57%    19.21%    19.21%
stream_1000                   724.451      898.945       24.09%    25.15%    25.15%
bulk_insert_1000             1291.785     1464.704       13.39%   -29.73%   -29.73%
bulk_insert_10000           12054.583    14323.451       18.82%    19.40%    19.40%
bulk_insert_count_10000      3081.905     4508.521       46.29%    67.34%    67.34%

These are illustrative local measurements, not promises for every machine. The harness uses the same process, pool, database, data, returned fields, and async runtime for both paths. Run it on your target database to make deployment decisions. The gate uses workload-specific median ceilings and a sample-average p99 ceiling for simple reads, writes, relationships, streaming, and bulk operations. Criterion times batches of operations: these p95/p99 values describe variation between batch averages, not individual request-tail latency. Use a per-request load test to evaluate application latency SLOs. just benchmark-quick prints this report; BENCHMARKS.md in the repository records the per-database snapshot and the commands behind it.

The executable bulk lesson needs only crates/rent/examples/tutorial_20_bulk_performance/schema.rsl. Run cargo run -p rent --example tutorial_20_bulk_performance; the same workflow runs under nextest.