Refactor: Enhance AppDao query capabilities
This commit refactors the `AppDao` to provide more flexible and robust querying options for application data. Key changes: - **Renamed `_rawQueryAppEntities` to `_rawStreamAppEntities`**: This clarifies the function's purpose of returning a Flow of entities. - **Added `_rawQueryAppEntities`**: A new suspend function that directly returns a List of entities, for non-streaming use cases. - **Introduced `query` function**: A new public suspend function that mirrors the functionality of `stream` but returns a `List<AppEntity>` instead of a `Flow`. - **Enhanced `searchQuery` private function**: - Now accepts lists for `categoriesToInclude`, `categoriesToExclude`, `antiFeaturesToInclude`, and `antiFeaturesToExclude` to allow filtering by multiple criteria. - Uses `DISTINCT` in the SQL query to avoid duplicate app entries. - Corrected join condition for `category_app_relation` from `app.id = category_app_relation.appId` to `app.id = category_app_relation.id`. - Corrected table name for anti-features from `anti_feature_app_relation` to `anti_features_app_relation`. - Improved SQL query construction for category and anti-feature filtering using `IN` and `NOT IN` clauses. - Ensured `ORDER BY` clause is always present, even if `searchQuery` is null. - Prefixed table names in `ORDER BY` clause (e.g., `app.lastUpdated`) for clarity and to avoid ambiguity. - **Updated `stream` function**: Now utilizes the refactored `searchQuery` function and passes through all new filtering parameters. - **Updated database schema**: - Changed `onDelete` action for the foreign key in the `authentication` table to `CASCADE`. - **Updated Room tests**: - Simplified setup by removing legacy database initialization. - Added tests for new sorting and category filtering functionalities in `AppDao`.
This commit is contained in:
parent
c943c2a149
commit
ae2bdaea19
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "099d07ab258fe12cb0a660545bd36e63",
|
||||
"identityHash": "e64dac21017b34894f6d9d7890184178",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "anti_feature",
|
||||
@ -142,7 +142,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "authentication",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` TEXT NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` TEXT NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "password",
|
||||
@ -179,7 +179,7 @@
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "repository",
|
||||
"onDelete": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"repoId"
|
||||
@ -1078,7 +1078,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '099d07ab258fe12cb0a660545bd36e63')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e64dac21017b34894f6d9d7890184178')"
|
||||
]
|
||||
}
|
||||
}
|
@ -3,32 +3,25 @@ package com.looker.droidify
|
||||
import android.content.Context
|
||||
import com.looker.droidify.data.local.dao.AppDao
|
||||
import com.looker.droidify.data.local.dao.IndexDao
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.domain.model.VersionInfo
|
||||
import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.index.RepositoryUpdater.IndexType
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.sync.FakeDownloader
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.common.assets
|
||||
import com.looker.droidify.sync.common.benchmark
|
||||
import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@HiltAndroidTest
|
||||
class RoomTesting {
|
||||
@ -53,98 +46,60 @@ class RoomTesting {
|
||||
@Before
|
||||
fun before() = runTest {
|
||||
hiltRule.inject()
|
||||
setupLegacy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roomBenchmark() = runTest {
|
||||
launch {
|
||||
val izzy = izzyLegacy.toRepo(1)
|
||||
val insert = benchmark(1) {
|
||||
val v2File = FakeDownloader
|
||||
.downloadIndex(context, izzy, "izzy-v2", "index-v2.json")
|
||||
measureTimeMillis {
|
||||
val index = JsonParser.decodeFromString<IndexV2>(
|
||||
v2File.readBytes().decodeToString(),
|
||||
)
|
||||
val izzyFile = FakeDownloader.downloadIndex(context, izzy, "i2", "index-v2.json")
|
||||
val izzyIndex =
|
||||
JsonParser.decodeFromString<IndexV2>(izzyFile.readBytes().decodeToString())
|
||||
indexDao.insertIndex(
|
||||
fingerprint = izzy.fingerprint!!,
|
||||
index = index,
|
||||
index = izzyIndex,
|
||||
expectedRepoId = izzy.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
val fdroid = fdroidLegacy.toRepo(2)
|
||||
val insertFDroid = benchmark(1) {
|
||||
val v2File = FakeDownloader
|
||||
.downloadIndex(context, fdroid, "fdroid-v2", "fdroid-index-v2.json")
|
||||
measureTimeMillis {
|
||||
val index = JsonParser.decodeFromString<IndexV2>(
|
||||
v2File.readBytes().decodeToString(),
|
||||
)
|
||||
indexDao.insertIndex(
|
||||
fingerprint = fdroid.fingerprint!!,
|
||||
index = index,
|
||||
expectedRepoId = fdroid.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
val query = appDao.queryAppEntity("com.looker.droidify")
|
||||
println(query.first().joinToString("\n"))
|
||||
println(insert)
|
||||
println(insertFDroid)
|
||||
// launch {
|
||||
// val fdroid = fdroidLegacy.toRepo(2)
|
||||
// val fdroidFile =
|
||||
// FakeDownloader.downloadIndex(context, fdroid, "f2", "fdroid-index-v2.json")
|
||||
// val fdroidIndex =
|
||||
// JsonParser.decodeFromString<IndexV2>(fdroidFile.readBytes().decodeToString())
|
||||
// indexDao.insertIndex(
|
||||
// fingerprint = fdroid.fingerprint!!,
|
||||
// index = fdroidIndex,
|
||||
// expectedRepoId = fdroid.id,
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun legacyBenchmark() {
|
||||
val insert = benchmark(1) {
|
||||
val createFile = File.createTempFile("index", "entry")
|
||||
val mergerFile = File.createTempFile("index", "merger")
|
||||
val jarStream = assets("izzy_index_v1.jar")
|
||||
jarStream.copyTo(createFile.outputStream())
|
||||
measureTimeMillis {
|
||||
RepositoryUpdater.processFile(
|
||||
context = context,
|
||||
repository = izzyLegacy,
|
||||
indexType = IndexType.INDEX_V1,
|
||||
unstable = false,
|
||||
file = createFile,
|
||||
mergerFile = mergerFile,
|
||||
lastModified = "",
|
||||
entityTag = "",
|
||||
callback = { _, _, _ -> },
|
||||
)
|
||||
createFile.delete()
|
||||
mergerFile.delete()
|
||||
}
|
||||
}
|
||||
val insertFDroid = benchmark(1) {
|
||||
val createFile = File.createTempFile("index", "entry")
|
||||
val mergerFile = File.createTempFile("index", "merger")
|
||||
val jarStream = assets("fdroid_index_v1.jar")
|
||||
jarStream.copyTo(createFile.outputStream())
|
||||
measureTimeMillis {
|
||||
RepositoryUpdater.processFile(
|
||||
context = context,
|
||||
repository = fdroidLegacy,
|
||||
indexType = IndexType.INDEX_V1,
|
||||
unstable = false,
|
||||
file = createFile,
|
||||
mergerFile = mergerFile,
|
||||
lastModified = "",
|
||||
entityTag = "",
|
||||
callback = { _, _, _ -> },
|
||||
)
|
||||
createFile.delete()
|
||||
mergerFile.delete()
|
||||
}
|
||||
}
|
||||
println(insert)
|
||||
println(insertFDroid)
|
||||
fun sortOrderTest() = runTest {
|
||||
val lastUpdatedQuery = appDao.query(sortOrder = SortOrder.UPDATED)
|
||||
var previousUpdated = Long.MAX_VALUE
|
||||
lastUpdatedQuery.forEach {
|
||||
println("Previous: $previousUpdated, Current: ${it.lastUpdated}")
|
||||
assertTrue(it.lastUpdated <= previousUpdated)
|
||||
previousUpdated = it.lastUpdated
|
||||
}
|
||||
|
||||
private fun setupLegacy() {
|
||||
Database.init(context)
|
||||
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
|
||||
val addedQuery = appDao.query(sortOrder = SortOrder.ADDED)
|
||||
var previousAdded = Long.MAX_VALUE
|
||||
addedQuery.forEach {
|
||||
println("Previous: $previousAdded, Current: ${it.added}")
|
||||
assertTrue(it.added <= previousAdded)
|
||||
previousAdded = it.added
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun categoryTest() = runTest {
|
||||
val categoryQuery = appDao.query(
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
categoriesToInclude = listOf("Games", "Food"),
|
||||
)
|
||||
val nonCategoryQuery = appDao.query(
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
categoriesToExclude = listOf("Games", "Food"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,67 +18,129 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface AppDao {
|
||||
|
||||
@RawQuery(observedEntities = [
|
||||
@RawQuery(
|
||||
observedEntities = [
|
||||
AppEntity::class,
|
||||
VersionEntity::class,
|
||||
CategoryAppRelation::class,
|
||||
AntiFeatureAppRelation::class,
|
||||
])
|
||||
fun _rawQueryAppEntities(query: SimpleSQLiteQuery): Flow<List<AppEntity>>
|
||||
],
|
||||
)
|
||||
fun _rawStreamAppEntities(query: SimpleSQLiteQuery): Flow<List<AppEntity>>
|
||||
|
||||
@RawQuery
|
||||
suspend fun _rawQueryAppEntities(query: SimpleSQLiteQuery): List<AppEntity>
|
||||
|
||||
/**
|
||||
* @param category: get by default name and use the boolean to ignore the app category
|
||||
* @param antiFeature: get by tag and use the boolean to ignore the anti feature
|
||||
*
|
||||
* Add `canUpdate` and `isInstalled` is the next layer
|
||||
* */
|
||||
fun stream(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String? = null,
|
||||
repoId: Int? = null,
|
||||
categoriesToInclude: List<DefaultName>? = null,
|
||||
categoriesToExclude: List<DefaultName>? = null,
|
||||
antiFeaturesToInclude: List<Tag>? = null,
|
||||
antiFeaturesToExclude: List<Tag>? = null,
|
||||
): Flow<List<AppEntity>> = _rawStreamAppEntities(
|
||||
searchQuery(
|
||||
sortOrder = sortOrder,
|
||||
searchQuery = searchQuery,
|
||||
repoId = repoId,
|
||||
categoriesToInclude = categoriesToInclude,
|
||||
categoriesToExclude = categoriesToExclude,
|
||||
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||
),
|
||||
)
|
||||
|
||||
suspend fun query(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String? = null,
|
||||
repoId: Int? = null,
|
||||
categoriesToInclude: List<DefaultName>? = null,
|
||||
categoriesToExclude: List<DefaultName>? = null,
|
||||
antiFeaturesToInclude: List<Tag>? = null,
|
||||
antiFeaturesToExclude: List<Tag>? = null,
|
||||
): List<AppEntity> = _rawQueryAppEntities(
|
||||
searchQuery(
|
||||
sortOrder = sortOrder,
|
||||
searchQuery = searchQuery,
|
||||
repoId = repoId,
|
||||
categoriesToInclude = categoriesToInclude,
|
||||
categoriesToExclude = categoriesToExclude,
|
||||
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||
),
|
||||
)
|
||||
|
||||
private fun searchQuery(
|
||||
sortOrder: SortOrder,
|
||||
searchQuery: String?,
|
||||
repoId: Int?,
|
||||
category: Pair<DefaultName, Boolean>?,
|
||||
antiFeature: Pair<Tag, Boolean>?,
|
||||
): Flow<List<AppEntity>> {
|
||||
categoriesToInclude: List<DefaultName>?,
|
||||
categoriesToExclude: List<DefaultName>?,
|
||||
antiFeaturesToInclude: List<Tag>?,
|
||||
antiFeaturesToExclude: List<Tag>?,
|
||||
): SimpleSQLiteQuery {
|
||||
val args = arrayListOf<Any?>()
|
||||
|
||||
val query = buildString(1024) {
|
||||
append("SELECT app.* FROM app")
|
||||
append("SELECT DISTINCT app.* FROM app")
|
||||
append(" LEFT JOIN version ON app.id = version.appId")
|
||||
append(" LEFT JOIN category_app_relation ON app.id = category_app_relation.appId")
|
||||
append(" LEFT JOIN anti_feature_app_relation ON app.id = anti_feature_app_relation.appId")
|
||||
append(" LEFT JOIN category_app_relation ON app.id = category_app_relation.id")
|
||||
append(" LEFT JOIN anti_features_app_relation ON app.id = anti_features_app_relation.appId")
|
||||
append(" WHERE 1")
|
||||
|
||||
if (repoId != null) {
|
||||
append(" AND app.repoId = ?")
|
||||
args.add(repoId)
|
||||
}
|
||||
|
||||
if (category != null) {
|
||||
val (name, hide) = category
|
||||
if (!hide) {
|
||||
append(" AND category_app_relation.defaultName = ?")
|
||||
} else {
|
||||
append(" AND category_app_relation.defaultName != ?")
|
||||
}
|
||||
args.add(name)
|
||||
if (categoriesToInclude != null) {
|
||||
append(" AND category_app_relation.defaultName IN (")
|
||||
append(categoriesToInclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(categoriesToInclude)
|
||||
}
|
||||
|
||||
if (antiFeature != null) {
|
||||
val (tag, hide) = antiFeature
|
||||
if (!hide) {
|
||||
append(" AND anti_feature_app_relation.tag = ?")
|
||||
} else {
|
||||
append(" AND anti_feature_app_relation.tag != ?")
|
||||
if (categoriesToExclude != null) {
|
||||
append(" AND category_app_relation.defaultName NOT IN (")
|
||||
append(categoriesToExclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(categoriesToExclude)
|
||||
}
|
||||
args.add(tag)
|
||||
|
||||
if (antiFeaturesToInclude != null) {
|
||||
append(" AND anti_features_app_relation.tag IN (")
|
||||
append(antiFeaturesToInclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(antiFeaturesToInclude)
|
||||
}
|
||||
|
||||
if (antiFeaturesToExclude != null) {
|
||||
append(" AND anti_features_app_relation.tag NOT IN (")
|
||||
append(antiFeaturesToExclude.joinToString(", ") { "?" })
|
||||
append(")")
|
||||
args.addAll(antiFeaturesToExclude)
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
val searchPattern = "%${searchQuery}%"
|
||||
append(" AND (app.name LIKE ? OR app.summary LIKE ? OR app.packageName LIKE ? OR app.description LIKE ?)")
|
||||
append(
|
||||
"""
|
||||
AND (
|
||||
app.name LIKE ?
|
||||
OR app.summary LIKE ?
|
||||
OR app.packageName LIKE ?
|
||||
OR app.description LIKE ?
|
||||
)""",
|
||||
)
|
||||
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||
}
|
||||
|
||||
append(" ORDER BY ")
|
||||
|
||||
// Weighting: name > summary > packageName > description
|
||||
if (searchQuery != null) {
|
||||
val searchPattern = "%${searchQuery}%"
|
||||
append("(CASE WHEN app.name LIKE ? THEN 4 ELSE 0 END) + ")
|
||||
append("(CASE WHEN app.summary LIKE ? THEN 3 ELSE 0 END) + ")
|
||||
append("(CASE WHEN app.packageName LIKE ? THEN 2 ELSE 0 END) + ")
|
||||
@ -86,12 +148,8 @@ interface AppDao {
|
||||
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||
}
|
||||
|
||||
if (searchQuery == null) {
|
||||
append(" ORDER BY ")
|
||||
}
|
||||
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("app,lastUpdated DESC, ")
|
||||
SortOrder.UPDATED -> append("app.lastUpdated DESC, ")
|
||||
SortOrder.ADDED -> append("app.added DESC, ")
|
||||
SortOrder.SIZE -> append("version.apk_size DESC, ")
|
||||
SortOrder.NAME -> Unit
|
||||
@ -99,7 +157,7 @@ interface AppDao {
|
||||
append("app.name COLLATE LOCALIZED ASC")
|
||||
}
|
||||
|
||||
return _rawQueryAppEntities(SimpleSQLiteQuery(query, args.toTypedArray()))
|
||||
return SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
}
|
||||
|
||||
@Query(
|
||||
|
Loading…
x
Reference in New Issue
Block a user