or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

column-adapters.mddatabase-driver.mdindex.mdlogging-utilities.mdquery-system.mdschema-management.mdtransaction-management.md

transaction-management.mddocs/

0

# Transaction Management

1

2

SQLDelight's transaction management system provides ACID guarantees, nested transaction support, and automatic resource cleanup. It offers both synchronous and coroutine-based asynchronous transaction APIs with comprehensive error handling and lifecycle callbacks.

3

4

## Capabilities

5

6

### Synchronous Transaction Management

7

8

Execute operations within ACID transactions with automatic rollback on exceptions.

9

10

```kotlin { .api }

11

/**

12

* A transaction-aware SqlDriver wrapper which can begin a Transaction on the current connection

13

*/

14

interface Transacter : TransacterBase {

15

/**

16

* Starts a Transaction and runs bodyWithReturn in that transaction

17

* @param noEnclosing If true, throws IllegalStateException if there is already an active Transaction on this thread

18

* @param bodyWithReturn Lambda that executes within the transaction and returns a value

19

* @returns The value returned by bodyWithReturn

20

* @throws IllegalStateException if noEnclosing is true and there is already an active Transaction on this thread

21

*/

22

fun <R> transactionWithResult(

23

noEnclosing: Boolean = false,

24

bodyWithReturn: TransactionWithReturn<R>.() -> R

25

): R

26

27

/**

28

* Starts a Transaction and runs body in that transaction

29

* @param noEnclosing If true, throws IllegalStateException if there is already an active Transaction on this thread

30

* @param body Lambda that executes within the transaction

31

* @throws IllegalStateException if noEnclosing is true and there is already an active Transaction on this thread

32

*/

33

fun transaction(

34

noEnclosing: Boolean = false,

35

body: TransactionWithoutReturn.() -> Unit

36

)

37

}

38

```

39

40

**Usage Examples:**

41

42

```kotlin

43

import app.cash.sqldelight.Transacter

44

45

// Simple transaction without return value

46

database.transaction {

47

userQueries.insertUser("Alice", "alice@example.com")

48

userQueries.updateLastLogin("Alice", System.currentTimeMillis())

49

// Both operations succeed or both are rolled back

50

}

51

52

// Transaction with return value

53

val newUserId: Long = database.transactionWithResult {

54

userQueries.insertUser("Bob", "bob@example.com")

55

val userId = userQueries.lastInsertRowId().executeAsOne()

56

profileQueries.createProfile(userId, "Bob's Profile")

57

userId // Return the new user ID

58

}

59

60

// Explicit rollback with return value

61

val result: String = database.transactionWithResult {

62

userQueries.insertUser("Charlie", "charlie@example.com")

63

val user = userQueries.selectByEmail("charlie@example.com").executeAsOneOrNull()

64

if (user == null) {

65

rollback("User creation failed")

66

}

67

"User created successfully"

68

}

69

70

// Manual rollback without return value

71

database.transaction {

72

val userCount = userQueries.countUsers().executeAsOne()

73

if (userCount > 1000) {

74

rollback() // Stop transaction without creating more users

75

}

76

userQueries.insertUser("David", "david@example.com")

77

}

78

```

79

80

### Asynchronous Transaction Management

81

82

Execute operations within transactions using Kotlin coroutines for non-blocking database access.

83

84

```kotlin { .api }

85

/**

86

* A transaction-aware SqlDriver wrapper which can begin a Transaction on the current connection

87

*/

88

interface SuspendingTransacter : TransacterBase {

89

/**

90

* Starts a Transaction and runs bodyWithReturn in that transaction

91

* @param noEnclosing If true, throws IllegalStateException if there is already an active Transaction on this thread

92

* @param bodyWithReturn Suspending lambda that executes within the transaction and returns a value

93

* @returns The value returned by bodyWithReturn

94

* @throws IllegalStateException if noEnclosing is true and there is already an active Transaction on this thread

95

*/

96

suspend fun <R> transactionWithResult(

97

noEnclosing: Boolean = false,

98

bodyWithReturn: suspend SuspendingTransactionWithReturn<R>.() -> R

99

): R

100

101

/**

102

* Starts a Transaction and runs body in that transaction

103

* @param noEnclosing If true, throws IllegalStateException if there is already an active Transaction on this thread

104

* @param body Suspending lambda that executes within the transaction

105

* @throws IllegalStateException if noEnclosing is true and there is already an active Transaction on this thread

106

*/

107

suspend fun transaction(

108

noEnclosing: Boolean = false,

109

body: suspend SuspendingTransactionWithoutReturn.() -> Unit

110

)

111

}

112

```

