Skip to content

driver-sql: bulkUpdate is a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it via super.) #13854

Description

@zhuangjianguo

Found while implementing #13435 (driver-memory's bulkUpdate/bulkDelete atomicity fix).

The observation

#13435's dispatch assumed (A2.3) that driver-turso's super.bulkUpdate/super.bulkDelete calls would inherit the InMemoryDriver fix "for free". That assumption is false: TursoDriver extends SqlDriver (packages/drivers/driver-turso/src/turso-driver.ts:270, import { SqlDriver } from '@objectstack/driver-sql'), and SqlDriver implements IDataDriver directly — it has no relationship to InMemoryDriver at all. So TursoDriver's super.bulkUpdate(...)/super.bulkDelete(...) (lines 1263/1268 on origin/main at the time of writing) resolve to SqlDriver's own, separate implementations — completely untouched by the driver-memory fix.

SqlDriver.bulkDelete (packages/drivers/driver-sql/src/sql-driver.ts:7666) is a single builder.whereIn('id', ids).delete() per rotation shard — one SQL statement, atomic on its own (barring cross-shard joint atomicity, a separate/pre-existing concern).

SqlDriver.bulkUpdate (packages/drivers/driver-sql/src/sql-driver.ts:7657) is different:

async bulkUpdate(object: string, updates: Array<{ id: string | number; data: Record<string, any> }>, options?: DriverOptions): Promise<Record<string, any>[]> {
  const results: Record<string, any>[] = [];
  for (const { id, data } of updates) {
    const updated = await this.update(object, id, data, options);
    if (updated) results.push(updated);
  }
  return results;
}

This is a sequential for-await loop over individual update() calls, not wrapped in a transaction. Each update() commits its own row immediately (autocommit). So a batch that refuses partway through — a unique-constraint violation, a missing-id throw, or any other per-row failure — leaves every row processed before the failure already committed to the database. That is the same defect class #13340/#13435 fixed on driver-memory, on driver-sql's (and therefore driver-turso's, via inheritance) bulkUpdate door.

Why this is out of scope for #13435

#13435's dispatch was explicit: "Do NOT edit driver-turso to fix it; if it needs its own change, that is a separate card." The actual defect lives one level up, in driver-sql, and fixing it needs driver-sql's own tools (a transaction wrapper — SqlDriver already has transaction support used elsewhere in the file) rather than anything driver-memory's fix could share.

Suggested direction (not binding)

Wrap SqlDriver.bulkUpdate's loop in a transaction so a mid-batch refusal rolls back every row already applied in that call — the SQL-native equivalent of the check-then-mutate discipline driver-memory now uses. Worth checking whether driver-mongodb's bulkUpdate/bulkDelete have the same shape while scoping this.

Where it is

  • packages/drivers/driver-sql/src/sql-driver.ts:7657SqlDriver.bulkUpdate
  • packages/drivers/driver-turso/src/turso-driver.ts:1256-1263 — inherits the shape via super.bulkUpdate on TursoDriver's local (non-remote) path

Related

#13435 (driver-memory's bulkUpdate/bulkDelete atomicity fix, which found this) · #13340 (driver-memory's bulkCreate atomicity fix, the origin of this defect class)

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions