Builder pattern completeness: Every insert(), update(), delete() operation using the builder pattern records a changelog entry
Returning required for full insert tracking: Inserts with .returning() capture the full row; without .returning(), only the ID is captured from the input values
Update/delete affected rows from the pre-mutation SELECT: Updates and deletes resolve their affected rows (and previousObject) from a pre-mutation SELECT over the where clause, so they record one accurate per-row entry with or without .returning() — they never key an entry off the driver's result-summary object. An update without .returning() reconstructs the new object by overlaying the set values on the previous row. (Requires capturePreviousState true, the default.)
Previous state capture: Updates and deletes capture previousObject via a pre-mutation SELECT (when capturePreviousState is true)
Public entry point: recordExternalMutation(resource, type, { objectId? }) is exported for writers outside the tracked db (cron jobs, other services, manual edits, CDC)
Effects: appends a changelog entry, invalidates the query cache, and sends subscribers an invalidate event
Coarse by default: objectId defaults to "*"; no object/previousObject is carried, so the event is always invalidate
Portable alternative to CDC: this is the supported mechanism for keeping Covara subscriptions/caches consistent when mutations bypass the tracked db
Transaction wrapping: Wrapped transactions track all mutations within them
Commit-gated side effects: Inside a tracked db.transaction(...), changelog entries, subscription pushes, and cache invalidations are buffered and only emitted after the transaction commits
Rollback discards effects: If a transaction rolls back (the callback throws), the buffered side effects are discarded — no changelog entry, subscription event, or cache invalidation is produced for uncommitted state
Engines Without Interactive Transactions (Cloudflare D1)
Auto-detected: D1 has no interactive transactions (drizzle's db.transaction() issues BEGIN/COMMIT, which D1 rejects). The resource layer detects D1 and skips db.transaction(); override with transactions: true | false in the resource config for a custom/unrecognized driver.
Single-statement mutations stay atomic: create / update / replace / delete (and batch update/delete by filter) issue a single write statement, which D1 auto-commits atomically. Changelog/subscription side effects (which are KV writes, never part of DB atomicity) fire exactly once, as on any engine.
Batch upsert uses db.batch(): the multi-row upsert path runs all statements through D1's atomic batch() primitive instead of a transaction.
❌ Nested writes are not atomic on D1: a nestedWrites create chains inserts whose ids feed the next (parent → main → children), which can't be a single batch(). On D1 they run sequentially; a mid-chain failure can leave partial rows.