skip to content

SQLDelight generates a common database API but needs a platform `SqlDriver`. Name the drivers per platform and explain how the generated `Database` is constructed from one.

level: seniorimportance: should knowfreq 40%

answer

  1. SQLDelight generates Database + Schema from .sq
  2. You supply SqlDriver per platform
  3. Android = AndroidSqliteDriver, iOS = NativeSqliteDriver
  4. JVM = JdbcSqliteDriver (in-memory for tests)
  5. Database(driver); pass Schema to driver

basics

~20 s

SQLDelight turns your .sq files into a common typed database API, but you must hand it a platform-specific SqlDriver to actually talk to SQLite: Android, native (iOS), and a web/JS driver. You build the driver per platform and pass it into the generated Database.

solid answer

~40 s

SQLDelight generates a typesafe `Database`/queries API in common code from your SQL, plus a `Schema`. The runtime piece it cannot generate portably is the `SqlDriver` — the bridge to a real SQLite engine — so you supply one per target: **`AndroidSqliteDriver`** (`sqldelight-android-driver`, wraps the Android SQLite framework / SupportSQLite), **`NativeSqliteDriver`** (`sqldelight-native-driver`, for iOS/macOS, backed by SQLite on Kotlin/Native), and the **web worker driver** (`sqldelight-web-worker-driver`) for JS/WASM. A typical pattern is `expect class DriverFactory { fun create(): SqlDriver }` with platform actuals; the generated `Database(driver)` (or `Database.invoke(driver)`) wires queries to it. You also pass the generated `Schema` so the driver can create/migrate tables. Common code only sees the generated queries; the SQLite backend is platform-specific.

code

kotlin · 19 lines
kotlin
// commonMain
expect class DriverFactory { fun create(): SqlDriver }
fun database(f: DriverFactory) = Database(f.create())

// androidMain
actual class DriverFactory(private val ctx: Context) {
    actual fun create(): SqlDriver =
        AndroidSqliteDriver(Database.Schema, ctx, "app.db")
}

// iosMain
actual class DriverFactory {
    actual fun create(): SqlDriver =
        NativeSqliteDriver(Database.Schema, "app.db")
}

// jvmTest
val testDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
    .also { Database.Schema.create(it) }

go deeper

for a junior

Knows SQLDelight needs a driver and that drivers differ by platform.

for a middle

Names the per-platform drivers and constructs Database(driver) with a DriverFactory.

for a senior

Explains Schema/migrations, in-memory JDBC driver for tests, and the expect/actual DriverFactory pattern.

for a principal

Reasons about Native concurrency/connection handling, reactive Flow extensions, and how to keep the driver boundary injectable and testable in a shared module.

## What SQLDelight generates vs what you supply SQLDelight reads your **`.sq` files** (SQL with typed queries) and **generates** at build time: - a typesafe `Database` interface plus query objects (each labeled SQL statement becomes a Kotlin function returning typed results), - a `Schema` object (`Database.Schema`) describing table creation and migrations. What it does **not** generate is the actual SQLite runtime — that is the **`SqlDriver`**, the abstraction over a concrete SQLite engine. You provide a platform-specific driver; this is the expect/actual seam for the database. ## Drivers per platform - **Android / JVM (Android)** — `AndroidSqliteDriver` from `app.cash.sqldelight:android-driver`. Wraps the Android SQLite framework (`SupportSQLiteOpenHelper`). You pass the generated schema, an Android `Context`, and a db name. - **iOS / macOS (Kotlin/Native)** — `NativeSqliteDriver` from `app.cash.sqldelight:native-driver`. Uses SQLite via a native binding on Kotlin/Native. - **JVM desktop / server** — `JdbcSqliteDriver` from `app.cash.sqldelight:sqlite-driver` (JDBC over SQLite); often used for tests too: `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)`. - **JS / Web (and WASM)** — `app.cash.sqldelight:web-worker-driver` running sql.js in a worker; you create a driver bound to a worker script. ## Constructing the generated `Database` The generated type is constructed from a driver: ```kotlin val driver: SqlDriver = /* platform-specific */ val database = Database(driver) // generated factory val users = database.userQueries.selectAll().executeAsList() ``` The **schema** is handed to the driver at creation so it can create tables / run migrations: ```kotlin // androidMain actual class DriverFactory(private val context: Context) { actual fun create(): SqlDriver = AndroidSqliteDriver(Database.Schema, context, "app.db") } ``` ```kotlin // iosMain actual class DriverFactory { actual fun create(): SqlDriver = NativeSqliteDriver(Database.Schema, "app.db") } ``` ```kotlin // commonMain expect class DriverFactory { fun create(): SqlDriver } fun makeDatabase(factory: DriverFactory): Database = Database(factory.create()) ``` ## Important details - **Schema migrations:** the `Schema` exposes `version` and `migrate(...)`; drivers run `create`/`migrate` when opening. You add `.sqm` migration files; SQLDelight verifies them at compile time. - **Threading / Native:** historically the Native driver dealt with Kotlin/Native memory and concurrency constraints; modern SQLDelight + the new memory model make this transparent, but senior candidates should know the Native driver manages its own connection pool. - **Coroutines extension:** `app.cash.sqldelight:coroutines-extensions` adds `asFlow()` so a query can be observed as a `Flow`, portable across platforms. - **Generated API is fully common:** all your query calls live in `commonMain`; only the `DriverFactory` actuals are platform-specific — the textbook one-common-API-per-target shape. ## Why a driver abstraction at all SQLite exists on every target but is reached through different host APIs (Android framework helper, native binding, JDBC, sql.js in a worker). `SqlDriver` normalizes prepare/execute/cursor semantics so the generated code is identical everywhere; the driver is the platform-specific actual.

  • Why pass the generated `Schema` to the driver?
    The driver uses it to create the tables and run versioned migrations when the database is opened, keeping schema setup consistent across platforms.
  • How would you observe a query reactively across platforms?
    Use the coroutines-extensions `asFlow()` (plus `mapToList`/`mapToOne`) to turn a SQLDelight query into a common `Flow` that emits on data changes.

saying these in an interview costs you the question

  • Thinking SQLDelight talks to SQLite without any platform driver
  • Using AndroidSqliteDriver on iOS or vice versa
  • Not knowing the Schema is needed for table creation/migration
  • Believing the generated query API differs per platform
  • Claiming migrations are written in Kotlin rather than .sqm files verified at compile time

context