Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
ahmed3elshaer avatar

Sqldelight Patterns

  • 21 installs
  • 60 repo stars
  • Updated June 14, 2026
  • ahmed3elshaer/everything-claude-code-mobile

sqldelight-patterns is a Claude Code skill that provides SQLDelight patterns for Kotlin Multiplatform, covering .sq definitions, platform drivers, type adapters, and migrations.

About

sqldelight-patterns is a Claude Code skill documenting SQLDelight database patterns for Kotlin Multiplatform. It shows how to configure the Gradle plugin, write .sq table and query files, wire up platform drivers for Android and iOS, define type adapters, run migrations, and observe queries as coroutines Flows. A developer uses it when building the shared persistence layer of a KMP mobile app.

  • SQLDelight setup for Kotlin Multiplatform: Gradle plugin, .sq files, Android + iOS drivers
  • Covers type adapters, migrations with verifyMigrations, and coroutines Flow observation
  • Shared commonMain database access across Android and iOS

Sqldelight Patterns by the numbers

  • 21 all-time installs (skills.sh)
  • Ranked #549 of 911 Databases skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
At a glance

sqldelight-patterns capabilities & compatibility

Capabilities
database schema · sql queries · db migrations
Use cases
database
From the docs

What sqldelight-patterns says it does

SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
SKILL.md
Place `.sq` files in `src/commonMain/sqldelight/com/example/db/`.
SKILL.md
return NativeSqliteDriver(
SKILL.md
npx skills add https://github.com/ahmed3elshaer/everything-claude-code-mobile --skill sqldelight-patterns

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs21
repo stars60
Last updatedJune 14, 2026
Repositoryahmed3elshaer/everything-claude-code-mobile

What it does

Set up and query a SQLDelight database shared across Android and iOS in a Kotlin Multiplatform app.

Who is it for?

Setting up a shared SQLDelight database in a Kotlin Multiplatform project targeting Android and iOS.

When should I use this skill?

Configuring SQLDelight, writing .sq queries, setting up platform drivers, or writing KMP database migrations.

What you get

  • Configured SQLDelight database
  • .sq table and query definitions
  • Android and iOS driver factories

By the numbers

  • Covers Android (AndroidSqliteDriver) and iOS (NativeSqliteDriver) drivers
  • Uses SQLDelight 2.0.2

Files

SKILL.mdMarkdownGitHub ↗

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("alice@test.com", users.first().email)
    }
}

Best Practices

  • Let SQLDelight generate type-safe Kotlin code from .sq files; do not write manual SQL wrappers.
  • Use database.transaction { } for multi-statement atomic operations.
  • Place type adapters in commonMain so they are shared across platforms.
  • Use .asFlow().mapToList() for reactive UI; use direct query calls for one-shot reads.
  • Keep .sq files organized by table name, one file per table.
  • Enable verifyMigrations to catch schema drift at build time.
  • Prefer INSERT OR REPLACE for upsert semantics in .sq files.

Related skills

Databasesdatabases

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.