Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class RoomLibraryConventionPlugin : Plugin<Project> {
private fun Project.configureAndroid() {
dependencies {
implementationBundle("room")
// Room 3 requires a SQLiteDriver via setDriver(); AndroidSQLiteDriver
// (framework SQLite) is referenced in AppDatabaseFactory and the test builders.
// Room 3 requires a SQLiteDriver via setDriver(); classic Android Room modules use
// the framework artifact, while KMP modules select their driver in configureKmp().
implementation("androidx-sqlite-framework")

ksp("androidx-room-compiler")
Expand All @@ -71,11 +71,14 @@ class RoomLibraryConventionPlugin : Plugin<Project> {
// where androidx.paging.PagingSource actually lives (phase-6 spec §0).
add("commonMainImplementation", libs.findBundle("room").get())
add("commonMainImplementation", libs.findLibrary("androidx-paging-common").get())
// The driver artifact is per-target by construction: its android variant carries
// AndroidSQLiteDriver, its Apple variants NativeSQLiteDriver, and no arrangement
// shares one across targets (phase-6 spec §6). iosMain gets a driver dependency
// the day an iOS composition root builds a database, not before.
add("androidMainImplementation", libs.findLibrary("androidx-sqlite-framework").get())
// BundledSQLiteDriver: one SQLite build (3.50.x) on every device instead of the
// per-OEM system one (phase-6 spec §6; the flip commit's own gate). Per-target by
// construction — iosMain gets a driver dependency the day an iOS composition root
// builds a database, not before. Robolectric HOST tests cannot use bundled (the
// android variant ships Android-ABI natives only; measured UnsatisfiedLinkError),
// so a module whose host tests build databases pins sqlite-framework on
// androidHostTestImplementation itself.
add("androidMainImplementation", libs.findLibrary("androidx-sqlite-bundled").get())

// Room's KSP codegen runs once per compilation target; the compiler artifact
// itself is JVM-only, which is fine — KSP always executes on the JVM.
Expand Down
7 changes: 6 additions & 1 deletion core/data/database-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ dependencies {
api(project(":core:data:database"))

api(libs.bundles.room)
// InMemoryDatabaseProvider builds a Room 3 DB and must setDriver(AndroidSQLiteDriver()).
// The two fixtures deliberately run DIFFERENT drivers. RepositoryTestEnv (Robolectric
// repository unit tests) pins AndroidSQLiteDriver: the bundled driver's android variant
// ships Android-ABI natives only and dies with UnsatisfiedLinkError on a desktop JVM
// (measured). InMemoryDatabaseProvider (on-device androidTest via MetroTestRule) runs
// BundledSQLiteDriver — the production driver since the flip.
api(libs.androidx.sqlite.framework)
api(libs.androidx.sqlite.bundled)
api(libs.androidx.test)
// RepositoryTestEnv runs suspending seeds and exposes a CoroutineScope to its callers.
api(libs.coroutines)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import org.jetbrains.annotations.TestOnly
* required (the in-memory builder needs an Android `Context`); add the
* `RobolectricExtension` to your test class along with
* `@Config(application = RepositoryTestEnv.TestApplication::class, sdk = [33])`.
*
* Stays on [AndroidSQLiteDriver] after the production flip to `BundledSQLiteDriver`: the
* bundled android variant carries Android-ABI natives only, and loading it under Robolectric
* on a desktop JVM fails with `UnsatisfiedLinkError` (measured). Driver behaviour is a
* device-suite concern; this fixture's oracle value is repository logic over a real schema.
*/
@TestOnly
class RepositoryTestEnv {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.stslex.workeeper.core.data.database_test

import android.content.Context
import androidx.room3.Room
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import io.github.stslex.workeeper.core.data.database.AppDatabase

/**
Expand All @@ -22,7 +22,7 @@ object InMemoryDatabaseProvider {
.inMemoryDatabaseBuilder<AppDatabase>(
context,
)
.setDriver(AndroidSQLiteDriver())
.setDriver(BundledSQLiteDriver())
.allowMainThreadQueries()
.build()
}
7 changes: 6 additions & 1 deletion core/data/database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ metro {
}

// The Room surface (entities, DAOs, converters, migrations, export) is commonMain; only what is
// platform-typed stays in androidMain — buildAppDatabase (Context + AndroidSQLiteDriver) and the
// platform-typed stays in androidMain — buildAppDatabase (Context + BundledSQLiteDriver) and the
// snapshot/ package (raw android.database.sqlite + java.io.File, deliberately outside Room).
kotlin {
sourceSets {
Expand All @@ -41,6 +41,11 @@ dependencies {
"androidHostTestImplementation"(libs.robolectric)
"androidHostTestImplementation"(libs.robolectric.junit5.extension)
"androidHostTestImplementation"(libs.androidx.test)
// Host tests stay on AndroidSQLiteDriver: the bundled driver's android variant carries
// Android-ABI natives only and dies with UnsatisfiedLinkError under Robolectric on a
// desktop JVM (measured). Robolectric is not an admissible driver oracle anyway — the
// device suite is where the production driver is exercised.
"androidHostTestImplementation"(libs.androidx.sqlite.framework)
// MigrationsRegistryTest introspects the registry against room-testing's Migration surface.
"androidHostTestImplementation"(libs.androidx.room.testing)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
package io.github.stslex.workeeper.core.data.database

import androidx.room3.testing.MigrationTestHelper
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.execSQL
import androidx.test.platform.app.InstrumentationRegistry
import io.github.stslex.workeeper.core.data.database.migration.Migration6
Expand Down Expand Up @@ -37,7 +37,7 @@ internal class AppDatabaseMigrationTest {
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
InstrumentationRegistry.getInstrumentation().targetContext.getDatabasePath(TEST_DB),
AndroidSQLiteDriver(),
BundledSQLiteDriver(),
AppDatabase::class,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.content.Context
import androidx.room3.Room
import androidx.room3.immediateTransaction
import androidx.room3.useWriterConnection
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.stslex.workeeper.core.core.coroutine.asyncForEach
Expand Down Expand Up @@ -79,7 +79,7 @@ internal class AtomicRollbackDeviceTest {
// Real file-backed DB on the device — NOT in-memory, so transaction/connection
// semantics match production, not Robolectric's shadow SQLite.
database = Room.databaseBuilder<AppDatabase>(context, PROBE_DB)
.setDriver(AndroidSQLiteDriver())
.setDriver(BundledSQLiteDriver())
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.content.Context
import androidx.room3.Room
import androidx.room3.immediateTransaction
import androidx.room3.useWriterConnection
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.stslex.workeeper.core.data.database.tag.TagEntity
Expand Down Expand Up @@ -53,7 +53,7 @@ internal class InvalidationDeviceTest {
context = ApplicationProvider.getApplicationContext()
context.deleteDatabase(PROBE_DB)
database = Room.databaseBuilder<AppDatabase>(context, PROBE_DB)
.setDriver(AndroidSQLiteDriver())
.setDriver(BundledSQLiteDriver())
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.paging.PagingSource
import androidx.room3.Room
import androidx.room3.immediateTransaction
import androidx.room3.useWriterConnection
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.stslex.workeeper.core.data.database.exercise.ExerciseEntity
Expand Down Expand Up @@ -58,7 +58,7 @@ internal class Room3RoundTripDeviceTest {

private fun openDb(): AppDatabase =
Room.databaseBuilder<AppDatabase>(context, ROUNDTRIP_DB)
.setDriver(AndroidSQLiteDriver())
.setDriver(BundledSQLiteDriver())
.build()

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.stslex.workeeper.core.data.database.exercise

import android.app.Application
import androidx.room3.Room
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.stslex.workeeper.core.data.database.AppDatabase
Expand Down Expand Up @@ -47,7 +47,7 @@ internal class ExerciseDaoRecentlyTrainedTest {
database = Room.inMemoryDatabaseBuilder<AppDatabase>(
ApplicationProvider.getApplicationContext<Application>(),
)
.setDriver(AndroidSQLiteDriver())
.setDriver(BundledSQLiteDriver())
.allowMainThreadQueries()
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.stslex.workeeper.core.data.database

import android.content.Context
import androidx.room3.Room
import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import io.github.stslex.workeeper.core.data.database.migration.MIGRATIONS

/**
Expand Down Expand Up @@ -38,8 +38,12 @@ fun buildAppDatabase(context: Context): AppDatabase = Room
context = context,
name = AppDatabase.NAME,
)
// Room 3 requires an explicit driver; AndroidSQLiteDriver is the framework SQLite
// implementation Room 2.8.4 used implicitly, so the on-disk format is unchanged.
.setDriver(AndroidSQLiteDriver())
// Room 3 requires an explicit driver. BundledSQLiteDriver ships one SQLite build (3.50.x)
// to every device instead of the per-OEM, per-API-level system one — the main-db and WAL
// file formats are frozen, so existing installations open unchanged. The snapshot/ package
// still opens the same file through framework SQLite (android.database.sqlite) for its
// pre-migration peek and checkpoint; that cross-library interop is deliberate and its
// paths are exercised by the recovery flow, not by this builder.
.setDriver(BundledSQLiteDriver())
Comment thread
stslex marked this conversation as resolved.
.apply { MIGRATIONS.forEach { addMigrations(it) } }
.build()
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ private const val PR_SINGLE_SQL = """

/**
* Batch PR: *every* eligible candidate for every requested exercise, grouped by exercise and
* ordered so the consumer takes `.first()` per group. No `LIMIT`/window function — `minSdk 28`
* ships SQLite 3.22 and `ROW_NUMBER()` needs 3.25 (the bundled-SQLite dependency is declared
* but inert; `AndroidSQLiteDriver` uses framework SQLite).
* ordered so the consumer takes `.first()` per group. A
* `ROW_NUMBER() OVER (PARTITION BY …)` rewrite is available under the production bundled
* driver, but any rewrite must retain device coverage because Robolectric host tests use a
* different SQLite engine. See kmp-phase-6-data-layer.md → §6
* "The driver decision — decided."
*/
private const val PR_BATCH_SQL = """
$PR_ROW_SELECT
Expand Down
13 changes: 6 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# KSP 2.3.9 (not 2.3.6): 2.3.6 silently skips KMP/native codegen — a proven false-green
# (Probe-2). KSP2 is Kotlin-decoupled; drives Room's DAO codegen (the only KSP consumer).
ksp = "2.3.9"
androidGradlePlugin = "9.3.0"

Check warning on line 7 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Android Gradle Plugin Version: A newer version of com.android.tools.build:gradle than 9.3.0 is available: 9.3.1

Check warning on line 7 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Android Gradle Plugin Version: A newer version of com.android.lint than 9.3.0 is available: 9.3.1

Check warning on line 7 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Android Gradle Plugin Version: A newer version of com.android.library than 9.3.0 is available: 9.3.1

Check warning on line 7 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Android Gradle Plugin Version: A newer version of com.android.kotlin.multiplatform.library than 9.3.0 is available: 9.3.1

Check warning on line 7 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Android Gradle Plugin Version: A newer version of com.android.application than 9.3.0 is available: 9.3.1
androidTools = "32.3.0"

Check warning on line 8 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Gradle Dependency: A newer version of com.android.tools:common than 32.3.0 is available: 32.3.1

minSdk = "28"
targetSdk = "37"
Expand All @@ -19,7 +19,7 @@
lifecycle = "2.11.0"
coroutines = "1.11.0"

composeBom = "2026.07.00"

Check warning on line 22 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Gradle Dependency: A newer version of androidx.compose:compose-bom-alpha than 2026.07.00 is available: 2026.08.00
composeGradle = "1.11.1"
# CMP artifact lines. The core artifacts (runtime/foundation/ui/components-resources) release
# at the plugin (composeGradle) version; material3 rides its own decoupled line. Keep this
Expand All @@ -27,10 +27,10 @@
# own accessors resolve (documentation/feature-specs/kmp-phase-2-probes.md, "Findings" §3).
cmpMaterial3 = "1.9.0"
navigation3 = "1.1.6"
composeJunit = "1.11.4"

Check warning on line 30 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Gradle Dependency: A newer version of androidx.compose.ui:ui-test-junit4 than 1.11.4 is available: 1.12.0
coil = "3.5.0"
composeActivity = "1.13.0"
fbBom = "34.16.0"

Check warning on line 33 in gradle/libs.versions.toml

View workflow job for this annotation

GitHub Actions / Build and Unit Tests

Obsolete Gradle Dependency: A newer version of com.google.firebase:firebase-bom than 34.16.0 is available: 34.18.0
fbPerf = "2.0.2"

workManager = "2.11.2"
Expand All @@ -48,10 +48,9 @@

# room = "2.8.4" was the androidx.room 2.x line; superseded by the room3 (androidx.room3)
# coordinates below, which are the applied ones. The 2.x version key is gone with its coords.
# androidx.sqlite 2.7.0 (transitive of room3-runtime): sqlite-framework carries the
# AndroidSQLiteDriver, now REQUIRED via setDriver() on every RoomDatabase.Builder in
# Room 3. sqlite-bundled (K/N BundledSQLiteDriver) stays declared but INERT — Android
# uses the framework driver; bundled lands when C.1 converts the DB module to K/N.
# androidx.sqlite 2.7.0 (transitive of room3-runtime): every Room 3 builder selects a driver
# explicitly. sqlite-bundled supplies the production Android driver for KMP Room modules;
# sqlite-framework remains for classic Android modules and Robolectric host-test fixtures.
room3 = "3.0.0"
sqlite = "2.7.0"
serialization = "1.11.0"
Expand Down Expand Up @@ -180,9 +179,9 @@
androidx-room-compiler = { group = "androidx.room3", name = "room3-compiler", version.ref = "room3" }
androidx-room-testing = { group = "androidx.room3", name = "room3-testing", version.ref = "room3" }

# AndroidSQLiteDriver (framework SQLite) — the Room 3 setDriver() driver on Android.
# AndroidSQLiteDriver (framework SQLite) — classic Android modules and JVM host-test fixtures.
androidx-sqlite-framework = { group = "androidx.sqlite", name = "sqlite-framework", version.ref = "sqlite" }
# K/N BundledSQLiteDriver — declared but inert (Android does not need it; lands with C.1).
# BundledSQLiteDriver — the production Android driver for KMP Room modules.
androidx-sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" }

androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
Expand Down Expand Up @@ -303,4 +302,4 @@
room = [
"androidx-room-runtime",
"androidx-room-paging"
]
]
Loading