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_performanceRun the quick same-process benchmark with:
bash scripts/benchmark-quick.shRent 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.