zanith

Insert & Upsert

Single insert, bulk insert, and upsert (ON CONFLICT) — all through the InsertBuilder. These are PostgreSQL-native operations, not hacked abstractions.

Two ways to insert

For simple single-record inserts, use db.user.create(). For bulk inserts, upserts (ON CONFLICT), or shaped RETURNING, use the InsertBuilder via db.user.insert().

create() — simple

  • ·Single record only
  • ·Always returns full record
  • ·No conflict handling
  • ·Simplest API

insert() — powerful

  • ·Single or bulk insert
  • ·ON CONFLICT (upsert)
  • ·Shaped RETURNING
  • ·InsertBuilder pattern

Single insert

TS
const user = await db.user
.insert({ email: '[email protected]', name: 'Alice', role: 'USER' })
.execute();
// Returns the created record (RETURNING *)

Bulk insert

Insert multiple rows in a single SQL statement. This is much faster than inserting one at a time — one round trip instead of N.

TS
const users = await db.user.insert([
{ email: '[email protected]', name: 'Alice', role: 'USER' },
{ email: '[email protected]', name: 'Bob', role: 'ADMIN' },
{ email: '[email protected]', name: 'Charlie', role: 'USER' },
]).executeMany();
 
// Generated SQL:
// INSERT INTO "users" ("email", "name", "role")
// VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9)
// RETURNING *

Upsert (ON CONFLICT DO UPDATE)

Insert a record, but if it conflicts with a unique constraint, update instead. This is PostgreSQL's native upsert — atomic, race-condition-free, and efficient. No "check if exists then insert or update" pattern needed.

TSupsert by email
await db.user
.insert({ email: '[email protected]', name: 'Alice' })
.onConflict({
columns: ['email'], // which unique constraint to check
action: 'update', // what to do on conflict
set: { name: 'Alice Updated' }, // which fields to update
})
.execute();
 
// Generated SQL:
// INSERT INTO "users" ("email", "name")
// VALUES ($1, $2)
// ON CONFLICT ("email") DO UPDATE SET "name" = $3
// RETURNING *

ON CONFLICT DO NOTHING

Skip the insert silently if it would conflict. Useful for idempotent operations — "insert this if it doesn't exist, otherwise do nothing."

TS
await db.user
.insert({ email: '[email protected]', name: 'Alice' })
.onConflict({ columns: ['email'], action: 'nothing' })
.execute();
 
// If email '[email protected]' already exists → nothing happens
// If it doesn't exist → record is inserted

Shaped RETURNING

By default, RETURNING * returns all columns. Use .returning()to specify exactly which columns you want back — useful when you only need the generated ID and don't want to transfer the entire record.

TS
const result = await db.user
.insert({ email: '[email protected]', name: 'X' })
.returning(['id', 'email'])
.execute();
 
// result: { id: '...', email: '[email protected]' } — no name, no timestamps
 
// Or return everything:
.returning('*') // this is the default

Method reference

MethodDescription
db.model.insert(data)Start an insert operation (single or array)
.onConflict({ columns, action, set? })Handle unique constraint conflicts
.returning(cols)Specify which columns to return (default: '*')
.execute()Execute and return first row (single insert)
.executeMany()Execute and return all rows (bulk insert)
.toSQL()Compile to { sql, params } without executing