Transactions

Sqkon exposes SQLDelight’s transaction API directly: KeyValueStorage<T> implements Transacter, so you can wrap multiple writes — across one store or many — in a single atomic block.

  1. When to use a transaction
  2. API
  3. What’s already atomic
  4. Flow emission timing
  5. Synchronous transactions
  6. See also

When to use a transaction

Reach for an explicit transaction { … } block when you need:

  • Atomic multi-write across stores (“debit one account, credit another” semantics).
  • A single Flow emission for a logically-grouped set of writes — observers see one emission per committed transaction, not one per write.
  • Read-then-write consistency where another writer must not slip in between the read and the write.

For single-row writes you don’t need anything; the per-call methods are already transactional.

API

KeyValueStorage<T> is Transacter by transacter, which means the standard SQLDelight transaction API is available on every store:

merchants.transaction {
    merchants.deleteAll()
    merchants.insertAll(fresh.associateBy { it.id })
}

// And the value-returning variant
val count = merchants.transactionWithResult {
    merchants.deleteAll()
    merchants.insertAll(fresh.associateBy { it.id })
    fresh.size
}

Two stores backed by the same Sqkon instance share a transactor — wrap them together for cross-store atomicity:

merchants.transaction {
    merchants.upsert(merchant.id, merchant)
    transactions.upsert(txn.id, txn)
}
// Both writes commit as one unit; observers on either store see one emission.

What’s already atomic

Every public write method on KeyValueStorage wraps its work in transaction { … } internally. You do not need to add an outer transaction for these:

  • insert, insertAll
  • update, updateAll
  • upsert, upsertAll
  • delete (by Where), deleteByKey, deleteByKeys, deleteAll
  • deleteExpired, deleteStale

Wrapping them in your own transaction { … } is harmless — SQLDelight nests — and is sometimes useful for grouping unrelated writes into one observer emission.

Flow emission timing

Flows emit after the transaction commits, not while it’s running. Inside a transaction { … } you can perform multiple writes and observers will see a single re-execution once the block returns. See Reactive flows for the underlying notification mechanism.

Synchronous transactions

Since PR #22 (commit 444823c), Sqkon’s transaction { … } blocks run synchronously on the calling thread, the same as SQLDelight upstream. If you’re upgrading from a pre-444823c version that ran transactions on a write dispatcher, move any runBlocking { … } or withContext(Dispatchers.IO) { … } you added around transaction calls — the block already executes on whatever thread called it.

The tradeoff is intentional: synchronous semantics make ordering predictable and prevent the ThreadLocal-based transaction tracking from getting muddled by suspension points. For long-running write batches, dispatch the entire operation onto a background thread yourself.

See also

  • Reactive flows — when emissions fire relative to commits.
  • Upstream API: SQLDelight Transacter.
  • Source: library/src/commonMain/kotlin/com/mercury/sqkon/db/utils/SqkonTransacter.kt, library/src/commonMain/kotlin/com/mercury/sqkon/db/KeyValueStorage.kt.