upsertMutation
upsertMutation( table: string, fieldset: readonly FieldSpec[], options?: MutationOptions,): { update: string; insert: string }Returns two statements for a non-destructive upsert: the
updateMutation UPDATE plus
a matching INSERT … SELECT … WHERE NOT EXISTS. Run both with the
same JSONB parameter — update first (so changed rows are touched),
then insert (so genuinely new rows are added).
import { upsertMutation } from '@smplcty/db';
const { update, insert } = upsertMutation( 'tares', [ ['plant_id', 'int', true], ['source_id', 'text', true], ['weight', 'numeric'], ], { bulk: true },);
const rows = JSON.stringify(records);await client.query(update, [rows]);await client.query(insert, [rows]);Why WHERE NOT EXISTS instead of plain ON CONFLICT?
Section titled “Why WHERE NOT EXISTS instead of plain ON CONFLICT?”On tables with serial/identity primary keys, an INSERT that attempts
every row and relies on ON CONFLICT to skip duplicates still asks the
sequence for a value on each attempt — so conflicts burn sequence
values and leave gaps in the id space. The WHERE NOT EXISTS guard
generates rows only for keys that genuinely don’t exist yet, so no
sequence value is wasted. It is always emitted and is never replaced by
ON CONFLICT.
Race backstop: onConflict
Section titled “Race backstop: onConflict”WHERE NOT EXISTS and ON CONFLICT DO NOTHING solve different
problems and are complementary, not alternatives:
WHERE NOT EXISTSis the anti-churn guard above — it keeps the INSERT from advancing the sequence for rows that already exist.ON CONFLICT DO NOTHINGcloses a concurrency race: between the not-exists check and the INSERT committing, two writers can both pass the check and then collide on the unique constraint, raisingunique_violation.ON CONFLICT DO NOTHINGswallows that without reintroducing churn — the sequence only advances for rows that actually insert.
Opt in with { onConflict: 'DO NOTHING' } (default: omitted):
const { update, insert } = upsertMutation('tares', fieldset, { bulk: true, onConflict: 'DO NOTHING',});// insert ends with: … where not exists (…) on conflict do nothing returning *A bare ON CONFLICT DO NOTHING (no conflict target) is enough as a race
guard. The option has no effect on the update half.
Options
Section titled “Options”Same MutationOptions
as updateMutation — { bulk: true } drives both halves from a single
jsonb_to_recordset (one round-trip each), and per-field valueExprs
apply to both the UPDATE SET and the INSERT SELECT. upsertMutation
additionally reads { onConflict: 'DO NOTHING' } (see above).