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 SqlDriverimplements 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:
asyncbulkUpdate(object: string,updates: Array<{id: string|number;data: Record<string,any>}>,options?: DriverOptions): Promise<Record<string,any>[]>{constresults: Record<string,any>[]=[];for(const{ id, data }ofupdates){constupdated=awaitthis.update(object,id,data,options);if(updated)results.push(updated);}returnresults;}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:7657 — SqlDriver.bulkUpdatepackages/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)
Found while implementing #13435 (
driver-memory'sbulkUpdate/bulkDeleteatomicity fix).The observation
#13435's dispatch assumed (A2.3) thatdriver-turso'ssuper.bulkUpdate/super.bulkDeletecalls would inherit theInMemoryDriverfix "for free". That assumption is false:TursoDriver extends SqlDriver(packages/drivers/driver-turso/src/turso-driver.ts:270,import { SqlDriver } from '@objectstack/driver-sql'), andSqlDriverimplements IDataDriverdirectly — it has no relationship toInMemoryDriverat all. SoTursoDriver'ssuper.bulkUpdate(...)/super.bulkDelete(...)(lines 1263/1268 onorigin/mainat the time of writing) resolve toSqlDriver's own, separate implementations — completely untouched by thedriver-memoryfix.SqlDriver.bulkDelete(packages/drivers/driver-sql/src/sql-driver.ts:7666) is a singlebuilder.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:This is a sequential
for-awaitloop over individualupdate()calls, not wrapped in a transaction. Eachupdate()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 ondriver-memory, ondriver-sql's (and thereforedriver-turso's, via inheritance)bulkUpdatedoor.Why this is out of scope for #13435
#13435's dispatch was explicit: "Do NOT edit
driver-tursoto fix it; if it needs its own change, that is a separate card." The actual defect lives one level up, indriver-sql, and fixing it needsdriver-sql's own tools (a transaction wrapper —SqlDriveralready has transaction support used elsewhere in the file) rather than anythingdriver-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 disciplinedriver-memorynow uses. Worth checking whetherdriver-mongodb'sbulkUpdate/bulkDeletehave the same shape while scoping this.Where it is
packages/drivers/driver-sql/src/sql-driver.ts:7657—SqlDriver.bulkUpdatepackages/drivers/driver-turso/src/turso-driver.ts:1256-1263— inherits the shape viasuper.bulkUpdateon TursoDriver's local (non-remote) pathRelated
#13435 (
driver-memory'sbulkUpdate/bulkDeleteatomicity fix, which found this) · #13340 (driver-memory'sbulkCreateatomicity fix, the origin of this defect class)