Merge DB migration branch (Warning: possible conflict solving failures)

This commit is contained in:
machiav3lli
2021-12-24 13:20:38 +01:00
22 changed files with 477 additions and 175 deletions

View File

@ -87,9 +87,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
val request = activeRequests[id]!!.request
val db = DatabaseX.getInstance(requireContext())
return QueryLoader(requireContext()) {
when (request) {
is Request.ProductsAvailable -> Database.ProductAdapter
is Request.ProductsAvailable -> db.productDao
.query(
installed = false,
updates = false,
@ -98,7 +99,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order,
signal = it
)
is Request.ProductsInstalled -> Database.ProductAdapter
is Request.ProductsInstalled -> db.productDao
.query(
installed = true,
updates = false,
@ -107,7 +108,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order,
signal = it
)
is Request.ProductsUpdates -> Database.ProductAdapter
is Request.ProductsUpdates -> db.productDao
.query(
installed = true,
updates = true,
@ -116,7 +117,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
order = request.order,
signal = it
)
is Request.Repositories -> Database.RepositoryAdapter.query(it)
is Request.Repositories -> db.repositoryDao.allCursor
}
}
}

View File

@ -1,57 +1,171 @@
package com.looker.droidify.database
import android.database.SQLException
import android.database.Cursor
import android.os.CancellationSignal
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.looker.droidify.*
import com.looker.droidify.entity.ProductItem
@Dao
interface RepositoryDao {
@Insert
@Throws(SQLException::class)
fun insert(vararg repository: Repository)
interface BaseDao<T> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg product: T)
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg repository: Repository?)
fun update(vararg obj: T): Int
fun put(repository: Repository) {
if (repository.id >= 0L) update(repository) else insert(repository)
@Delete
fun delete(obj: T)
}
@Dao
interface RepositoryDao : BaseDao<Repository> {
fun put(repository: com.looker.droidify.entity.Repository): com.looker.droidify.entity.Repository {
repository.let {
val dbRepo = Repository().apply {
id = it.id
enabled = if (it.enabled) 1 else 0
deleted = false
data = it
}
val newId = if (repository.id >= 0L) update(dbRepo).toLong() else returnInsert(dbRepo)
return if (newId != repository.id) repository.copy(id = newId) else repository
}
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun returnInsert(product: Repository): Long
@Query("SELECT * FROM repository WHERE _id = :id and deleted == 0")
fun get(id: Long): Repository?
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
val allCursor: Cursor
@get:Query("SELECT * FROM repository WHERE deleted == 0 ORDER BY _id ASC")
val all: List<Repository>
@get:Query("SELECT _id, deleted FROM repository WHERE deleted != 0 and enabled == 0 ORDER BY _id ASC")
val allDisabledDeleted: List<Repository.IdAndDeleted>
@Delete
fun delete(repository: Repository)
@Query("DELETE FROM repository WHERE _id = :id")
fun deleteById(vararg id: Long): Int
// TODO optimize
@Update(onConflict = OnConflictStrategy.REPLACE)
fun markAsDeleted(id: Long) {
update(get(id).apply { this?.deleted = 1 })
get(id).apply { this?.deleted = true }?.let { update(it) }
}
}
@Dao
interface ProductDao {
interface ProductDao : BaseDao<Product> {
@Query("SELECT COUNT(*) FROM product WHERE repository_id = :id")
fun countForRepository(id: Long): Long
@Query("SELECT * FROM product WHERE package_name = :packageName")
fun get(packageName: String): Product?
fun get(packageName: String): List<Product?>
@Query("DELETE FROM product WHERE repository_id = :id")
fun deleteById(vararg id: Long): Int
@RawQuery
fun query(
query: SupportSQLiteQuery
): Cursor
// TODO optimize and simplify
@Transaction
fun query(
installed: Boolean, updates: Boolean, searchQuery: String,
section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?
): Cursor {
val builder = QueryBuilder()
val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND
product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND
product.${ROW_SIGNATURES} != ''"""
builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID},
product.${ROW_PACKAGE_NAME}, product.${ROW_NAME},
product.${ROW_SUMMARY}, installed.${ROW_VERSION},
(COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND
product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} >
COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE},
product.${ROW_DATA_ITEM},"""
if (searchQuery.isNotEmpty()) {
builder += """(((product.${ROW_NAME} LIKE ? OR
product.${ROW_SUMMARY} LIKE ?) * 7) |
((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) |
(product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},"""
builder %= List(4) { "%$searchQuery%" }
} else {
builder += "0 AS ${ROW_MATCH_RANK},"
}
builder += """MAX((product.${ROW_COMPATIBLE} AND
(installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product"""
builder += """JOIN $ROW_REPOSITORY_NAME AS repository
ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}"""
builder += """LEFT JOIN $ROW_LOCK_NAME AS lock
ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}"""
if (!installed && !updates) {
builder += "LEFT"
}
builder += """JOIN $ROW_INSTALLED_NAME AS installed
ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}"""
if (section is ProductItem.Section.Category) {
builder += """JOIN $ROW_CATEGORY_NAME AS category
ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}"""
}
builder += """WHERE repository.${ROW_ENABLED} != 0 AND
repository.${ROW_DELETED} == 0"""
if (section is ProductItem.Section.Category) {
builder += "AND category.${ROW_NAME} = ?"
builder %= section.name
} else if (section is ProductItem.Section.Repository) {
builder += "AND product.${ROW_REPOSITORY_ID} = ?"
builder %= section.id.toString()
}
if (searchQuery.isNotEmpty()) {
builder += """AND $ROW_MATCH_RANK > 0"""
}
builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1"
if (updates) {
builder += "AND $ROW_CAN_UPDATE"
}
builder += "ORDER BY"
if (searchQuery.isNotEmpty()) {
builder += """$ROW_MATCH_RANK DESC,"""
}
when (order) {
ProductItem.Order.NAME -> Unit
ProductItem.Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC,"
ProductItem.Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC,"
}::class
builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC"
return query(SimpleSQLiteQuery(builder.build()))
}
}
@Dao
interface CategoryDao {
@Query(
interface CategoryDao : BaseDao<Category> {
@get:Query(
"""SELECT DISTINCT category.name
FROM category AS category
JOIN repository AS repository
@ -59,31 +173,85 @@ interface CategoryDao {
WHERE repository.enabled != 0 AND
repository.deleted == 0"""
)
fun getAll(): List<String>
val allNames: List<String>
@Query("DELETE FROM category WHERE repository_id = :id")
fun deleteById(vararg id: Long): Int
}
@Dao
interface InstalledDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Throws(SQLException::class)
fun insert(vararg installed: Installed)
interface InstalledDao : BaseDao<Installed> {
fun put(vararg isntalled: com.looker.droidify.entity.InstalledItem) {
isntalled.forEach {
insert(Installed(it.packageName).apply {
version = it.version
version_code = it.versionCode
signature = it.signature
})
}
}
@Query("SELECT * FROM installed WHERE package_name = :packageName")
fun get(packageName: String): Installed?
@Query("SELECT * FROM memory_installed WHERE package_name = :packageName")
fun get(packageName: String): Cursor
@Query("DELETE FROM installed WHERE package_name = :packageName")
@Query("DELETE FROM memory_installed WHERE package_name = :packageName")
fun delete(packageName: String)
}
@Dao
interface LockDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Throws(SQLException::class)
fun insert(vararg lock: Lock)
@Query("DELETE FROM lock WHERE package_name = :packageName")
interface LockDao : BaseDao<Lock> {
@Query("DELETE FROM memory_lock WHERE package_name = :packageName")
fun delete(packageName: String)
}
@Dao
interface ProductTempDao : BaseDao<ProductTemp> {
@get:Query("SELECT * FROM temporary_product")
val all: Array<ProductTemp>
@Query("DELETE FROM temporary_product")
fun emptyTable()
@Insert
fun insertCategory(vararg product: CategoryTemp)
@Transaction
fun putTemporary(products: List<com.looker.droidify.entity.Product>) {
products.forEach {
val signatures = it.signatures.joinToString { ".$it" }
.let { if (it.isNotEmpty()) "$it." else "" }
insert(it.let {
ProductTemp().apply {
repository_id = it.repositoryId
package_name = it.packageName
name = it.name
summary = it.summary
description = it.description
added = it.added
updated = it.updated
version_code = it.versionCode
this.signatures = signatures
compatible = if (it.compatible) 1 else 0
data = it
data_item = it.item()
}
})
it.categories.forEach { category ->
insertCategory(CategoryTemp().apply {
repository_id = it.repositoryId
package_name = it.packageName
name = category
})
}
}
}
}
@Dao
interface CategoryTempDao : BaseDao<CategoryTemp> {
@get:Query("SELECT * FROM temporary_category")
val all: Array<CategoryTemp>
@Query("DELETE FROM temporary_category")
fun emptyTable()
}

View File

@ -10,7 +10,9 @@ import androidx.room.TypeConverters
entities = [
Repository::class,
Product::class,
ProductTemp::class,
Category::class,
CategoryTemp::class,
Installed::class,
Lock::class
], version = 1
@ -19,7 +21,9 @@ import androidx.room.TypeConverters
abstract class DatabaseX : RoomDatabase() {
abstract val repositoryDao: RepositoryDao
abstract val productDao: ProductDao
abstract val productTempDao: ProductTempDao
abstract val categoryDao: CategoryDao
abstract val categoryTempDao: CategoryTempDao
abstract val installedDao: InstalledDao
abstract val lockDao: LockDao
@ -46,17 +50,33 @@ abstract class DatabaseX : RoomDatabase() {
}
fun cleanUp(pairs: Set<Pair<Long, Boolean>>) {
val result = pairs.windowed(10, 10, true).map {
val ids = it.map { it.first }.toLongArray()
val productsCount = productDao.deleteById(*ids)
val categoriesCount = categoryDao.deleteById(*ids)
val deleteIds = it.filter { it.second }.map { it.first }.toLongArray()
repositoryDao.deleteById(*deleteIds)
productsCount != 0 || categoriesCount != 0
runInTransaction {
val result = pairs.windowed(10, 10, true).map {
val ids = it.map { it.first }.toLongArray()
val productsCount = productDao.deleteById(*ids)
val categoriesCount = categoryDao.deleteById(*ids)
val deleteIds = it.filter { it.second }.map { it.first }.toLongArray()
repositoryDao.deleteById(*deleteIds)
productsCount != 0 || categoriesCount != 0
}
}
// Use live objects and observers instead
/*if (result.any { it }) {
com.looker.droidify.database.Database.notifyChanged(com.looker.droidify.database.Database.Subject.Products)
}*/
}
fun finishTemporary(repository: com.looker.droidify.entity.Repository, success: Boolean) {
runInTransaction {
if (success) {
productDao.deleteById(repository.id)
categoryDao.deleteById(repository.id)
productDao.insert(*(productTempDao.all))
categoryDao.insert(*(categoryTempDao.all))
repositoryDao.put(repository)
}
productTempDao.emptyTable()
categoryTempDao.emptyTable()
}
}
}

View File

@ -33,6 +33,8 @@ class QueryBuilder {
this.arguments += arguments
}
fun build() = builder.toString()
fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor {
val query = builder.toString()
val arguments = arguments.toTypedArray()
@ -47,4 +49,4 @@ class QueryBuilder {
}
return db.rawQuery(query, arguments, signal)
}
}
}

View File

@ -4,11 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import com.looker.droidify.database.Database.jsonGenerate
import com.looker.droidify.database.Database.jsonParse
import com.looker.droidify.entity.Product
import com.looker.droidify.entity.ProductItem
import com.looker.droidify.entity.Repository
import com.looker.droidify.utility.jsonGenerate
import com.looker.droidify.utility.jsonParse
@Entity
class Repository {
@ -17,7 +16,7 @@ class Repository {
var id: Long = 0
var enabled = 0
var deleted = 0
var deleted = false
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data: Repository? = null
@ -26,54 +25,60 @@ class Repository {
@ColumnInfo(name = "_id")
var id = 0L
var deleted = 0
var deleted = false
}
}
@Entity(primaryKeys = ["repository_id", "package_name"])
class Product {
var repository_id: Long = 0
@Entity(tableName = "product", primaryKeys = ["repository_id", "package_name"])
open class Product {
var repository_id = 0L
var package_name = ""
var name = ""
var summary = ""
var description = ""
var added = 0
var updated = 0
var version_code = 0
var added = 0L
var updated = 0L
var version_code = 0L
var signatures = ""
var compatible = 0
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data: Product? = null
var data: com.looker.droidify.entity.Product? = null
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var data_item: ProductItem? = null
}
@Entity(primaryKeys = ["repository_id", "package_name", "name"])
class Category {
@Entity(tableName = "temporary_product")
class ProductTemp : Product()
@Entity(tableName = "category", primaryKeys = ["repository_id", "package_name", "name"])
open class Category {
var repository_id: Long = 0
var package_name = ""
var name = ""
}
@Entity
class Installed {
@Entity(tableName = "temporary_category")
class CategoryTemp : Category()
@Entity(tableName = "memory_installed")
class Installed(pName: String = "") {
@PrimaryKey
var package_name = ""
var package_name = pName
var version = ""
var version_code = 0
var version_code = 0L
var signature = ""
}
@Entity
@Entity(tableName = "memory_lock")
class Lock {
@PrimaryKey
var package_name = ""
var version_code = 0
var version_code = 0L
}
object Converters {
@ -87,11 +92,12 @@ object Converters {
@TypeConverter
@JvmStatic
fun toProduct(byteArray: ByteArray) = byteArray.jsonParse { Product.deserialize(it) }
fun toProduct(byteArray: ByteArray) =
byteArray.jsonParse { com.looker.droidify.entity.Product.deserialize(it) }
@TypeConverter
@JvmStatic
fun toByteArray(product: Product) = jsonGenerate(product::serialize)
fun toByteArray(product: com.looker.droidify.entity.Product) = jsonGenerate(product::serialize)
@TypeConverter
@JvmStatic