SQLDelight Patterns
Communityahmed3elshaer
SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
Overview
- Repository
- ahmed3elshaer/everything-claude-code-mobile
- Publisher
- ahmed3elshaer
- Stars
- 66
- Forks
- 18
- Bundled files
- 0
- License
- MIT
- Updated
- Jun 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/sqldelight-patterns" ~/.claude/skills/sqldelight-patterns
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/sqldelight-patterns" ~/.codex/skills/sqldelight-patterns
rm -rf "$tmp"Install the SQLDelight Patterns agent skill from https://github.com/ahmed3elshaer/everything-claude-code-mobile/tree/main/skills/sqldelight-patterns into this project's skills folder.Instructions
SQLDelight Patterns for KMP
Gradle Plugin Configuration
// 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")
}
}
}.sq File Syntax
Place .sq files in src/commonMain/sqldelight/com/example/db/.
Table Definitions
-- 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);Named Queries
-- 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 = ?;Parameterized Queries with Joins
-- 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;Platform Driver Setup
Common Interface
// 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
)
)
}Android Driver
// androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(
schema = AppDatabase.Schema,
context = context,
name = "app.db"
)
}
}iOS Driver
// iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(
schema = AppDatabase.Schema,
name = "app.db"
)
}
}Type Adapters
// 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
)
)Migration Strategy
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)
}
}
}Coroutines Flow Extension
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
)
}
}
}
}KMP Module Structure
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)
Testing with In-Memory Driver
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)
}
}Best Practices
- Let SQLDelight generate type-safe Kotlin code from
.sqfiles; do not write manual SQL wrappers. - Use
database.transaction { }for multi-statement atomic operations. - Place type adapters in
commonMainso they are shared across platforms. - Use
.asFlow().mapToList()for reactive UI; use direct query calls for one-shot reads. - Keep
.sqfiles organized by table name, one file per table. - Enable
verifyMigrationsto catch schema drift at build time. - Prefer
INSERT OR REPLACEfor upsert semantics in .sq files.
FAQ
What does SQLDelight Patterns do?
SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
Who made SQLDelight Patterns?
ahmed3elshaer (a community member) publishes it in the ahmed3elshaer/everything-claude-code-mobile repository on GitHub.
Is SQLDelight Patterns free?
Yes. SQLDelight Patterns is open source, published by ahmed3elshaer under the MIT license.
What comes with SQLDelight Patterns?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use SQLDelight Patterns?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.