113

114

**Usage Examples:**

115

116

```kotlin

117

import app.cash.sqldelight.SuspendingTransacter

118

import kotlinx.coroutines.delay

119

120

// Async transaction without return value

121

suspendingDatabase.transaction {

122

userQueries.insertUser("Alice", "alice@example.com")

123

delay(100) // Simulate async operation

124

userQueries.updateLastLogin("Alice", System.currentTimeMillis())

125

}

126

127

// Async transaction with return value

128

val newUserId: Long = suspendingDatabase.transactionWithResult {

129

userQueries.insertUser("Bob", "bob@example.com")

130

delay(50) // Simulate async validation

131

val userId = userQueries.lastInsertRowId().executeAsOne()

132

profileQueries.createProfile(userId, "Bob's Profile")

133

userId

134

}

135

136

// Conditional rollback in async context

137

val result: String = suspendingDatabase.transactionWithResult {

138

userQueries.insertUser("Charlie", "charlie@example.com")

139

val validationResult = validateUserAsync("charlie@example.com")

140

if (!validationResult.isValid) {

141

rollback("Validation failed: ${validationResult.error}")

142

}

143

"User created successfully"

144

}

145

```

146

147

### Transaction Context Interfaces

148

149

Interfaces that define the transaction execution context and available operations.

150

151

```kotlin { .api }

152

/**

153

* Transaction context that supports returning values and nested transactions

154

*/

155

interface TransactionWithReturn<R> : TransactionCallbacks {

156

/**

157

* Rolls back this transaction with a return value

158

*/

159

fun rollback(returnValue: R): Nothing

160

161

/**

162

* Begin an inner transaction that returns a value

163

*/

164

fun <R> transaction(body: TransactionWithReturn<R>.() -> R): R

165

}

166

167

/**

168

* Transaction context that doesn't return values but supports nested transactions

169

*/

170

interface TransactionWithoutReturn : TransactionCallbacks {

171

/**

172

* Rolls back this transaction

173

*/

174

fun rollback(): Nothing

175

176

/**

177

* Begin an inner transaction without return value

178

*/

179

fun transaction(body: TransactionWithoutReturn.() -> Unit)

180

}

181

182

/**

183

* Suspending transaction context that supports returning values and nested transactions

184

*/

185

interface SuspendingTransactionWithReturn<R> : TransactionCallbacks {

186

/**

187

* Rolls back this transaction with a return value

188

*/

189

fun rollback(returnValue: R): Nothing

190

191

/**

192

* Begin an inner suspending transaction that returns a value

193

*/

194

suspend fun <R> transaction(body: suspend SuspendingTransactionWithReturn<R>.() -> R): R

195

}

196

197

/**

198

* Suspending transaction context that doesn't return values but supports nested transactions

199

*/

200

interface SuspendingTransactionWithoutReturn : TransactionCallbacks {

201

/**

202

* Rolls back this transaction

203

*/

204

fun rollback(): Nothing

205

206

/**

207

* Begin an inner suspending transaction without return value

208

*/

209

suspend fun transaction(body: suspend SuspendingTransactionWithoutReturn.() -> Unit)

210

}

211

```

212

213

### Transaction Implementation Classes

214

215

Base implementation classes for creating custom database classes with transaction support.

216

217

