.agents/skills/trail-sense-database-persistence/SKILL.md
Add new Room database persistence to Trail-Sense Android app. Use when the user asks to create, add, or implement database persistence for a model, including Entity, DAO, Repository, and AppDatabase migration. Covers entity-to-model mapping, index configuration, and standard CRUD operations.
npx skillsauth add kylecorry31/trail-sense trail-sense-database-persistenceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Add Room database persistence for a domain model following Trail-Sense patterns.
app/src/main/java/com/kylecorry/trail_sense/tools/{toolName}/
├── domain/
│ └── {Model}.kt # Domain model (if not already exists)
└── infrastructure/persistence/
├── {Model}Entity.kt # Room entity
├── {Model}Dao.kt # DAO interface
└── {Model}Repo.kt # Repository
Also update:
app/src/main/java/com/kylecorry/trail_sense/main/persistence/AppDatabase.ktapp/src/main/java/com/kylecorry/trail_sense/main/persistence/Converters.kt (if new types needed){ToolName}ToolRegistration.kt - register repo singletonpackage com.kylecorry.trail_sense.tools.{toolname}.infrastructure.persistence
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.kylecorry.trail_sense.tools.{toolname}.domain.{Model}
@Entity(
tableName = "{table_name}", // plural, lowercase, snake_case
indices = [
// Add indices for foreign keys and frequently queried columns
// Index(value = ["parent_id"]),
// Index(value = ["time"])
]
)
data class {Model}Entity(
@ColumnInfo(name = "column_name") val property: Type,
// ... other properties
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "_id")
var id: Long = 0
fun to{Model}(): {Model} {
return {Model}(
id = id,
// Map entity properties to domain model
)
}
companion object {
fun from(model: {Model}): {Model}Entity {
return {Model}Entity(
// Map domain model properties to entity
).also {
it.id = model.id
}
}
}
}
Add indices for:
parent_id, group_id)Index(value = ["col1", "col2"])Instant -> stored as Long (epoch millis), converter existsDuration -> stored as Long (millis), converter existsCoordinate -> split into latitude: Double, longitude: DoubleDistance -> store as Float in metersid property -> use existing converters or add to Converters.ktAppColor -> converter existspackage com.kylecorry.trail_sense.tools.{toolname}.infrastructure.persistence
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface {Model}Dao {
@Query("SELECT * FROM {table_name}")
fun getAll(): Flow<List<{Model}Entity>>
@Query("SELECT * FROM {table_name}")
suspend fun getAllSync(): List<{Model}Entity>
@Query("SELECT * FROM {table_name} WHERE _id = :id LIMIT 1")
suspend fun get(id: Long): {Model}Entity?
@Upsert
suspend fun upsert(entity: {Model}Entity): Long
@Delete
suspend fun delete(entity: {Model}Entity)
}
// Filter by parent/group
@Query("SELECT * FROM {table_name} WHERE parent_id IS :parentId")
suspend fun getAllInGroup(parentId: Long?): List<{Model}Entity>
// Time-based cleanup
@Query("DELETE FROM {table_name} WHERE time < :minEpochMillis")
suspend fun deleteOlderThan(minEpochMillis: Long)
// Get latest
@Query("SELECT * FROM {table_name} ORDER BY _id DESC LIMIT 1")
suspend fun getLast(): {Model}Entity?
In AppDatabase.kt, add entity to @Database annotation:
@Database(
entities = [..., {Model}Entity::class],
version = {NEXT_VERSION}, // Increment from current
exportSchema = false
)
abstract fun {model}Dao(): {Model}Dao
Inside buildDatabase(), add migration before the return Room.databaseBuilder:
val MIGRATION_{PREV}_{NEXT} = object : Migration({PREV}, {NEXT}) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("""
CREATE TABLE IF NOT EXISTS `{table_name}` (
`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`column_name` TEXT NOT NULL,
`nullable_column` TEXT DEFAULT NULL
-- Match column types: TEXT, INTEGER, REAL
-- NOT NULL for non-nullable, DEFAULT NULL for nullable
)
""".trimIndent())
// Add indices if defined in entity
// db.execSQL("CREATE INDEX IF NOT EXISTS index_{table_name}_{column} ON {table_name}({column})")
}
}
Add to .addMigrations():
.addMigrations(
...,
MIGRATION_{PREV}_{NEXT}
)
package com.kylecorry.trail_sense.tools.{toolname}.infrastructure.persistence
import android.annotation.SuppressLint
import android.content.Context
import com.kylecorry.luna.coroutines.onIO
import com.kylecorry.trail_sense.main.persistence.AppDatabase
import com.kylecorry.trail_sense.tools.{toolname}.domain.{Model}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class {Model}Repo private constructor(context: Context) {
private val dao = AppDatabase.getInstance(context).{model}Dao()
fun getAll(): Flow<List<{Model}>> = dao.getAll()
.map { it.map { entity -> entity.to{Model}() } }
.flowOn(Dispatchers.IO)
suspend fun getAllSync(): List<{Model}> = onIO {
dao.getAllSync().map { it.to{Model}() }
}
suspend fun get(id: Long): {Model}? = onIO {
dao.get(id)?.to{Model}()
}
suspend fun add(model: {Model}): Long = onIO {
dao.upsert({Model}Entity.from(model))
}
suspend fun delete(model: {Model}) = onIO {
dao.delete({Model}Entity.from(model))
}
companion object {
@SuppressLint("StaticFieldLeak")
private var instance: {Model}Repo? = null
@Synchronized
fun getInstance(context: Context): {Model}Repo {
if (instance == null) {
instance = {Model}Repo(context.applicationContext)
}
return instance!!
}
}
}
In {ToolName}ToolRegistration.kt, add the repo to singletons:
object {ToolName}ToolRegistration : ToolRegistration {
override fun getTool(context: Context): Tool {
return Tool(
// ... other properties
singletons = listOf(
{Model}Repo::getInstance
),
// ...
)
}
}
| Kotlin Type | SQLite Type | Notes |
|-------------|-------------|-------|
| Long, Int | INTEGER | |
| Double, Float | REAL | |
| String | TEXT | |
| Boolean | INTEGER | 0/1 |
| Instant | INTEGER | epoch millis |
| Enums | INTEGER | via id property |
| Nullable | Add DEFAULT NULL | |
tools
Write or update user guide documentation for Trail-Sense tools. Use when asked to create, write, update, or modify a tool's user guide, help documentation, or user-facing documentation. Covers guide structure, formatting conventions, and common sections.
tools
Add UI automation tests to Trail-Sense Android app using AutomationLibrary. Use when asked to create, add, write, or implement automated tests, UI tests, integration tests, or androidTests for Trail Sense tools. Covers test class structure, AutomationLibrary functions, and testing patterns.
testing
Audit translation accuracy for strings changed in a GitHub PR. Use when asked to check, review, audit, or verify translations in a pull request. Identifies inaccurate translated strings by comparing PR changes against the English source.
data-ai
Example TaskFlow authoring pattern for inbox triage. Use when messages need different treatment based on intent, with some routes notifying immediately, some waiting on outside answers, and others rolling into a later summary.