Skip to content

feat: Batch upsert (upsertMany) #60

Description

@teetangh

Problem

There is no bulk upsert operation. Developers must loop over individual upsert calls, resulting in N separate SQL round-trips. This is unacceptable for data sync workflows, CSV imports, and any scenario where hundreds or thousands of rows need to be created-or-updated atomically.

Current pattern (N round-trips)

// Painfully slow: 500 individual upserts = 500 SQL round-trips
for (final record in syncPayload) {
  await _prisma.client.contact.upsert(
    where: ContactWhereUniqueInput(externalId: record.externalId),
    create: ContactCreateInput(
      externalId: record.externalId,
      name: record.name,
      email: record.email,
    ),
    update: ContactUpdateInput(
      name: record.name,
      email: record.email,
    ),
  );
}

Desired pattern (single SQL round-trip)

// Single SQL statement: INSERT ... ON CONFLICT DO UPDATE
final results = await _prisma.client.contact.upsertMany(
  data: syncPayload.map((r) => ContactUpsertInput(
    where: ContactWhereUniqueInput(externalId: r.externalId),
    create: ContactCreateInput(
      externalId: r.externalId,
      name: r.name,
      email: r.email,
    ),
    update: ContactUpdateInput(
      name: r.name,
      email: r.email,
    ),
  )).toList(),
);

Implementation approach

  1. Generate upsertMany on each model delegate accepting a List<ModelUpsertInput>.
  2. In SqlCompiler, emit a single INSERT INTO ... VALUES (...), (...), ... ON CONFLICT (unique_key) DO UPDATE SET ... statement.
    • Detect the conflict target from the where unique input fields.
    • Map update fields to the SET clause using EXCLUDED.column references.
  3. For PostgreSQL, use ON CONFLICT ... DO UPDATE. (MySQL adapter, when available, would use ON DUPLICATE KEY UPDATE.)
  4. Handle RETURNING * to return all upserted rows.
  5. Chunk very large batches (e.g., >1000 rows) to stay within parameter limits, executing multiple statements inside a single transaction.
  6. Add unit tests for: basic bulk upsert, conflict on composite unique keys, partial updates, and chunked batches.

Impact

  • Transforms O(N) round-trips into O(1) for sync and import flows
  • Critical for mobile offline-sync patterns where hundreds of records sync on reconnect
  • PostgreSQL ON CONFLICT is battle-tested and efficient
  • Prisma JS lacks upsertMany — this would be a differentiating feature

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions