or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

column-adapters.mddatabase-driver.mdindex.mdlogging-debugging.mdquery-execution.mdschema-management.mdtransaction-management.md

schema-management.mddocs/

0

# Schema Management

1

2

Database schema creation, versioning, and migration support with callback-based upgrade logic and version management for maintaining database structure consistency.

3

4

## Capabilities

5

6

### SqlSchema Interface

7

8

API for creating and migrating SQL database schemas with version management.

9

10

```kotlin { .api }

11

/**

12

* API for creating and migrating a SQL database. The implementation of this interface

13

* is generated by SQLDelight.

14

* @param T Says whether the generated code for this database is asynchronous or synchronous.

15

* Most implementations of SqlDriver will require QueryResult.Value for synchronous

16

* runtime, QueryResult.AsyncValue for asynchronous runtime

17

*/

18

interface SqlSchema<T : QueryResult<Unit>> {

19

/**

20

* The version of this schema

21

*/

22

val version: Long

23

24

/**

25

* Use driver to create the schema from scratch. Assumes no existing database state

26

* @param driver SqlDriver instance to execute schema creation statements

27

* @return QueryResult indicating completion

28

*/

29

fun create(driver: SqlDriver): T

30

31

/**

32

* Use driver to migrate from schema oldVersion to newVersion.

33

* Each of the callbacks are executed during the migration whenever the upgrade

34

* to the version specified by AfterVersion.afterVersion has been completed

35

* @param driver SqlDriver instance to execute migration statements

36

* @param oldVersion Current schema version in the database

37

* @param newVersion Target schema version to migrate to

38

* @param callbacks Migration callbacks to execute at specific version milestones

39

* @return QueryResult indicating migration completion

40

*/

41

fun migrate(

42

driver: SqlDriver,

43

oldVersion: Long,

44

newVersion: Long,

45

vararg callbacks: AfterVersion

46

): T

47

}

48

```

49

50

### AfterVersion Class

51

52

Represents migration callbacks that execute after reaching specific schema versions.

53

54

```kotlin { .api }

55

/**

56

* Represents a block of code that should be executed during a migration after the migration

57

* has finished migrating to afterVersion

58

* @param afterVersion The schema version after which this callback should execute

59

* @param block Lambda to execute after reaching the specified version

60

*/

61

class AfterVersion(

62

val afterVersion: Long,

63

val block: (SqlDriver) -> Unit

64

)

65

```

66

67

**Usage Examples:**

68

69

