SQLDelight multiplatform runtime library providing Kotlin APIs for type-safe database operations with compile-time SQL verification
—
Database schema creation, versioning, and migration support with callback-based upgrade logic and version management for maintaining database structure consistency.
API for creating and migrating SQL database schemas with version management.
/**
* API for creating and migrating a SQL database. The implementation of this interface
* is generated by SQLDelight.
* @param T Says whether the generated code for this database is asynchronous or synchronous.
* Most implementations of SqlDriver will require QueryResult.Value for synchronous
* runtime, QueryResult.AsyncValue for asynchronous runtime
*/
interface SqlSchema<T : QueryResult<Unit>> {
/**
* The version of this schema
*/
val version: Long
/**
* Use driver to create the schema from scratch. Assumes no existing database state
* @param driver SqlDriver instance to execute schema creation statements
* @return QueryResult indicating completion
*/
fun create(driver: SqlDriver): T
/**
* Use driver to migrate from schema oldVersion to newVersion.
* Each of the callbacks are executed during the migration whenever the upgrade
* to the version specified by AfterVersion.afterVersion has been completed
* @param driver SqlDriver instance to execute migration statements
* @param oldVersion Current schema version in the database
* @param newVersion Target schema version to migrate to
* @param callbacks Migration callbacks to execute at specific version milestones
* @return QueryResult indicating migration completion
*/
fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion
): T
}Represents migration callbacks that execute after reaching specific schema versions.
/**
* Represents a block of code that should be executed during a migration after the migration
* has finished migrating to afterVersion
* @param afterVersion The schema version after which this callback should execute
* @param block Lambda to execute after reaching the specified version
*/
class AfterVersion(
val afterVersion: Long,
val block: (SqlDriver) -> Unit
)Usage Examples:
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.AfterVersion
// Example schema implementation (typically generated by SQLDelight)
class UserDatabaseSchema : SqlSchema<QueryResult.Value<Unit>> {
override val version: Long = 3L
override fun create(driver: SqlDriver): QueryResult.Value<Unit> {
// Create all tables for the current schema version
driver.execute(null, """
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'ACTIVE'
)
""".trimIndent(), 0)
driver.execute(null, """
CREATE TABLE user_profiles (
user_id INTEGER PRIMARY KEY,
bio TEXT,
avatar_url TEXT,
last_updated TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
""".trimIndent(), 0)
driver.execute(null, """
CREATE INDEX idx_users_email ON users (email)
""".trimIndent(), 0)
driver.execute(null, """
CREATE INDEX idx_users_status ON users (status)
""".trimIndent(), 0)
return QueryResult.Value(Unit)
}
override fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion
): QueryResult.Value<Unit> {
// Migration logic for each version increment
var currentVersion = oldVersion
// Migration from version 1 to 2: Add email column
if (currentVersion < 2 && newVersion >= 2) {
driver.execute(null, "ALTER TABLE users ADD COLUMN email TEXT", 0)
driver.execute(null, "CREATE UNIQUE INDEX idx_users_email ON users (email)", 0)
currentVersion = 2
// Execute callbacks for version 2
callbacks.filter { it.afterVersion == 2L }.forEach { callback ->
callback.block(driver)
}
}
// Migration from version 2 to 3: Add user_profiles table and status column
if (currentVersion < 3 && newVersion >= 3) {
driver.execute(null, "ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'ACTIVE'", 0)
driver.execute(null, """
CREATE TABLE user_profiles (
user_id INTEGER PRIMARY KEY,
bio TEXT,
avatar_url TEXT,
last_updated TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
""".trimIndent(), 0)
driver.execute(null, "CREATE INDEX idx_users_status ON users (status)", 0)
currentVersion = 3
// Execute callbacks for version 3
callbacks.filter { it.afterVersion == 3L }.forEach { callback ->
callback.block(driver)
}
}
return QueryResult.Value(Unit)
}
}
// Database initialization with schema management
class UserDatabase private constructor(
private val driver: SqlDriver,
private val schema: SqlSchema<QueryResult.Value<Unit>>
) {
companion object {
fun create(driver: SqlDriver): UserDatabase {
val schema = UserDatabaseSchema()
// Check if database exists and get current version
val currentVersion = getCurrentSchemaVersion(driver)
if (currentVersion == 0L) {
// Fresh database - create schema
schema.create(driver)
setSchemaVersion(driver, schema.version)
} else if (currentVersion < schema.version) {
// Existing database needs migration
schema.migrate(
driver = driver,
oldVersion = currentVersion,
newVersion = schema.version,
// Migration callbacks
AfterVersion(2L) { driver ->
// Populate email field for existing users
driver.execute(null, """
UPDATE users
SET email = name || '@example.com'
WHERE email IS NULL
""".trimIndent(), 0)
},
AfterVersion(3L) { driver ->
// Create default profiles for existing users
driver.execute(null, """
INSERT INTO user_profiles (user_id, last_updated)
SELECT id, datetime('now')
FROM users
WHERE id NOT IN (SELECT user_id FROM user_profiles)
""".trimIndent(), 0)
}
)
setSchemaVersion(driver, schema.version)
}
return UserDatabase(driver, schema)
}
private fun getCurrentSchemaVersion(driver: SqlDriver): Long {
return try {
driver.executeQuery(
identifier = null,
sql = "PRAGMA user_version",
mapper = { cursor ->
cursor.next()
val version = cursor.getLong(0) ?: 0L
QueryResult.Value(version)
},
parameters = 0
).value
} catch (e: Exception) {
0L // Assume fresh database if version query fails
}
}
private fun setSchemaVersion(driver: SqlDriver, version: Long) {
driver.execute(
identifier = null,
sql = "PRAGMA user_version = $version",
parameters = 0
)
}
}
}
// Async schema example
class AsyncUserDatabaseSchema : SqlSchema<QueryResult.AsyncValue<Unit>> {
override val version: Long = 2L
override fun create(driver: SqlDriver): QueryResult.AsyncValue<Unit> {
return QueryResult.AsyncValue {
// Async schema creation
driver.execute(null, """
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
""".trimIndent(), 0).await()
driver.execute(null, """
CREATE INDEX idx_users_email ON users (email)
""".trimIndent(), 0).await()
}
}
override fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion
): QueryResult.AsyncValue<Unit> {
return QueryResult.AsyncValue {
var currentVersion = oldVersion
if (currentVersion < 2 && newVersion >= 2) {
driver.execute(null, "ALTER TABLE users ADD COLUMN email TEXT", 0).await()
driver.execute(null, "CREATE UNIQUE INDEX idx_users_email ON users (email)", 0).await()
currentVersion = 2
// Execute async callbacks
callbacks.filter { it.afterVersion == 2L }.forEach { callback ->
callback.block(driver)
}
}
}
}
}
// Complex migration with data transformation
class ComplexMigrationSchema : SqlSchema<QueryResult.Value<Unit>> {
override val version: Long = 5L
override fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion
): QueryResult.Value<Unit> {
var currentVersion = oldVersion
// Version 4: Split name into first_name and last_name
if (currentVersion < 4 && newVersion >= 4) {
// Add new columns
driver.execute(null, "ALTER TABLE users ADD COLUMN first_name TEXT", 0)
driver.execute(null, "ALTER TABLE users ADD COLUMN last_name TEXT", 0)
// Migrate existing data
callbacks.filter { it.afterVersion == 4L }.forEach { callback ->
callback.block(driver)
}
// Remove old column (SQLite requires table recreation)
driver.execute(null, """
CREATE TABLE users_new (
id INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'ACTIVE'
)
""".trimIndent(), 0)
driver.execute(null, """
INSERT INTO users_new (id, first_name, last_name, email, status)
SELECT id, first_name, last_name, email, status FROM users
""".trimIndent(), 0)
driver.execute(null, "DROP TABLE users", 0)
driver.execute(null, "ALTER TABLE users_new RENAME TO users", 0)
currentVersion = 4
}
return QueryResult.Value(Unit)
}
}
// Usage with migration callbacks
val database = UserDatabase.create(
driver = sqlDriver,
// Custom migration callbacks can be provided
migrationCallbacks = arrayOf(
AfterVersion(2L) { driver ->
// Custom logic after migrating to version 2
println("Migrated to version 2, populating email fields...")
// Populate email addresses from external source
populateEmailsFromApi(driver)
},
AfterVersion(3L) { driver ->
// Custom logic after migrating to version 3
println("Migrated to version 3, creating user profiles...")
// Initialize user profiles with default settings
initializeUserProfiles(driver)
}
)
)Install with Tessl CLI
npx tessl i tessl/maven-app-cash-sqldelight--runtime-iossimulatorarm64