```kotlin { .api }

218

/**

219

* Base implementation for synchronous transaction management

220

*/

221

abstract class TransacterImpl(driver: SqlDriver) : BaseTransacterImpl(driver), Transacter

222

223

/**

224

* Base implementation for asynchronous transaction management

225

*/

226

abstract class SuspendingTransacterImpl(driver: SqlDriver) : BaseTransacterImpl(driver), SuspendingTransacter

227

228

/**

229

* Common base class for both synchronous and asynchronous transacter implementations

230

*/

231

abstract class BaseTransacterImpl(protected val driver: SqlDriver) {

232

/**

233

* Notify query listeners that results have changed for the given identifier and tables

234

*/

235

protected fun notifyQueries(identifier: Int, tableProvider: ((String) -> Unit) -> Unit)

236

237

/**

238

* Create SQL parameter placeholders for the given count

239

*/

240

protected fun createArguments(count: Int): String

241

}

242

```

243

244

**Usage Examples:**

245

246

```kotlin

247

import app.cash.sqldelight.TransacterImpl

248

249

// Custom database implementation

250

abstract class MyDatabase : TransacterImpl(driver) {

251

// Generated query interfaces would be properties here

252

abstract val userQueries: UserQueries

253

abstract val profileQueries: ProfileQueries

254

255

companion object {

256

fun create(driver: SqlDriver): MyDatabase {

257

return MyDatabaseImpl(driver)

258

}

259

}

260

}

261

262

// For async databases

263

abstract class MyAsyncDatabase : SuspendingTransacterImpl(driver) {

264

abstract val userQueries: UserQueries

265

abstract val profileQueries: ProfileQueries

266

267

companion object {

268

fun create(driver: SqlDriver): MyAsyncDatabase {

269

return MyAsyncDatabaseImpl(driver)

270

}

271

}

272

}

273

```

274

275

### Transaction Lifecycle and Callbacks

276

277

Manage transaction lifecycle with commit and rollback callbacks.

278

279

```kotlin { .api }

280

/**

281

* Base interface for transaction callback management

282

*/

283

interface TransactionCallbacks {

284

/**

285

* Queues function to be run after this transaction successfully commits

286

*/

287

fun afterCommit(function: () -> Unit)

288

289

/**

290

* Queues function to be run after this transaction rolls back

291

*/

292

fun afterRollback(function: () -> Unit)

293

}

294

295

/**

296

* A SQL transaction with lifecycle management and thread confinement

297

*/

298

abstract class Transacter.Transaction : TransactionCallbacks {

299

/**

300

* The parent transaction, if there is any

301

*/

302

protected abstract val enclosingTransaction: Transaction?

303

304

/**

305

* Signal to the underlying SQL driver that this transaction should be finished

306

* @param successful Whether the transaction completed successfully or not

307

*/

308

protected abstract fun endTransaction(successful: Boolean): QueryResult<Unit>

309

310

/**

311

* Queues function to be run after this transaction successfully commits

312

*/

313

override fun afterCommit(function: () -> Unit)

314

315

/**

316

* Queues function to be run after this transaction rolls back

317

*/

318

override fun afterRollback(function: () -> Unit)

319

}

320

```

321

322

**Usage Examples:**

323

324

```kotlin

325

import app.cash.sqldelight.TransactionCallbacks

326

327

// Using commit and rollback callbacks

328

database.transaction {

329

afterCommit {

330

println("Transaction committed successfully")

331

notifyUI("Data saved")

332

clearCache()

333

}

334

335

afterRollback {

336

println("Transaction was rolled back")

337

notifyUI("Save failed")

338

resetFormData()

339

}

340

341

userQueries.insertUser("Alice", "alice@example.com")

342

profileQueries.createProfile(userId = 1, name = "Alice Profile")

343

}

344

345

// Multiple callbacks

346

database.transaction {

347

afterCommit {

348

updateSearchIndex()

349

}

350

351

afterCommit {

352

sendNotificationEmail()

353

}

354

355

afterRollback {

356

logError("User creation failed")

357

}

358

359

userQueries.insertUser("Bob", "bob@example.com")

360

}

361

```

362

363

### Nested Transactions

364

365

Execute nested transactions with proper isolation and error handling.

