createMany
Create multiple records in a single operation
Create multiple records in one call:
const result = await client.user.createMany({
data: [
{ email: "alice@example.com", name: "Alice" },
{ email: "bob@example.com", name: "Bob" },
{ email: "carol@example.com", name: "Carol" },
],
});
// Result: { count: 3 }
createMany returns a { count }. Each row takes the same data a single
create takes, nested relation writes included — see
Writing relations below.
A direct polymorphic field is the one relation that stays connect-only in a bulk
row: each row may provide connect, which VibORM resolves into that row’s
private type-and-identity membership, and a required polymorphic field must
provide it. See
Root createMany with polymorphic relations.
Writing relations
await client.user.createMany({
data: [
{
email: "alice@example.com",
profile: { create: { bio: "Hi" } }, // one fresh child for this row
team: { connectOrCreate: { // resolved per row, in order
where: { name: "core" },
create: { name: "core" },
} },
},
{
email: "bob@example.com",
team: { connectOrCreate: { // the same team as row 1
where: { name: "core" },
create: { name: "core" },
} },
},
],
});
// Result: { count: 2 } — and one team, not two.
Rows run left to right, each one exactly as if you had called create with
it. That order is the contract, not an artifact: row 2 sees what row 1 wrote,
which is why two rows naming the same connectOrCreate target converge on one
row instead of racing to create two.
What follows from it:
countis the number of root rows inserted — nested children are not counted.- Transaction-capable drivers roll everything back together. A failure in any row undoes every row.
- Drivers with native atomic batches keep the same successful result and row order, with a different failure boundary. Without an interactive transaction, VibORM runs safe ordered segments after each previous batch returns a normalized success. If a later segment fails, earlier segments can remain committed and the error reports committed or possibly committed progress. A driver’s strong committed-segment capability adds callback-before-decode attribution; it is not required for this route. See D1 transactions and batching for one provider-specific example.
- A driver with neither interactive transactions nor native atomic batches rejects
this relation-bearing form. Rows that stay on the grouped
INSERTpath — scalar rows and the direct polymorphicconnectabove — are unaffected.
With skipDuplicates, a conflicting root row suppresses its complete nested
record subtree. VibORM does not apply that row’s relation effects to the
existing record:
await client.user.createMany({
data: [
{
email: "already-exists@example.com",
posts: { create: { title: "also skipped" } },
},
],
skipDuplicates: true,
});
An interactive transaction isolates the complete member with a savepoint. A batch-only route isolates a root-first skippable INSERT, inspects its exact row count, and dispatches descendants only when that root inserted. A zero-row root therefore suppresses its descendants. If a write or nested record series must run before the skippable root, VibORM refuses the shape before any operation write: suppressing that root would otherwise strand the earlier effect.
Explicit $transaction([...]) arrays remain one indivisible atomic batch. They do
not use the progressive fallback and refuse when the dynamic series cannot lower
exactly into that one batch.
Skip duplicates
const result = await client.user.createMany({
data: [
{ email: "alice@example.com", name: "Alice" },
{ email: "alice@example.com", name: "Alice Duplicate" }, // Skipped
],
skipDuplicates: true,
});
// Result: { count: 1 }
skipDuplicates skips unique/primary-key conflicts only. Foreign-key,
NOT NULL, validation, conversion, and other integrity failures still reject
the whole operation on every supported database.
A row containing only database-owned defaults cannot use skipDuplicates.
VibORM rejects that shape before writing because no supported database shares a
duplicate-only DEFAULT VALUES primitive.
For relation-bearing rows, a skipped root suppresses its whole nested subtree. Interactive savepoints and safe root-first batch isolation implement that same meaning; see Writing relations.
Getting the rows back
Add a select and createMany returns the created rows instead of a count. The
return type follows: with select it is an array of the projected rows, without
it, { count }.
const users = await client.user.createMany({
data: [
{ email: "alice@example.com", name: "Alice" },
{ email: "bob@example.com", name: "Bob" },
],
select: { id: true, email: true },
});
// Type: { id: string; email: string }[]
select and skipDuplicates can only be combined on a database with
RETURNING (PostgreSQL, SQLite, LibSQL, D1). On MySQL the combination is
refused with a typed error rather than guessed at, because a skipped insert
cannot be told apart from a fresh one — drop select to get { count }, or
drop skipDuplicates.
When the rows carry relations, the returned rows are read after every row has
finished, in input order, so a child written by a later row cannot leave an
earlier row’s projection stale. VibORM groups those final reads into bounded
set queries, normally one query; large inputs are split only to respect the
driver’s parameter limit. If a later row moved an earlier row’s primary key —
legal whenever a key member is also a foreign key — the read finds nothing,
and the call fails rather than hand back a shorter list. The { count }
form of the same payload succeeds.
What it costs
The scalar path uses one grouped INSERT per contiguous row shape (plus the
skip-duplicate preparation it always used). When that statement would exceed a
driver’s verified bound-parameter limit, VibORM compiles the largest fitting
contiguous chunks and keeps them in the same transaction or native atomic
batch. Counts and returned rows concatenate in input order. A single row that
cannot fit remains indivisible and is rejected before database I/O.
Chunking changes one database-visible detail only for a payload that could not previously execute: a statement-level trigger fires once per chunk. Row-level triggers still fire once per row, and an under-limit row run remains one statement.
On an interactive driver, a relation-bearing call costs one transaction plus,
per row, exactly what that row would have cost as a single create — the same
target probes, the same child writes, in the same order. A no-transaction driver
with native batching sends one atomic batch per safe executable segment instead.
Ten rows that each connect a target issue ten probes, because that is what ten
create calls issue. Adding a select adds one or more bounded set reads at the
end, normally one, rather than one public-result read per row.
Options
await client.user.createMany({
data: [{ ... }], // Required: array of records — the ordinary create data
// shape, nested relation writes included
skipDuplicates: true, // Optional: skip duplicate-key conflicts; with relation
// writes, the complete skipped subtree is suppressed
select: { ... }, // Optional (scalar fields only): return the created rows
// instead of { count }
});