```kotlin

70

import app.cash.sqldelight.db.SqlSchema

71

import app.cash.sqldelight.db.SqlDriver

72

import app.cash.sqldelight.db.QueryResult

73

import app.cash.sqldelight.db.AfterVersion

74

75

// Example schema implementation (typically generated by SQLDelight)

76

class UserDatabaseSchema : SqlSchema<QueryResult.Value<Unit>> {

77

override val version: Long = 3L

78

79

override fun create(driver: SqlDriver): QueryResult.Value<Unit> {

80

// Create all tables for the current schema version

81

driver.execute(null, """

82

CREATE TABLE users (

83

id INTEGER PRIMARY KEY,

84

name TEXT NOT NULL,

85

email TEXT NOT NULL UNIQUE,

86

created_at TEXT NOT NULL,

87

status TEXT NOT NULL DEFAULT 'ACTIVE'

88

)

89

""".trimIndent(), 0)

90

91

driver.execute(null, """

92

CREATE TABLE user_profiles (

93

user_id INTEGER PRIMARY KEY,

94

bio TEXT,

95

avatar_url TEXT,

96

last_updated TEXT NOT NULL,

97

FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE

98

)

99

""".trimIndent(), 0)

100

101

driver.execute(null, """

102

CREATE INDEX idx_users_email ON users (email)

103

""".trimIndent(), 0)

104

105

driver.execute(null, """

106

CREATE INDEX idx_users_status ON users (status)

107

""".trimIndent(), 0)

108

109

return QueryResult.Value(Unit)

110

}

111

112

override fun migrate(

113

driver: SqlDriver,

114

oldVersion: Long,

115

newVersion: Long,

116

vararg callbacks: AfterVersion

117

): QueryResult.Value<Unit> {

118

// Migration logic for each version increment

119

var currentVersion = oldVersion

120

121

// Migration from version 1 to 2: Add email column

122

if (currentVersion < 2 && newVersion >= 2) {

123

driver.execute(null, "ALTER TABLE users ADD COLUMN email TEXT", 0)

124

driver.execute(null, "CREATE UNIQUE INDEX idx_users_email ON users (email)", 0)

125

currentVersion = 2

126

127

// Execute callbacks for version 2

128

callbacks.filter { it.afterVersion == 2L }.forEach { callback ->

129

callback.block(driver)

130

}

131

}

132

133

// Migration from version 2 to 3: Add user_profiles table and status column

134

if (currentVersion < 3 && newVersion >= 3) {

135

driver.execute(null, "ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'ACTIVE'", 0)

136

137

driver.execute(null, """

138

CREATE TABLE user_profiles (

139

user_id INTEGER PRIMARY KEY,

140

bio TEXT,

141

avatar_url TEXT,

142

last_updated TEXT NOT NULL,

143

FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE

144

)

145

""".trimIndent(), 0)

146

147

driver.execute(null, "CREATE INDEX idx_users_status ON users (status)", 0)

148

currentVersion = 3

149

150

// Execute callbacks for version 3

151

callbacks.filter { it.afterVersion == 3L }.forEach { callback ->

152

callback.block(driver)

153

}

154

}

155

156

return QueryResult.Value(Unit)

157

}

158

}

159

160

// Database initialization with schema management

161

class UserDatabase private constructor(

162

private val driver: SqlDriver,

163

private val schema: SqlSchema<QueryResult.Value<Unit>>

164

) {

165

companion object {

166

fun create(driver: SqlDriver): UserDatabase {

167

val schema = UserDatabaseSchema()

168

169

// Check if database exists and get current version

170

val currentVersion = getCurrentSchemaVersion(driver)

171

172

if (currentVersion == 0L) {

173

// Fresh database - create schema

174

schema.create(driver)

175

setSchemaVersion(driver, schema.version)

176

} else if (currentVersion < schema.version) {

177

// Existing database needs migration

178

schema.migrate(

179

driver = driver,

180

oldVersion = currentVersion,

181

newVersion = schema.version,

182

// Migration callbacks

183

AfterVersion(2L) { driver ->

184

// Populate email field for existing users

185

driver.execute(null, """

186

UPDATE users

187

SET email = name || '@example.com'

188

WHERE email IS NULL

189

""".trimIndent(), 0)

190

},

191

AfterVersion(3L) { driver ->

192

// Create default profiles for existing users

193

driver.execute(null, """

194

INSERT INTO user_profiles (user_id, last_updated)

195

SELECT id, datetime('now')

196

FROM users

197

WHERE id NOT IN (SELECT user_id FROM user_profiles)

198

""".trimIndent(), 0)

199

}

200

)

201

setSchemaVersion(driver, schema.version)

202

}

203

204

return UserDatabase(driver, schema)

205

}

206

207

private fun getCurrentSchemaVersion(driver: SqlDriver): Long {

208

return try {

209

driver.executeQuery(

210

identifier = null,

211

sql = "PRAGMA user_version",

212

mapper = { cursor ->

213

cursor.next()

214

val version = cursor.getLong(0) ?: 0L

215

QueryResult.Value(version)

216

},

217

parameters = 0

218

).value

219

} catch (e: Exception) {

220

0L // Assume fresh database if version query fails

221

}

222

}

223

224

private fun setSchemaVersion(driver: SqlDriver, version: Long) {

225

driver.execute(

226

identifier = null,

227

sql = "PRAGMA user_version = $version",

228

parameters = 0

229

)

230

}

231

}

232

}

233

234

// Async schema example

235

class AsyncUserDatabaseSchema : SqlSchema<QueryResult.AsyncValue<Unit>> {

236

override val version: Long = 2L

237

238

override fun create(driver: SqlDriver): QueryResult.AsyncValue<Unit> {

239

return QueryResult.AsyncValue {

240

// Async schema creation

241

driver.execute(null, """

242

CREATE TABLE users (

243

id INTEGER PRIMARY KEY,

244

name TEXT NOT NULL,

245

email TEXT NOT NULL UNIQUE

246

)

247

""".trimIndent(), 0).await()

248

249

driver.execute(null, """

250

CREATE INDEX idx_users_email ON users (email)

251

""".trimIndent(), 0).await()

252

}

253

}

254

255

override fun migrate(

256

driver: SqlDriver,

257

oldVersion: Long,

258

newVersion: Long,

259

vararg callbacks: AfterVersion

260

): QueryResult.AsyncValue<Unit> {

261

return QueryResult.AsyncValue {

262

var currentVersion = oldVersion

263

264

if (currentVersion < 2 && newVersion >= 2) {

265

driver.execute(null, "ALTER TABLE users ADD COLUMN email TEXT", 0).await()

266

driver.execute(null, "CREATE UNIQUE INDEX idx_users_email ON users (email)", 0).await()

267

currentVersion = 2

268

269

// Execute async callbacks

270

callbacks.filter { it.afterVersion == 2L }.forEach { callback ->

271

callback.block(driver)

272

}

273

}

274

}

275

}

276

}

277

278

// Complex migration with data transformation

279

class ComplexMigrationSchema : SqlSchema<QueryResult.Value<Unit>> {

280

override val version: Long = 5L

281

282

override fun migrate(

283

driver: SqlDriver,

284

oldVersion: Long,

285

newVersion: Long,

286

vararg callbacks: AfterVersion

287

): QueryResult.Value<Unit> {

288

var currentVersion = oldVersion

289

290

// Version 4: Split name into first_name and last_name

291

if (currentVersion < 4 && newVersion >= 4) {

292

// Add new columns

293

driver.execute(null, "ALTER TABLE users ADD COLUMN first_name TEXT", 0)

294

driver.execute(null, "ALTER TABLE users ADD COLUMN last_name TEXT", 0)

295

296

// Migrate existing data

297

callbacks.filter { it.afterVersion == 4L }.forEach { callback ->

298

callback.block(driver)

299

}

300

301

// Remove old column (SQLite requires table recreation)

302

driver.execute(null, """

303

CREATE TABLE users_new (

304

id INTEGER PRIMARY KEY,

305

first_name TEXT NOT NULL,

306

last_name TEXT NOT NULL,

307

email TEXT NOT NULL UNIQUE,

308

status TEXT NOT NULL DEFAULT 'ACTIVE'

309

)

310

""".trimIndent(), 0)

311

312

driver.execute(null, """

313

INSERT INTO users_new (id, first_name, last_name, email, status)

314

SELECT id, first_name, last_name, email, status FROM users

315

""".trimIndent(), 0)

316

317

driver.execute(null, "DROP TABLE users", 0)

318

driver.execute(null, "ALTER TABLE users_new RENAME TO users", 0)

319

320

currentVersion = 4

321

}

322

323

return QueryResult.Value(Unit)

324

}

325

}

326

327

// Usage with migration callbacks

328

val database = UserDatabase.create(

329

driver = sqlDriver,

330

// Custom migration callbacks can be provided

331

migrationCallbacks = arrayOf(

332

AfterVersion(2L) { driver ->

333

// Custom logic after migrating to version 2

334

println("Migrated to version 2, populating email fields...")

335

// Populate email addresses from external source

336

populateEmailsFromApi(driver)

337

},

338

AfterVersion(3L) { driver ->

339

// Custom logic after migrating to version 3

340

println("Migrated to version 3, creating user profiles...")

341

// Initialize user profiles with default settings

342

initializeUserProfiles(driver)

343

}

344

)

345

)

346

```

347

348

### Migration Best Practices

349

350

1. **Incremental Migrations**: Always migrate one version at a time to ensure data integrity

351

2. **Backup Critical Data**: Use AfterVersion callbacks to backup important data before destructive changes

352

3. **Test Migrations**: Thoroughly test migration paths with realistic data sets

353

4. **Rollback Planning**: Consider how to handle migration failures and potential rollbacks

354

5. **Performance**: For large datasets, consider batching operations and progress tracking

355

6. **Validation**: Use callbacks to validate data integrity after each migration step

356

357

### Version Management

358

359

- **Schema versions** should be incremented for any schema changes

360

- **Version storage** is typically handled using database-specific mechanisms (e.g., PRAGMA user_version in SQLite)

361

- **Version validation** ensures migrations are applied in the correct order

362

- **Callback execution** allows custom logic at specific migration milestones