366

367

**Usage Examples:**

368

369

```kotlin

370

// Nested transactions

371

database.transaction {

372

userQueries.insertUser("Parent", "parent@example.com")

373

374

// Inner transaction - will be committed with outer transaction

375

transaction {

376

profileQueries.createProfile(1, "Parent Profile")

377

settingsQueries.createUserSettings(1, theme = "dark")

378

}

379

380

// Another inner transaction

381

val configId: Long = transactionWithResult {

382

configQueries.insertConfig("user_1_config")

383

configQueries.lastInsertRowId().executeAsOne()

384

}

385

386

userQueries.updateConfigId(1, configId)

387

}

388

389

// Independent nested transaction

390

database.transactionWithResult {

391

val userId = userQueries.insertUser("Independent", "independent@example.com")

392

393

// This inner transaction can be rolled back independently

394

try {

395

transaction(noEnclosing = true) {

396

riskyQueries.performRiskyOperation(userId)

397

}

398

} catch (e: IllegalStateException) {

399

// Inner transaction conflicts are handled separately

400

println("Inner transaction failed: ${e.message}")

401

}

402

403

userId

404

}

405

```

406

407

### Transaction Error Handling

408

409

Handle transaction failures and implement proper error recovery strategies.

410

411

**Usage Examples:**

412

413

```kotlin

414

// Basic error handling

415

try {

416

database.transaction {

417

userQueries.insertUser("Test", "test@example.com")

418

// This will cause a constraint violation if email already exists

419

userQueries.insertUser("Test2", "test@example.com")

420

}

421

} catch (e: SQLException) {

422

println("Transaction failed: ${e.message}")

423

// Transaction was automatically rolled back

424

}

425

426

// Error handling with rollback callbacks

427

database.transaction {

428

afterRollback {

429

println("Cleaning up after transaction failure")

430

cleanupTempFiles()

431

resetState()

432

}

433

434

try {

435

userQueries.insertUser("Risky", "risky@example.com")

436

performRiskyOperation()

437

} catch (e: RiskyOperationException) {

438

// Explicit rollback

439

rollback()

440

}

441

}

442

443

// Conditional operations with manual rollback

444

val result = database.transactionWithResult<String> {

445

val user = userQueries.insertUser("Conditional", "conditional@example.com")

446

val validation = validateUser(user)

447

448

if (!validation.isValid) {

449

rollback("Validation failed: ${validation.errors.joinToString()}")

450

}

451

452

"User created successfully"

453

}

454

```

455

456

### Thread Safety and Confinement

457

458

Transaction objects are confined to the thread they were created on and cannot be accessed from other threads. SQLDelight uses internal thread identification mechanisms to enforce this constraint.

459

460

```kotlin { .api }

461

/**

462

* Internal function for thread confinement checking

463

* Used by transaction implementations to ensure thread safety

464

* Note: This is an internal API and should not be used directly

465

*/

466

internal expect fun currentThreadId(): Long

467

```

468

469

**Usage Examples:**

470

471

```kotlin

472

// Correct usage - all operations on same thread

473

database.transaction {

474

userQueries.insertUser("ThreadSafe", "safe@example.com")

475

val userId = userQueries.lastInsertRowId().executeAsOne()

476

profileQueries.createProfile(userId, "Profile")

477

}

478

479

// Incorrect usage - would throw IllegalStateException

480

database.transaction {

481

val transaction = this

482

483

// This would fail - transaction used from different thread

484

Thread {

485

try {

486

transaction.afterCommit {

487

println("This will throw an exception")

488

}

489

} catch (e: IllegalStateException) {

490

println("Cannot access transaction from different thread: ${e.message}")

491

}

492

}.start()

493

}

494

495

// Async operations should use SuspendingTransacter instead

496

suspendingDatabase.transaction {

497

userQueries.insertUser("Async", "async@example.com")

498

// This is safe because the coroutine context maintains thread affinity

499

withContext(Dispatchers.IO) {

500

// Async operations within the same coroutine scope

501

}

502

}

503

```