skills/sqldelight-patterns/SKILL.md
SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
npx skillsauth add ahmed3elshaer/everything-claude-code-mobile sqldelight-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
// build.gradle.kts (shared module)
plugins {
id("app.cash.sqldelight") version "2.0.2"
}
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.example.db")
schemaOutputDirectory.set(file("src/commonMain/sqldelight/databases"))
verifyMigrations.set(true)
}
}
}
// Dependencies
kotlin {
sourceSets {
commonMain.dependencies {
implementation("app.cash.sqldelight:coroutines-extensions:2.0.2")
implementation("app.cash.sqldelight:primitive-adapters:2.0.2")
}
androidMain.dependencies {
implementation("app.cash.sqldelight:android-driver:2.0.2")
}
iosMain.dependencies {
implementation("app.cash.sqldelight:native-driver:2.0.2")
}
}
}
Place .sq files in src/commonMain/sqldelight/com/example/db/.
-- User.sq
CREATE TABLE User (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
avatar_url TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL
);
CREATE INDEX idx_user_email ON User(email);
CREATE INDEX idx_user_created_at ON User(created_at);
-- User.sq (continued)
selectAll:
SELECT * FROM User ORDER BY created_at DESC;
selectById:
SELECT * FROM User WHERE id = ?;
selectByEmail:
SELECT * FROM User WHERE email = ?;
selectByStatus:
SELECT * FROM User WHERE status = :status;
insert:
INSERT OR REPLACE INTO User(display_name, email, avatar_url, status, created_at)
VALUES (?, ?, ?, ?, ?);
updateDisplayName:
UPDATE User SET display_name = ? WHERE id = ?;
deleteById:
DELETE FROM User WHERE id = ?;
deleteAll:
DELETE FROM User;
countByStatus:
SELECT COUNT(*) FROM User WHERE status = ?;
-- Post.sq
CREATE TABLE Post (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL REFERENCES User(id),
title TEXT NOT NULL,
body TEXT NOT NULL,
published_at INTEGER
);
selectWithAuthor:
SELECT Post.*, User.display_name AS author_name
FROM Post
INNER JOIN User ON Post.author_id = User.id
WHERE Post.id = ?;
selectByAuthorId:
SELECT * FROM Post WHERE author_id = ? ORDER BY published_at DESC;
// commonMain
expect class DriverFactory {
fun createDriver(): SqlDriver
}
fun createDatabase(driverFactory: DriverFactory): AppDatabase {
val driver = driverFactory.createDriver()
return AppDatabase(
driver = driver,
UserAdapter = User.Adapter(
statusAdapter = statusAdapter,
created_atAdapter = instantAdapter
)
)
}
// androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(
schema = AppDatabase.Schema,
context = context,
name = "app.db"
)
}
}
// iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(
schema = AppDatabase.Schema,
name = "app.db"
)
}
}
// Enum adapter
val statusAdapter = object : ColumnAdapter<UserStatus, String> {
override fun decode(databaseValue: String): UserStatus =
UserStatus.valueOf(databaseValue)
override fun encode(value: UserStatus): String =
value.name
}
// Instant adapter (kotlinx.datetime)
val instantAdapter = object : ColumnAdapter<Instant, Long> {
override fun decode(databaseValue: Long): Instant =
Instant.fromEpochMilliseconds(databaseValue)
override fun encode(value: Instant): Long =
value.toEpochMilliseconds()
}
// Usage in database creation
val database = AppDatabase(
driver = driver,
UserAdapter = User.Adapter(
statusAdapter = statusAdapter,
created_atAdapter = instantAdapter
)
)
Place migration files in src/commonMain/sqldelight/databases/migrations/.
-- 1.sqm (migration from version 1 to 2)
ALTER TABLE User ADD COLUMN avatar_url TEXT;
-- 2.sqm (migration from version 2 to 3)
CREATE INDEX idx_user_created_at ON User(created_at);
ALTER TABLE User ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
Enable migration verification in Gradle:
sqldelight {
databases {
create("AppDatabase") {
verifyMigrations.set(true)
}
}
}
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.coroutines.mapToOne
import app.cash.sqldelight.coroutines.mapToOneOrNull
class UserRepository(private val database: AppDatabase) {
fun observeAll(): Flow<List<User>> =
database.userQueries.selectAll()
.asFlow()
.mapToList(Dispatchers.IO)
fun observeById(id: Long): Flow<User?> =
database.userQueries.selectById(id)
.asFlow()
.mapToOneOrNull(Dispatchers.IO)
suspend fun insert(user: User) = withContext(Dispatchers.IO) {
database.userQueries.insert(
display_name = user.displayName,
email = user.email,
avatar_url = user.avatarUrl,
status = user.status,
created_at = user.createdAt
)
}
suspend fun deleteById(id: Long) = withContext(Dispatchers.IO) {
database.userQueries.deleteById(id)
}
suspend fun replaceAll(users: List<User>) = withContext(Dispatchers.IO) {
database.transaction {
database.userQueries.deleteAll()
users.forEach { user ->
database.userQueries.insert(
display_name = user.displayName,
email = user.email,
avatar_url = user.avatarUrl,
status = user.status,
created_at = user.createdAt
)
}
}
}
}
shared/
src/
commonMain/
kotlin/com/example/db/
DriverFactory.kt (expect class)
DatabaseModule.kt (Koin module)
UserRepository.kt
sqldelight/com/example/db/
User.sq
Post.sq
sqldelight/databases/
1.sqm
androidMain/
kotlin/com/example/db/
DriverFactory.android.kt (actual class)
iosMain/
kotlin/com/example/db/
DriverFactory.ios.kt (actual class)
class UserRepositoryTest {
private lateinit var database: AppDatabase
private lateinit var repository: UserRepository
@BeforeTest
fun setup() {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
AppDatabase.Schema.create(driver)
database = AppDatabase(
driver = driver,
UserAdapter = User.Adapter(
statusAdapter = statusAdapter,
created_atAdapter = instantAdapter
)
)
repository = UserRepository(database)
}
@Test
fun insertAndRetrieve() = runTest {
repository.insert(testUser)
val users = repository.observeAll().first()
assertEquals(1, users.size)
assertEquals("[email protected]", users.first().email)
}
}
.sq files; do not write manual SQL wrappers.database.transaction { } for multi-statement atomic operations.commonMain so they are shared across platforms..asFlow().mapToList() for reactive UI; use direct query calls for one-shot reads..sq files organized by table name, one file per table.verifyMigrations to catch schema drift at build time.INSERT OR REPLACE for upsert semantics in .sq files.data-ai
Room database patterns for Android - entity definitions, DAO interfaces, Database class, migrations, TypeConverters, and Flow integration.
tools
Push notification patterns - FCM setup for Android, APNs for iOS, notification channels, payload handling, foreground/background behavior, and rich notifications.
content-media
Pagination patterns for mobile - Paging 3 for Android (PagingSource, RemoteMediator, LazyPagingItems), cursor-based and offset-based strategies.
tools
Offline-first architecture patterns - NetworkBoundResource, sync strategies, conflict resolution, cache invalidation, and connectivity monitoring.