Add Trail Sense Room persistence for a model, including entity mapping, DAO, repository, AppDatabase migration, and tool registration.
67
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/trail-sense-database-persistence/SKILL.mdAdd 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 # RepositoryAlso 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 singletonfrom() and to{Model}().AppDatabase. This step is complete when the migration SQL exactly matches the entity columns, nullability, primary key, and indices../scripts/build.sh or a focused unit test passes, or the exact blocker is reported.package 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 -> SQL INTEGER epoch millis; entity field may use Instant with converter or Long mappingDuration -> SQL INTEGER millis; entity field may use Duration with converter or Long mappingCoordinate -> split into latitude: Double, longitude: DoubleDistance -> store as Float in metersid property -> SQL INTEGER; use existing converters or manual id mappingAppColor -> SQL INTEGER; 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}DaoInside 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 |
154b390
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.