@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/docs/facts.json +5 -5
- package/manifest.json +52 -31
- package/package.json +1 -1
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
|
@@ -8,6 +8,11 @@ description: "Implement Room database persistence on Android with @Entity, @Dao,
|
|
|
8
8
|
Room persistence library patterns for Android targeting Room 2.6+ with
|
|
9
9
|
KSP, Kotlin coroutines, and Flow-based reactive queries.
|
|
10
10
|
|
|
11
|
+
Detailed, copy-ready code for every section below lives in
|
|
12
|
+
[references/patterns.md](references/patterns.md). Load that file when you need a
|
|
13
|
+
full example to adapt (entity, DAO, migration, Hilt module, or a test). Each
|
|
14
|
+
heading there mirrors a section here.
|
|
15
|
+
|
|
11
16
|
## Contents
|
|
12
17
|
|
|
13
18
|
- [Entity Definition](#entity-definition)
|
|
@@ -18,6 +23,9 @@ KSP, Kotlin coroutines, and Flow-based reactive queries.
|
|
|
18
23
|
- [Relations](#relations)
|
|
19
24
|
- [Migration Strategies](#migration-strategies)
|
|
20
25
|
- [Hilt Integration](#hilt-integration)
|
|
26
|
+
- [Encrypted Database (SQLCipher)](#encrypted-database-sqlcipher)
|
|
27
|
+
- [Async Database Provisioning](#async-database-provisioning)
|
|
28
|
+
- [Runtime Hygiene](#runtime-hygiene)
|
|
21
29
|
- [Testing with In-Memory Database](#testing-with-in-memory-database)
|
|
22
30
|
- [Do's and Don'ts](#dos-and-donts)
|
|
23
31
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -25,107 +33,22 @@ KSP, Kotlin coroutines, and Flow-based reactive queries.
|
|
|
25
33
|
|
|
26
34
|
## Entity Definition
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Index(value = ["origin_code", "destination_code"]),
|
|
33
|
-
Index(value = ["departure_time"]),
|
|
34
|
-
],
|
|
35
|
-
)
|
|
36
|
-
data class FlightEntity(
|
|
37
|
-
@PrimaryKey
|
|
38
|
-
val id: String,
|
|
39
|
-
@ColumnInfo(name = "origin_code")
|
|
40
|
-
val originCode: String,
|
|
41
|
-
@ColumnInfo(name = "origin_name")
|
|
42
|
-
val originName: String,
|
|
43
|
-
@ColumnInfo(name = "destination_code")
|
|
44
|
-
val destinationCode: String,
|
|
45
|
-
@ColumnInfo(name = "destination_name")
|
|
46
|
-
val destinationName: String,
|
|
47
|
-
@ColumnInfo(name = "departure_time")
|
|
48
|
-
val departureTime: String,
|
|
49
|
-
@ColumnInfo(name = "arrival_time")
|
|
50
|
-
val arrivalTime: String,
|
|
51
|
-
@ColumnInfo(name = "price_amount")
|
|
52
|
-
val priceAmount: Double,
|
|
53
|
-
@ColumnInfo(name = "price_currency")
|
|
54
|
-
val priceCurrency: String,
|
|
55
|
-
val status: String,
|
|
56
|
-
@ColumnInfo(name = "updated_at", defaultValue = "0")
|
|
57
|
-
val updatedAt: Long = System.currentTimeMillis(),
|
|
58
|
-
)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Composite Primary Key
|
|
36
|
+
Annotate a `data class` with `@Entity(tableName = ...)`. Declare a `@PrimaryKey`,
|
|
37
|
+
map snake_case columns with `@ColumnInfo(name = ...)`, and add `indices` for
|
|
38
|
+
columns that appear in `WHERE`/`ORDER BY`. Use `primaryKeys = [...]` on the
|
|
39
|
+
entity for a composite key instead of `@PrimaryKey`.
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
tableName = "booking_passengers",
|
|
66
|
-
primaryKeys = ["booking_id", "passenger_id"],
|
|
67
|
-
)
|
|
68
|
-
data class BookingPassengerEntity(
|
|
69
|
-
@ColumnInfo(name = "booking_id")
|
|
70
|
-
val bookingId: String,
|
|
71
|
-
@ColumnInfo(name = "passenger_id")
|
|
72
|
-
val passengerId: String,
|
|
73
|
-
@ColumnInfo(name = "seat_number")
|
|
74
|
-
val seatNumber: String?,
|
|
75
|
-
)
|
|
76
|
-
```
|
|
41
|
+
See [references/patterns.md#entity-definition](references/patterns.md#entity-definition)
|
|
42
|
+
for a full entity and a composite-key entity.
|
|
77
43
|
|
|
78
44
|
## DAO Interface
|
|
79
45
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@Query("SELECT * FROM flights WHERE origin_code = :origin AND destination_code = :destination ORDER BY departure_time ASC")
|
|
85
|
-
fun getFlights(origin: String, destination: String): Flow<List<FlightEntity>>
|
|
86
|
-
|
|
87
|
-
@Query("SELECT * FROM flights WHERE id = :id")
|
|
88
|
-
fun getFlightById(id: String): Flow<FlightEntity?>
|
|
89
|
-
|
|
90
|
-
@Query("SELECT * FROM flights WHERE id = :id")
|
|
91
|
-
suspend fun getFlightByIdOnce(id: String): FlightEntity?
|
|
92
|
-
|
|
93
|
-
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
94
|
-
suspend fun insertFlight(flight: FlightEntity)
|
|
46
|
+
Mark the interface `@Dao`. Return `Flow<T>` for reactive reads and `suspend`
|
|
47
|
+
for one-shot reads/writes. Use `@Query`, `@Insert`, `@Update`, `@Delete`,
|
|
48
|
+
`@Upsert`, and wrap multi-statement operations in a `@Transaction` default
|
|
49
|
+
method.
|
|
95
50
|
|
|
96
|
-
|
|
97
|
-
suspend fun insertFlights(flights: List<FlightEntity>)
|
|
98
|
-
|
|
99
|
-
@Upsert
|
|
100
|
-
suspend fun upsertFlights(flights: List<FlightEntity>)
|
|
101
|
-
|
|
102
|
-
@Update
|
|
103
|
-
suspend fun updateFlight(flight: FlightEntity)
|
|
104
|
-
|
|
105
|
-
@Delete
|
|
106
|
-
suspend fun deleteFlight(flight: FlightEntity)
|
|
107
|
-
|
|
108
|
-
@Query("DELETE FROM flights WHERE id = :id")
|
|
109
|
-
suspend fun deleteFlightById(id: String)
|
|
110
|
-
|
|
111
|
-
@Query("DELETE FROM flights WHERE updated_at < :threshold")
|
|
112
|
-
suspend fun deleteStaleFlights(threshold: Long)
|
|
113
|
-
|
|
114
|
-
@Query("SELECT COUNT(*) FROM flights")
|
|
115
|
-
fun getFlightCount(): Flow<Int>
|
|
116
|
-
|
|
117
|
-
@Transaction
|
|
118
|
-
suspend fun replaceAllFlights(flights: List<FlightEntity>) {
|
|
119
|
-
deleteAllFlights()
|
|
120
|
-
insertFlights(flights)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@Query("DELETE FROM flights")
|
|
124
|
-
suspend fun deleteAllFlights()
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### @Upsert vs @Insert(onConflict = REPLACE)
|
|
51
|
+
Pick the write strategy by intent:
|
|
129
52
|
|
|
130
53
|
| Strategy | Behavior | Use When |
|
|
131
54
|
|----------|----------|----------|
|
|
@@ -133,30 +56,13 @@ interface FlightDao {
|
|
|
133
56
|
| `@Insert(REPLACE)` | Delete + re-insert on conflict | Simpler; triggers delete cascade |
|
|
134
57
|
| `@Insert(IGNORE)` | Skip on conflict | Inserting if not exists, no update needed |
|
|
135
58
|
|
|
136
|
-
|
|
59
|
+
Full DAO surface: [references/patterns.md#dao-interface](references/patterns.md#dao-interface).
|
|
137
60
|
|
|
138
|
-
|
|
139
|
-
@Database(
|
|
140
|
-
entities = [
|
|
141
|
-
FlightEntity::class,
|
|
142
|
-
BookingEntity::class,
|
|
143
|
-
BookingPassengerEntity::class,
|
|
144
|
-
],
|
|
145
|
-
version = 2,
|
|
146
|
-
autoMigrations = [
|
|
147
|
-
AutoMigration(from = 1, to = 2),
|
|
148
|
-
],
|
|
149
|
-
exportSchema = true,
|
|
150
|
-
)
|
|
151
|
-
@TypeConverters(Converters::class)
|
|
152
|
-
abstract class AppDatabase : RoomDatabase() {
|
|
153
|
-
abstract fun flightDao(): FlightDao
|
|
154
|
-
abstract fun bookingDao(): BookingDao
|
|
155
|
-
}
|
|
156
|
-
```
|
|
61
|
+
## Database Setup
|
|
157
62
|
|
|
158
|
-
|
|
159
|
-
`
|
|
63
|
+
Declare an `abstract class ... : RoomDatabase()` with `@Database(entities, version)`,
|
|
64
|
+
one abstract accessor per DAO, and `@TypeConverters` when needed. Always set
|
|
65
|
+
`exportSchema = true` and point KSP at a schema directory:
|
|
160
66
|
|
|
161
67
|
```kotlin
|
|
162
68
|
ksp {
|
|
@@ -164,357 +70,103 @@ ksp {
|
|
|
164
70
|
}
|
|
165
71
|
```
|
|
166
72
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
DAO methods returning `Flow` automatically emit new values when the underlying
|
|
170
|
-
table changes.
|
|
73
|
+
Full `@Database` declaration:
|
|
74
|
+
[references/patterns.md#database-setup](references/patterns.md#database-setup).
|
|
171
75
|
|
|
172
|
-
|
|
173
|
-
// DAO returns Flow
|
|
174
|
-
@Query("SELECT * FROM flights WHERE origin_code = :origin")
|
|
175
|
-
fun getFlightsByOrigin(origin: String): Flow<List<FlightEntity>>
|
|
176
|
-
|
|
177
|
-
// Repository maps to domain models
|
|
178
|
-
class FlightRepositoryImpl @Inject constructor(
|
|
179
|
-
private val dao: FlightDao,
|
|
180
|
-
private val mapper: FlightMapper,
|
|
181
|
-
) : FlightRepository {
|
|
182
|
-
|
|
183
|
-
override fun getFlights(origin: String): Flow<List<Flight>> =
|
|
184
|
-
dao.getFlightsByOrigin(origin)
|
|
185
|
-
.map { entities -> entities.map(mapper::toDomain) }
|
|
186
|
-
.flowOn(Dispatchers.IO)
|
|
187
|
-
}
|
|
76
|
+
## Reactive Queries with Flow
|
|
188
77
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) : ViewModel() {
|
|
194
|
-
|
|
195
|
-
val flights: StateFlow<List<Flight>> = repository.getFlights("IST")
|
|
196
|
-
.stateIn(
|
|
197
|
-
scope = viewModelScope,
|
|
198
|
-
started = SharingStarted.WhileSubscribed(5_000),
|
|
199
|
-
initialValue = emptyList(),
|
|
200
|
-
)
|
|
201
|
-
}
|
|
78
|
+
A DAO method returning `Flow` re-emits whenever its table changes. Map entities
|
|
79
|
+
to domain models in the repository (`.map { ... }.flowOn(Dispatchers.IO)`),
|
|
80
|
+
expose a `StateFlow` from the ViewModel via `stateIn(..., WhileSubscribed(5_000),
|
|
81
|
+
...)`, and collect with `collectAsStateWithLifecycle()` in Compose.
|
|
202
82
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
fun FlightListScreen(viewModel: FlightListViewModel = hiltViewModel()) {
|
|
206
|
-
val flights by viewModel.flights.collectAsStateWithLifecycle()
|
|
207
|
-
LazyColumn {
|
|
208
|
-
items(flights, key = { it.id }) { flight ->
|
|
209
|
-
FlightCard(flight = flight)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
|
83
|
+
Full repository -> ViewModel -> Compose chain:
|
|
84
|
+
[references/patterns.md#reactive-queries-with-flow](references/patterns.md#reactive-queries-with-flow).
|
|
214
85
|
|
|
215
86
|
## TypeConverters
|
|
216
87
|
|
|
217
|
-
For types Room does not natively support (lists, enums, dates)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
class Converters {
|
|
221
|
-
|
|
222
|
-
private val json = Json { ignoreUnknownKeys = true }
|
|
223
|
-
|
|
224
|
-
@TypeConverter
|
|
225
|
-
fun fromStringList(value: List<String>): String =
|
|
226
|
-
json.encodeToString(value)
|
|
227
|
-
|
|
228
|
-
@TypeConverter
|
|
229
|
-
fun toStringList(value: String): List<String> =
|
|
230
|
-
json.decodeFromString(value)
|
|
231
|
-
|
|
232
|
-
@TypeConverter
|
|
233
|
-
fun fromInstant(value: Instant?): Long? = value?.toEpochMilliseconds()
|
|
88
|
+
For types Room does not natively support (lists, enums, dates), write a
|
|
89
|
+
`Converters` class with paired `@TypeConverter` functions and register it with
|
|
90
|
+
`@TypeConverters(Converters::class)` on the `@Database` (or a specific entity/DAO).
|
|
234
91
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@TypeConverter
|
|
239
|
-
fun fromFlightStatus(value: FlightStatus): String = value.name
|
|
240
|
-
|
|
241
|
-
@TypeConverter
|
|
242
|
-
fun toFlightStatus(value: String): FlightStatus = FlightStatus.valueOf(value)
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Register in `@Database`:
|
|
247
|
-
|
|
248
|
-
```kotlin
|
|
249
|
-
@Database(entities = [...], version = 1)
|
|
250
|
-
@TypeConverters(Converters::class)
|
|
251
|
-
abstract class AppDatabase : RoomDatabase()
|
|
252
|
-
```
|
|
92
|
+
Full converter set (list, `Instant`, enum):
|
|
93
|
+
[references/patterns.md#typeconverters](references/patterns.md#typeconverters).
|
|
253
94
|
|
|
254
95
|
## Relations
|
|
255
96
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
```kotlin
|
|
261
|
-
data class AddressEntity(
|
|
262
|
-
val street: String,
|
|
263
|
-
val city: String,
|
|
264
|
-
val country: String,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
@Entity(tableName = "passengers")
|
|
268
|
-
data class PassengerEntity(
|
|
269
|
-
@PrimaryKey val id: String,
|
|
270
|
-
val name: String,
|
|
271
|
-
@Embedded(prefix = "address_")
|
|
272
|
-
val address: AddressEntity,
|
|
273
|
-
)
|
|
274
|
-
// Creates columns: id, name, address_street, address_city, address_country
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### @Relation (One-to-Many)
|
|
278
|
-
|
|
279
|
-
```kotlin
|
|
280
|
-
data class BookingWithPassengers(
|
|
281
|
-
@Embedded val booking: BookingEntity,
|
|
282
|
-
@Relation(
|
|
283
|
-
parentColumn = "id",
|
|
284
|
-
entityColumn = "booking_id",
|
|
285
|
-
)
|
|
286
|
-
val passengers: List<BookingPassengerEntity>,
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
@Dao
|
|
290
|
-
interface BookingDao {
|
|
291
|
-
@Transaction
|
|
292
|
-
@Query("SELECT * FROM bookings WHERE id = :bookingId")
|
|
293
|
-
fun getBookingWithPassengers(bookingId: String): Flow<BookingWithPassengers?>
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Junction Table (Many-to-Many)
|
|
298
|
-
|
|
299
|
-
```kotlin
|
|
300
|
-
@Entity(
|
|
301
|
-
tableName = "flight_tags",
|
|
302
|
-
primaryKeys = ["flight_id", "tag_id"],
|
|
303
|
-
)
|
|
304
|
-
data class FlightTagCrossRef(
|
|
305
|
-
@ColumnInfo(name = "flight_id") val flightId: String,
|
|
306
|
-
@ColumnInfo(name = "tag_id") val tagId: String,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
data class FlightWithTags(
|
|
310
|
-
@Embedded val flight: FlightEntity,
|
|
311
|
-
@Relation(
|
|
312
|
-
parentColumn = "id",
|
|
313
|
-
entityColumn = "id",
|
|
314
|
-
associateBy = Junction(
|
|
315
|
-
value = FlightTagCrossRef::class,
|
|
316
|
-
parentColumn = "flight_id",
|
|
317
|
-
entityColumn = "tag_id",
|
|
318
|
-
),
|
|
319
|
-
)
|
|
320
|
-
val tags: List<TagEntity>,
|
|
321
|
-
)
|
|
322
|
-
```
|
|
97
|
+
- `@Embedded(prefix = ...)` flattens a nested object into the parent table.
|
|
98
|
+
- `@Relation(parentColumn, entityColumn)` models one-to-many.
|
|
99
|
+
- `associateBy = Junction(...)` models many-to-many through a cross-ref entity.
|
|
323
100
|
|
|
324
|
-
Always annotate relation queries with `@Transaction`
|
|
101
|
+
Always annotate relation queries with `@Transaction` for consistent reads.
|
|
102
|
+
Full embedded / one-to-many / junction examples:
|
|
103
|
+
[references/patterns.md#relations](references/patterns.md#relations).
|
|
325
104
|
|
|
326
105
|
## Migration Strategies
|
|
327
106
|
|
|
328
|
-
|
|
107
|
+
- Auto migration (`autoMigrations = [AutoMigration(from, to)]`) for additive
|
|
108
|
+
changes; supply an `AutoMigrationSpec` with `@RenameColumn`/`@DeleteColumn`
|
|
109
|
+
for renames or deletes.
|
|
110
|
+
- Manual `Migration(from, to)` with raw `execSQL` for data transformation or
|
|
111
|
+
table merges; register via `.addMigrations(...)`.
|
|
112
|
+
- `.fallbackToDestructiveMigration()` only for development or throwaway data,
|
|
113
|
+
never in release builds.
|
|
329
114
|
|
|
330
|
-
|
|
115
|
+
Full auto, manual, and destructive examples:
|
|
116
|
+
[references/patterns.md#migration-strategies](references/patterns.md#migration-strategies).
|
|
331
117
|
|
|
332
|
-
|
|
333
|
-
@Database(
|
|
334
|
-
entities = [FlightEntity::class],
|
|
335
|
-
version = 3,
|
|
336
|
-
autoMigrations = [
|
|
337
|
-
AutoMigration(from = 1, to = 2),
|
|
338
|
-
AutoMigration(from = 2, to = 3, spec = Migration2To3::class),
|
|
339
|
-
],
|
|
340
|
-
)
|
|
341
|
-
abstract class AppDatabase : RoomDatabase()
|
|
342
|
-
|
|
343
|
-
// Spec needed for column/table renames or deletes
|
|
344
|
-
@RenameColumn(tableName = "flights", fromColumnName = "price", toColumnName = "price_amount")
|
|
345
|
-
class Migration2To3 : AutoMigrationSpec
|
|
346
|
-
```
|
|
118
|
+
## Hilt Integration
|
|
347
119
|
|
|
348
|
-
|
|
120
|
+
Provide the database as a `@Singleton` from a `@Module @InstallIn(SingletonComponent::class)`
|
|
121
|
+
object, and provide each DAO from the database instance. Full `DatabaseModule`:
|
|
122
|
+
[references/patterns.md#hilt-integration](references/patterns.md#hilt-integration).
|
|
349
123
|
|
|
350
|
-
|
|
124
|
+
## Encrypted Database (SQLCipher)
|
|
351
125
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
id TEXT NOT NULL PRIMARY KEY,
|
|
358
|
-
flight_id TEXT NOT NULL,
|
|
359
|
-
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
360
|
-
created_at INTEGER NOT NULL DEFAULT 0,
|
|
361
|
-
FOREIGN KEY (flight_id) REFERENCES flights(id) ON DELETE CASCADE
|
|
362
|
-
)
|
|
363
|
-
""")
|
|
364
|
-
db.execSQL("""
|
|
365
|
-
INSERT INTO bookings_new (id, flight_id, status, created_at)
|
|
366
|
-
SELECT id, flight_id, status, 0 FROM bookings
|
|
367
|
-
""")
|
|
368
|
-
db.execSQL("DROP TABLE bookings")
|
|
369
|
-
db.execSQL("ALTER TABLE bookings_new RENAME TO bookings")
|
|
370
|
-
}
|
|
371
|
-
}
|
|
126
|
+
Encrypt the database file by passing a SQLCipher `SupportOpenHelperFactory` to
|
|
127
|
+
`.openHelperFactory(...)` on the builder. This skill owns the encrypted-Room
|
|
128
|
+
wiring; it does not own the passphrase. Derive and store the passphrase through
|
|
129
|
+
the key hierarchy in the `android-security` skill (Keystore-wrapped key), and
|
|
130
|
+
pass the resulting bytes into the factory.
|
|
372
131
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
.addMigrations(MIGRATION_3_4)
|
|
376
|
-
.build()
|
|
377
|
-
```
|
|
132
|
+
Full builder wiring:
|
|
133
|
+
[references/patterns.md#encrypted-database-sqlcipher](references/patterns.md#encrypted-database-sqlcipher).
|
|
378
134
|
|
|
379
|
-
|
|
135
|
+
## Async Database Provisioning
|
|
380
136
|
|
|
381
|
-
|
|
137
|
+
When the passphrase (or any setup input) must be derived or fetched before
|
|
138
|
+
`Room.databaseBuilder`, a synchronous `@Provides` cannot await it. Expose the
|
|
139
|
+
database from a suspend, `Mutex`-guarded, double-checked lazy singleton provider
|
|
140
|
+
instead: return the cached instance if present, otherwise build it once inside
|
|
141
|
+
the lock. Hilt provides the provider; repositories fetch DAOs from a suspend
|
|
142
|
+
accessor.
|
|
382
143
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
.fallbackToDestructiveMigration()
|
|
386
|
-
.build()
|
|
387
|
-
```
|
|
144
|
+
Full `DatabaseProvider`:
|
|
145
|
+
[references/patterns.md#async-database-provisioning](references/patterns.md#async-database-provisioning).
|
|
388
146
|
|
|
389
|
-
##
|
|
147
|
+
## Runtime Hygiene
|
|
390
148
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
Room.databaseBuilder(
|
|
400
|
-
context,
|
|
401
|
-
AppDatabase::class.java,
|
|
402
|
-
"app.db",
|
|
403
|
-
)
|
|
404
|
-
.addMigrations(MIGRATION_3_4)
|
|
405
|
-
.build()
|
|
406
|
-
|
|
407
|
-
@Provides
|
|
408
|
-
fun provideFlightDao(database: AppDatabase): FlightDao =
|
|
409
|
-
database.flightDao()
|
|
410
|
-
|
|
411
|
-
@Provides
|
|
412
|
-
fun provideBookingDao(database: AppDatabase): BookingDao =
|
|
413
|
-
database.bookingDao()
|
|
414
|
-
}
|
|
415
|
-
```
|
|
149
|
+
- Cap WAL file growth on a write-heavy database with an `onOpen` callback issuing
|
|
150
|
+
`PRAGMA wal_autocheckpoint`.
|
|
151
|
+
- Do multi-step read-then-write logic (dedupe, insert, trim-to-cap) atomically in
|
|
152
|
+
a `@Transaction` DAO default method, not across separate DAO calls.
|
|
153
|
+
- `exportSchema = false` is acceptable only for a cache or destructive-fallback
|
|
154
|
+
database whose file is disposable. A migration-tested production database keeps
|
|
155
|
+
`exportSchema = true` and commits its schema JSON, which `MigrationTestHelper`
|
|
156
|
+
validates against.
|
|
416
157
|
|
|
417
|
-
|
|
158
|
+
Full callback and transactional DAO:
|
|
159
|
+
[references/patterns.md#runtime-hygiene](references/patterns.md#runtime-hygiene).
|
|
418
160
|
|
|
419
|
-
|
|
420
|
-
class FlightDaoTest {
|
|
421
|
-
|
|
422
|
-
private lateinit var database: AppDatabase
|
|
423
|
-
private lateinit var dao: FlightDao
|
|
424
|
-
|
|
425
|
-
@Before
|
|
426
|
-
fun setup() {
|
|
427
|
-
database = Room.inMemoryDatabaseBuilder(
|
|
428
|
-
ApplicationProvider.getApplicationContext(),
|
|
429
|
-
AppDatabase::class.java,
|
|
430
|
-
)
|
|
431
|
-
.allowMainThreadQueries() // OK for tests only
|
|
432
|
-
.build()
|
|
433
|
-
|
|
434
|
-
dao = database.flightDao()
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
@After
|
|
438
|
-
fun tearDown() {
|
|
439
|
-
database.close()
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
@Test
|
|
443
|
-
fun insertAndQuery_returnsCorrectFlight() = runTest {
|
|
444
|
-
val flight = FlightEntity(
|
|
445
|
-
id = "TK1",
|
|
446
|
-
originCode = "IST",
|
|
447
|
-
originName = "Istanbul",
|
|
448
|
-
destinationCode = "JFK",
|
|
449
|
-
destinationName = "New York",
|
|
450
|
-
departureTime = "2025-01-15T10:00:00Z",
|
|
451
|
-
arrivalTime = "2025-01-15T18:00:00Z",
|
|
452
|
-
priceAmount = 799.0,
|
|
453
|
-
priceCurrency = "USD",
|
|
454
|
-
status = "SCHEDULED",
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
dao.insertFlight(flight)
|
|
458
|
-
|
|
459
|
-
val result = dao.getFlightByIdOnce("TK1")
|
|
460
|
-
assertThat(result).isNotNull()
|
|
461
|
-
assertThat(result?.originCode).isEqualTo("IST")
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
@Test
|
|
465
|
-
fun flowQuery_emitsUpdates() = runTest {
|
|
466
|
-
dao.getFlights("IST", "JFK").test {
|
|
467
|
-
assertThat(awaitItem()).isEmpty()
|
|
468
|
-
|
|
469
|
-
dao.insertFlight(testFlight)
|
|
470
|
-
|
|
471
|
-
val updated = awaitItem()
|
|
472
|
-
assertThat(updated).hasSize(1)
|
|
473
|
-
|
|
474
|
-
cancelAndIgnoreRemainingEvents()
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
@Test
|
|
479
|
-
fun upsert_updatesExistingFlight() = runTest {
|
|
480
|
-
dao.insertFlight(testFlight)
|
|
481
|
-
val updated = testFlight.copy(status = "DELAYED")
|
|
482
|
-
dao.upsertFlights(listOf(updated))
|
|
483
|
-
|
|
484
|
-
val result = dao.getFlightByIdOnce(testFlight.id)
|
|
485
|
-
assertThat(result?.status).isEqualTo("DELAYED")
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
```
|
|
161
|
+
## Testing with In-Memory Database
|
|
489
162
|
|
|
490
|
-
|
|
163
|
+
Build a `Room.inMemoryDatabaseBuilder(...)` in `@Before`, close it in `@After`,
|
|
164
|
+
and run DAO tests with `runTest`. Use Turbine's `.test { awaitItem() }` for
|
|
165
|
+
`Flow` assertions. Test migrations with `MigrationTestHelper`
|
|
166
|
+
(`createDatabase` at the old version, then `runMigrationsAndValidate`).
|
|
491
167
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
class MigrationTest {
|
|
495
|
-
|
|
496
|
-
@get:Rule
|
|
497
|
-
val helper = MigrationTestHelper(
|
|
498
|
-
InstrumentationRegistry.getInstrumentation(),
|
|
499
|
-
AppDatabase::class.java,
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
@Test
|
|
503
|
-
fun migrate3To4() {
|
|
504
|
-
// Create database at version 3
|
|
505
|
-
helper.createDatabase("test-db", 3).apply {
|
|
506
|
-
execSQL("INSERT INTO bookings (id, flight_id, status) VALUES ('B1', 'F1', 'CONFIRMED')")
|
|
507
|
-
close()
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Run migration and validate
|
|
511
|
-
val db = helper.runMigrationsAndValidate("test-db", 4, true, MIGRATION_3_4)
|
|
512
|
-
val cursor = db.query("SELECT * FROM bookings WHERE id = 'B1'")
|
|
513
|
-
assertThat(cursor.moveToFirst()).isTrue()
|
|
514
|
-
assertThat(cursor.getColumnIndex("created_at")).isAtLeast(0)
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
```
|
|
168
|
+
Full DAO tests and migration test:
|
|
169
|
+
[references/patterns.md#testing-with-in-memory-database](references/patterns.md#testing-with-in-memory-database).
|
|
518
170
|
|
|
519
171
|
## Do's and Don'ts
|
|
520
172
|
|
|
@@ -527,6 +179,10 @@ class MigrationTest {
|
|
|
527
179
|
- Use in-memory databases for unit tests
|
|
528
180
|
- Test migrations with `MigrationTestHelper`
|
|
529
181
|
- Use `stateIn` with `WhileSubscribed(5000)` in ViewModels for Flow collection
|
|
182
|
+
- Encrypt sensitive databases with a SQLCipher `SupportOpenHelperFactory`
|
|
183
|
+
- Build a database needing async setup from a suspend, `Mutex`-guarded provider
|
|
184
|
+
- Cap WAL growth with an `onOpen` `PRAGMA wal_autocheckpoint`
|
|
185
|
+
- Keep multi-step read-then-write logic inside a `@Transaction` default method
|
|
530
186
|
|
|
531
187
|
### Don'ts
|
|
532
188
|
- Do not perform Room operations on the main thread (except in tests with `allowMainThreadQueries`)
|
|
@@ -561,4 +217,9 @@ class MigrationTest {
|
|
|
561
217
|
- [ ] Migrations tested with `MigrationTestHelper`
|
|
562
218
|
- [ ] No `fallbackToDestructiveMigration` in release builds
|
|
563
219
|
- [ ] DAOs provided via Hilt as singletons (database) / transient (DAOs)
|
|
220
|
+
- [ ] Sensitive databases encrypted via a SQLCipher `SupportOpenHelperFactory`
|
|
221
|
+
- [ ] Passphrase sourced from the `android-security` key hierarchy, not reproduced
|
|
222
|
+
- [ ] Async-setup databases built from a suspend, `Mutex`-guarded provider
|
|
223
|
+
- [ ] `onOpen` `PRAGMA wal_autocheckpoint` on write-heavy databases
|
|
224
|
+
- [ ] Multi-step read-then-write logic wrapped in a `@Transaction` default method
|
|
564
225
|
- [ ] In-memory database used in unit tests
|