fix(#1): address PR review — split upsert into insert/update, add @Transaction, runBlocking seed, trailing newline
Per review on PR #8 (#8): - Split @Upsert into @Insert(OnConflictStrategy.REPLACE) + @Update in all 3 DAOs. @Upsert returns -1 on the update path, so callers wanting the row ID would get a junk value. Interactors now call insert vs update based on id == 0. UpsertCategory returns category.id explicitly for the id != 0 branch. - Add @Transaction to the 3 @Relation queries (ExpenseDao.subscribeAll, ExpenseDao.subscribeByDateRange, RecurringExpenseDao.subscribeAll). This silences the KSP warnings the PR body mentioned and makes the intent explicit. - Switch MainApplication seeding from a fire-and-forget CoroutineScope to runBlocking(Dispatchers.IO). A fast first-tap on HomeScreen could otherwise call GetCategories.awaitDefault() before seeding completed and crash. - Add documenting comment on CategoryDao.getDefault() noting that the 'only one isDefault = 1' invariant is maintained at the interactor layer (partial unique index would be a v2 migration). - Add trailing newline to app/build.gradle.kts. - File follow-up issue #9 for flipping exportSchema to true before any v2 migration lands.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package dev.achmad.ledgerr.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import androidx.room.Update
|
||||
import dev.achmad.ledgerr.data.local.entity.CategoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -18,16 +20,27 @@ interface CategoryDao {
|
||||
@Query("SELECT * FROM categories WHERE id = :id")
|
||||
suspend fun getById(id: Long): CategoryEntity?
|
||||
|
||||
/**
|
||||
* Returns the single category marked as the system default. The "Uncategorized"
|
||||
* category is seeded as the only default on first launch. There is no
|
||||
* DB-level uniqueness constraint on `isDefault = 1` (a partial unique index
|
||||
* would be a v2 migration); the invariant is maintained by `SeedDefaultCategories`
|
||||
* and `UpsertCategory` at the interactor layer. If multiple defaults ever
|
||||
* sneak in, this returns an arbitrary one.
|
||||
*/
|
||||
@Query("SELECT * FROM categories WHERE isDefault = 1 LIMIT 1")
|
||||
suspend fun getDefault(): CategoryEntity?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM categories")
|
||||
suspend fun count(): Int
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(category: CategoryEntity): Long
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(category: CategoryEntity): Long
|
||||
|
||||
@Upsert
|
||||
@Update
|
||||
suspend fun update(category: CategoryEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertAll(categories: List<CategoryEntity>)
|
||||
|
||||
@Query("DELETE FROM categories WHERE id = :id")
|
||||
|
||||
@@ -3,9 +3,11 @@ package dev.achmad.ledgerr.data.local.dao
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Relation
|
||||
import androidx.room.Upsert
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import dev.achmad.ledgerr.data.local.entity.CategoryEntity
|
||||
import dev.achmad.ledgerr.data.local.entity.ExpenseEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -22,9 +24,11 @@ data class ExpenseWithCategoryRow(
|
||||
@Dao
|
||||
interface ExpenseDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM expenses ORDER BY date DESC, createdAt DESC")
|
||||
fun subscribeAll(): Flow<List<ExpenseWithCategoryRow>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM expenses " +
|
||||
"WHERE date BETWEEN :startDay AND :endDay " +
|
||||
@@ -50,8 +54,11 @@ interface ExpenseDao {
|
||||
)
|
||||
suspend fun getByDateRange(startDay: Long, endDay: Long): List<ExpenseEntity>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(expense: ExpenseEntity): Long
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(expense: ExpenseEntity): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(expense: ExpenseEntity)
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(expenses: List<ExpenseEntity>)
|
||||
|
||||
@@ -2,9 +2,12 @@ package dev.achmad.ledgerr.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Relation
|
||||
import androidx.room.Upsert
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import dev.achmad.ledgerr.data.local.entity.CategoryEntity
|
||||
import dev.achmad.ledgerr.data.local.entity.RecurringExpenseEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -21,6 +24,7 @@ data class RecurringExpenseWithCategoryRow(
|
||||
@Dao
|
||||
interface RecurringExpenseDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM recurring_expenses ORDER BY nextDueDate ASC")
|
||||
fun subscribeAll(): Flow<List<RecurringExpenseWithCategoryRow>>
|
||||
|
||||
@@ -30,8 +34,11 @@ interface RecurringExpenseDao {
|
||||
@Query("SELECT * FROM recurring_expenses WHERE isActive = 1 AND nextDueDate <= :today")
|
||||
suspend fun getDue(today: Long): List<RecurringExpenseEntity>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(recurring: RecurringExpenseEntity): Long
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(recurring: RecurringExpenseEntity): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(recurring: RecurringExpenseEntity)
|
||||
|
||||
@Query("DELETE FROM recurring_expenses WHERE id = :id")
|
||||
suspend fun deleteById(id: Long)
|
||||
|
||||
@@ -9,7 +9,7 @@ class UpsertCategory(
|
||||
) {
|
||||
suspend fun await(category: Category): Long {
|
||||
if (category.id == 0L) {
|
||||
return dao.upsert(category.toEntity())
|
||||
return dao.insert(category.toEntity())
|
||||
}
|
||||
val existing = dao.getById(category.id)
|
||||
val resolved = if (existing != null && category.isDefault && !existing.isDefault) {
|
||||
@@ -17,6 +17,7 @@ class UpsertCategory(
|
||||
} else {
|
||||
category
|
||||
}
|
||||
return dao.upsert(resolved.toEntity())
|
||||
dao.update(resolved.toEntity())
|
||||
return category.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import dev.achmad.ledgerr.di.domainModule
|
||||
import dev.achmad.ledgerr.di.preferenceModule
|
||||
import dev.achmad.ledgerr.di.util.inject
|
||||
import dev.achmad.ledgerr.domain.category.interactor.SeedDefaultCategories
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
@@ -23,7 +22,7 @@ class MainApplication : Application() {
|
||||
androidContext(this@MainApplication)
|
||||
modules(coreModule, dataModule, domainModule, preferenceModule)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
inject<SeedDefaultCategories>().await()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user