From c49b9f7841ca718400efe1bbfd1054a708b65ba6 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 27 Nov 2023 17:38:55 +0100 Subject: [PATCH] DBStore query support and tests --- .../platformplayer/ManagedDBStoreTests.kt | 120 ++++++++++++++++++ .../futo/platformplayer/states/StateApp.kt | 2 +- .../stores/db/ManagedDBStore.kt | 83 +++++++++--- 3 files changed, 183 insertions(+), 22 deletions(-) diff --git a/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt b/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt index ff606e8f..5f30353c 100644 --- a/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt +++ b/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt @@ -127,8 +127,123 @@ class ManagedDBStoreTests { store.shutdown(); } + @Test + fun getPage() { + val store = ManagedDBStore.create("test", Descriptor()) + .load(context, true); + store.deleteAll(); + + val testObjs = createSequence(store, 25); + + val page1 = store.getPage(0, 10); + val page2 = store.getPage(1, 10); + val page3 = store.getPage(2, 10); + Assert.assertEquals(10, page1.size); + Assert.assertEquals(10, page2.size); + Assert.assertEquals(5, page3.size); + + store.shutdown(); + } + @Test + fun query() { + val store = ManagedDBStore.create("test", Descriptor()) + .load(context, true); + store.deleteAll(); + + val testStr = UUID.randomUUID().toString(); + + val testObj1 = DBTOs.TestObject(); + val testObj2 = DBTOs.TestObject(); + val testObj3 = DBTOs.TestObject(); + val testObj4 = DBTOs.TestObject(); + testObj3.someStr = testStr; + testObj4.someStr = testStr; + val obj1 = createAndAssert(store, testObj1); + val obj2 = createAndAssert(store, testObj2); + val obj3 = createAndAssert(store, testObj3); + val obj4 = createAndAssert(store, testObj4); + + val results = store.query(DBTOs.TestIndex::someString, testStr); + + Assert.assertEquals(2, results.size); + for(result in results) { + if(result.someNum == obj3.someNum) + assertIndexEquals(obj3, result); + else + assertIndexEquals(obj4, result); + } + store.shutdown(); + } + @Test + fun queryPage() { + val index = ConcurrentHashMap(); + val store = ManagedDBStore.create("test", Descriptor()) + .withIndex({ it.someNum }, index) + .load(context, true); + store.deleteAll(); + + val testStr = UUID.randomUUID().toString(); + + val testResults = createSequence(store, 40, { i, testObject -> + if(i % 2 == 0) + testObject.someStr = testStr; + }); + val page1 = store.queryPage(DBTOs.TestIndex::someString, testStr, 0,10); + val page2 = store.queryPage(DBTOs.TestIndex::someString, testStr, 1,10); + val page3 = store.queryPage(DBTOs.TestIndex::someString, testStr, 2,10); + + Assert.assertEquals(10, page1.size); + Assert.assertEquals(10, page2.size); + Assert.assertEquals(0, page3.size); + + + store.shutdown(); + } + @Test + fun queryPager() { + val store = ManagedDBStore.create("test", Descriptor()) + .load(context, true); + store.deleteAll(); + + val testStr = UUID.randomUUID().toString(); + + val testResults = createSequence(store, 100, { i, testObject -> + if(i % 2 == 0) + testObject.someStr = testStr; + }); + val pager = store.queryPager(DBTOs.TestIndex::someString, testStr, 10); + + val items = pager.getResults().toMutableList(); + while(pager.hasMorePages()) { + pager.nextPage(); + items.addAll(pager.getResults()); + } + Assert.assertEquals(50, items.size); + for(i in 0 until 50) { + val k = i * 2; + Assert.assertEquals(k, items[i].someNum); + } + + store.shutdown(); + } + + + + + + + private fun createSequence(store: ManagedDBStore, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List { + val list = mutableListOf(); + for(i in 0 until count) { + val obj = DBTOs.TestObject(); + obj.someNum = i; + modifier?.invoke(i, obj); + list.add(createAndAssert(store, obj)); + } + return list; + } private fun createAndAssert(store: ManagedDBStore, obj: DBTOs.TestObject): DBTOs.TestIndex { val id = store.insert(obj); @@ -148,6 +263,11 @@ class ManagedDBStoreTests { Assert.assertEquals(obj1.someNum, obj2.someNum); assertObjectEquals(obj1.obj, obj2); } + private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestIndex) { + Assert.assertEquals(obj1.someString, obj2.someString); + Assert.assertEquals(obj1.someNum, obj2.someNum); + assertIndexEquals(obj1, obj2.obj); + } class Descriptor: ManagedDBDescriptor() { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 0b4f8d78..0fde6ff8 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -533,7 +533,7 @@ class StateApp { StatePlaylists.instance.migrateLegacyHistory(); - if(true) { + if(false) { /* Logger.i(TAG, "TEST:--------(200)---------"); testHistoryDB(200); diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt index 4eacb6a4..30076fd3 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt @@ -15,6 +15,7 @@ import java.lang.reflect.Field import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import kotlin.reflect.KClass +import kotlin.reflect.KProperty import kotlin.reflect.KType import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.hasAnnotation @@ -30,7 +31,7 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA private var _dbDaoBase: ManagedDBDAOBase? = null; val dbDaoBase: ManagedDBDAOBase get() = _dbDaoBase ?: throw IllegalStateException("Not initialized db [${name}]"); - private var _dbDescriptor: ManagedDBDescriptor; + val descriptor: ManagedDBDescriptor; private val _columnInfo: List; @@ -52,9 +53,10 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA private val _indexCollection = ConcurrentHashMap(); private var _withUnique: Pair<(I)->Any, ConcurrentMap>? = null; + private val _orderSQL: String?; constructor(name: String, descriptor: ManagedDBDescriptor, clazz: KType, serializer: StoreSerializer, niceName: String? = null) { - _dbDescriptor = descriptor; + this.descriptor = descriptor; _name = name; this.name = niceName ?: name.let { if(it.isNotEmpty()) @@ -63,29 +65,29 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA }; _serializer = serializer; _class = clazz; - _columnInfo = _dbDescriptor.indexClass().memberProperties + _columnInfo = this.descriptor.indexClass().memberProperties .filter { it.hasAnnotation() && it.name != "serialized" } .map { ColumnMetadata(it.javaField!!, it.findAnnotation()!!, it.findAnnotation()) }; val indexColumnNames = _columnInfo.map { it.name }; val orderedColumns = _columnInfo.filter { it.ordered != null }.sortedBy { it.ordered!!.priority }; - val orderSQL = if(orderedColumns.size > 0) + _orderSQL = if(orderedColumns.size > 0) " ORDER BY " + orderedColumns.map { "${it.name} ${if(it.ordered!!.descending) "DESC" else "ASC"}" }.joinToString(", "); else ""; - _sqlGet = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id = ?", arrayOf(it)) }; - _sqlGetIndex = { SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${_dbDescriptor.table_name} WHERE id = ?", arrayOf(it)) }; - _sqlGetAll = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id IN (?)", arrayOf(it)) }; - _sqlAll = SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} ${orderSQL}"); - _sqlCount = SimpleSQLiteQuery("SELECT COUNT(id) FROM ${_dbDescriptor.table_name}"); - _sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${_dbDescriptor.table_name}"); - _sqlDeleteById = { id -> SimpleSQLiteQuery("DELETE FROM ${_dbDescriptor.table_name} WHERE id = :id", arrayOf(id)) }; - _sqlIndexed = SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${_dbDescriptor.table_name}"); + _sqlGet = { SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} WHERE id = ?", arrayOf(it)) }; + _sqlGetIndex = { SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${this.descriptor.table_name} WHERE id = ?", arrayOf(it)) }; + _sqlGetAll = { SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} WHERE id IN (?)", arrayOf(it)) }; + _sqlAll = SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} ${_orderSQL}"); + _sqlCount = SimpleSQLiteQuery("SELECT COUNT(id) FROM ${this.descriptor.table_name}"); + _sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${this.descriptor.table_name}"); + _sqlDeleteById = { id -> SimpleSQLiteQuery("DELETE FROM ${this.descriptor.table_name} WHERE id = :id", arrayOf(id)) }; + _sqlIndexed = SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${this.descriptor.table_name}"); if(orderedColumns.size > 0) { _sqlPage = { page, length -> - SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} ${orderSQL} LIMIT ? OFFSET ?", arrayOf(length, page * length)); + SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} ${_orderSQL} LIMIT ? OFFSET ?", arrayOf(length, page * length)); } } } @@ -110,9 +112,9 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA fun load(context: Context? = null, inMemory: Boolean = false): ManagedDBStore { _db = (if(!inMemory) - Room.databaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java, _name) + Room.databaseBuilder(context ?: StateApp.instance.context, descriptor.dbClass().java, _name) else - Room.inMemoryDatabaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java)) + Room.inMemoryDatabaseBuilder(context ?: StateApp.instance.context, descriptor.dbClass().java)) .fallbackToDestructiveMigration() .allowMainThreadQueries() .build() @@ -150,7 +152,7 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA } fun insert(obj: T): Long { - val newIndex = _dbDescriptor.create(obj); + val newIndex = descriptor.create(obj); if(_withUnique != null) { val unique = getUnique(newIndex); @@ -174,7 +176,7 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA fun update(id: Long, obj: T) { val existing = if(_indexes.any { it.checkChange }) _dbDaoBase!!.getNullable(_sqlGetIndex(id)) else null - val newIndex = _dbDescriptor.create(obj); + val newIndex = descriptor.create(obj); newIndex.id = id; newIndex.serialized = serialize(obj); dbDaoBase.update(newIndex); @@ -223,23 +225,54 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA return deserializeIndexes(dbDaoBase.getMultiple(_sqlGetAll(id))); } - fun getObjectPage(page: Int, length: Int): List = convertObjects(getPage(page, length)); - fun getObjectPager(pageLength: Int = 20): IPager { + fun query(field: String, obj: Any): List { + val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ?"; + val query = SimpleSQLiteQuery(queryStr, arrayOf(obj)); + return deserializeIndexes(dbDaoBase.getMultiple(query)); + } + fun query(field: KProperty<*>, obj: Any): List = query(validateFieldName(field), obj); + fun queryPage(field: String, obj: Any, page: Int, pageSize: Int): List { + val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ? ${_orderSQL} LIMIT ? OFFSET ?"; + val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize)); + return deserializeIndexes(dbDaoBase.getMultiple(query)); + } + fun queryPage(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List = queryPage(validateFieldName(field), obj, page, pageSize); + + fun queryPageObjects(field: String, obj: Any, page: Int, pageSize: Int): List = convertObjects(queryPage(field, obj, page, pageSize)); + fun queryPageObjects(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List = queryPageObjects(validateFieldName(field), obj, page, pageSize); + + fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager = queryPager(validateFieldName(field), obj, pageSize); + fun queryPager(field: String, obj: Any, pageSize: Int): IPager { return AdhocPager({ - getObjectPage(it - 1, pageLength); + queryPage(field, obj, it - 1, pageSize); }); } + + fun queryObjectPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager = queryObjectPager(validateFieldName(field), obj, pageSize); + fun queryObjectPager(field: String, obj: Any, pageSize: Int): IPager { + return AdhocPager({ + queryPageObjects(field, obj, it - 1, pageSize); + }); + } + fun getPage(page: Int, length: Int): List { if(_sqlPage == null) throw IllegalStateException("DB Store [${name}] does not have ordered fields to provide pages"); val query = _sqlPage!!(page, length) ?: throw IllegalStateException("Paged db not setup for ${_name}"); - return dbDaoBase.getMultiple(query); + return deserializeIndexes(dbDaoBase.getMultiple(query)); } + fun getPageObjects(page: Int, length: Int): List = convertObjects(getPage(page, length)); + fun getPager(pageLength: Int = 20): IPager { return AdhocPager({ getPage(it - 1, pageLength); }); } + fun getObjectPager(pageLength: Int = 20): IPager { + return AdhocPager({ + getPageObjects(it - 1, pageLength); + }); + } fun delete(item: I) { dbDaoBase.delete(item); @@ -284,6 +317,14 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA return _serializer.serialize(_class, obj); } + + private fun validateFieldName(prop: KProperty<*>): String { + val declaringClass = prop.javaField?.declaringClass; + if(declaringClass != descriptor.indexClass().java) + throw IllegalStateException("Cannot query by property [${prop.name}] from ${declaringClass?.simpleName} not part of ${descriptor.indexClass().simpleName}"); + return prop.name; + } + companion object { inline fun , D: ManagedDBDatabase, DA: ManagedDBDAOBase> create(name: String, descriptor: ManagedDBDescriptor, serializer: KSerializer? = null) = ManagedDBStore(name, descriptor, kotlin.reflect.typeOf(), JsonStoreSerializer.create(serializer));