Unittests and fixes for dbstore

This commit is contained in:
Kelvin 2023-11-24 22:42:30 +01:00
parent f3c9e0196e
commit 662e94bcee
8 changed files with 273 additions and 24 deletions

View file

@ -0,0 +1,159 @@
package com.futo.platformplayer
import androidx.test.platform.app.InstrumentationRegistry
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.testing.DBTOs
import org.junit.Assert
import org.junit.Test
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
class ManagedDBStoreTests {
val context = InstrumentationRegistry.getInstrumentation().targetContext;
@Test
fun startup() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
store.shutdown();
}
@Test
fun insert() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
createAndAssert(store, testObj);
store.shutdown();
}
@Test
fun update() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
testObj.someStr = "Testing";
store.update(obj.id!!, testObj);
val obj2 = store.get(obj.id!!);
assertIndexEquals(obj2, testObj);
store.shutdown();
}
@Test
fun delete() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
store.delete(obj.id!!);
Assert.assertEquals(store.count(), 0);
Assert.assertNull(store.getOrNull(obj.id!!));
store.shutdown();
}
@Test
fun withIndex() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
val obj3 = createAndAssert(store, testObj3);
Assert.assertEquals(store.count(), 3);
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
Assert.assertEquals(index.size, 3);
val oldStr = testObj1.someStr;
testObj1.someStr = UUID.randomUUID().toString();
store.update(obj1.id!!, testObj1);
Assert.assertEquals(index.size, 3);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.delete(obj2.id!!);
Assert.assertEquals(index.size, 2);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertFalse(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.shutdown();
}
@Test
fun withUnique() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, false, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
testObj3.someStr = testObj2.someStr;
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
Assert.assertEquals(store.count(), 2);
store.shutdown();
}
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
val id = store.insert(obj);
Assert.assertTrue(id > 0);
val dbObj = store.get(id);
assertIndexEquals(dbObj, obj);
return dbObj;
}
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someStr, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
}
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someString, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
assertObjectEquals(obj1.obj, obj2);
}
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
override val table_name: String = "testing";
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
}
}

View file

@ -58,7 +58,7 @@ class StatePlaylists {
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex, true)
.withIndex({ it.url }, historyIndex, false, true)
.load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");

View file

@ -15,6 +15,8 @@ interface ManagedDBDAOBase<T, I: ManagedDBIndex<T>> {
@RawQuery
fun get(query: SupportSQLiteQuery): I;
@RawQuery
fun getNullable(query: SupportSQLiteQuery): I?;
@RawQuery
fun getMultiple(query: SupportSQLiteQuery): List<I>;
@RawQuery

View file

@ -13,5 +13,12 @@ open class ManagedDBIndex<T> {
var serialized: ByteArray? = null;
@Ignore
var obj: T? = null;
private var _obj: T? = null;
@get:Ignore
val obj: T get() = _obj ?: throw IllegalStateException("Attempted to access serialized object on a index-only instance");
fun setInstance(obj: T) {
this._obj = obj;
}
}

View file

@ -1,15 +1,13 @@
package com.futo.platformplayer.stores.db
import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.assume
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer
@ -37,6 +35,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
private val _columnInfo: List<ColumnMetadata>;
private val _sqlGet: (Long)-> SimpleSQLiteQuery;
private val _sqlGetIndex: (Long)-> SimpleSQLiteQuery;
private val _sqlGetAll: (LongArray)-> SimpleSQLiteQuery;
private val _sqlAll: SimpleSQLiteQuery;
private val _sqlCount: SimpleSQLiteQuery;
@ -49,7 +48,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
val name: String;
private val _indexes: ArrayList<Pair<(I)->Any, ConcurrentMap<Any, I>>> = arrayListOf();
private val _indexes: ArrayList<IndexDescriptor<I>> = arrayListOf();
private val _indexCollection = ConcurrentHashMap<Long, I>();
private var _withUnique: Pair<(I)->Any, ConcurrentMap<Any, I>>? = null;
@ -76,6 +75,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
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}");
@ -90,10 +90,10 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
}
}
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, allowChange: Boolean = false, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
_indexes.add(Pair(keySelector, indexContainer));
_indexes.add(IndexDescriptor(keySelector, indexContainer, allowChange));
if(withUnique)
withUnique(keySelector, indexContainer);
@ -108,8 +108,11 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
return this;
}
fun load(): ManagedDBStore<I, T, D, DA> {
_db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass().java, _name)
fun load(context: Context? = null, inMemory: Boolean = false): ManagedDBStore<I, T, D, DA> {
_db = (if(!inMemory)
Room.databaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java, _name)
else
Room.inMemoryDatabaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java))
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
@ -117,11 +120,17 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
if(_indexes.any()) {
val allItems = _dbDaoBase!!.getMultiple(_sqlIndexed!!);
for(index in _indexes)
index.second.putAll(allItems.associateBy(index.first));
index.collection.putAll(allItems.associateBy(index.keySelector));
}
return this;
}
fun shutdown() {
val db = _db;
_db = null;
_dbDaoBase = null;
db?.close();
}
fun getUnique(obj: I): I? {
if(_withUnique == null)
@ -142,9 +151,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun insert(obj: T): Long {
val newIndex = _dbDescriptor.create(obj);
val unique = getUnique(newIndex);
if(unique != null)
return unique.id!!;
if(_withUnique != null) {
val unique = getUnique(newIndex);
if (unique != null)
return unique.id!!;
}
newIndex.serialized = serialize(obj);
newIndex.id = dbDaoBase.insert(newIndex);
@ -153,13 +165,15 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.first(newIndex);
index.second.put(key, newIndex);
val key = index.keySelector(newIndex);
index.collection.put(key, newIndex);
}
}
return newIndex.id!!;
}
fun update(id: Long, obj: T) {
val existing = if(_indexes.any { it.checkChange }) _dbDaoBase!!.getNullable(_sqlGetIndex(id)) else null
val newIndex = _dbDescriptor.create(obj);
newIndex.id = id;
newIndex.serialized = serialize(obj);
@ -168,8 +182,13 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.first(newIndex);
index.second.put(key, newIndex);
val key = index.keySelector(newIndex);
if(index.checkChange && existing != null) {
val keyExisting = index.keySelector(existing);
if(keyExisting != key)
index.collection.remove(keyExisting);
}
index.collection.put(key, newIndex);
}
}
}
@ -189,6 +208,15 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun get(id: Long): I {
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
}
fun getOrNull(id: Long): I? {
val result = dbDaoBase.getNullable(_sqlGet(id));
if(result == null)
return null;
return deserializeIndex(result);
}
fun getIndexOnlyOrNull(id: Long): I? {
return dbDaoBase.get(_sqlGetIndex(id));
}
fun getAllObjects(vararg id: Long): List<T> = getAll(*id).map { it.obj!! };
fun getAll(vararg id: Long): List<I> {
@ -217,19 +245,20 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
dbDaoBase.delete(item);
for(index in _indexes)
index.second.remove(index.first(item));
index.collection.remove(index.keySelector(item));
}
fun delete(id: Long) {
dbDaoBase.action(_sqlDeleteById(id));
for(index in _indexes)
index.second.values.removeIf { it.id == id }
index.collection.values.removeIf { it.id == id }
}
fun deleteAll() {
dbDaoBase.action(_sqlDeleteAll);
_indexCollection.clear();
for(index in _indexes)
index.second.clear();
index.collection.clear();
}
fun convertObject(index: I): T? {
@ -241,7 +270,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun deserializeIndex(index: I): I {
if(index.serialized == null) throw IllegalStateException("Cannot deserialize index-only items from [${name}]");
val obj = _serializer.deserialize(_class, index.serialized!!);
index.obj = obj;
index.setInstance(obj);
index.serialized = null;
return index;
}
@ -260,6 +289,13 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
= ManagedDBStore(name, descriptor, kotlin.reflect.typeOf<T>(), JsonStoreSerializer.create(serializer));
}
//Pair<(I)->Any, ConcurrentMap<Any, I>>
class IndexDescriptor<I>(
val keySelector: (I) -> Any,
val collection: ConcurrentMap<Any, I>,
val checkChange: Boolean
)
class ColumnMetadata(
val field: Field,
val info: ColumnIndex,

View file

@ -24,7 +24,6 @@ class DBChannelCache {
constructor(sCache: SerializedPlatformContent) {
id = null;
serialized = null;
obj = sCache;
channelUrl = sCache.author.url;
}
}

View file

@ -66,7 +66,6 @@ class DBHistory {
url = historyVideo.video.url;
position = historyVideo.position;
date = historyVideo.date.toEpochSecond();
obj = historyVideo;
}
}
}

View file

@ -0,0 +1,47 @@
package com.futo.platformplayer.testing
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBIndex
import kotlinx.serialization.Serializable
import java.util.Random
import java.util.UUID
class DBTOs {
@Dao
interface DBDAO: ManagedDBDAOBase<TestObject, TestIndex> {}
@Database(entities = [TestIndex::class], version = 2)
abstract class DB: ManagedDBDatabase<TestObject, TestIndex, DBDAO>() {
abstract override fun base(): DBDAO;
}
@Entity("testing")
class TestIndex(): ManagedDBIndex<TestObject>() {
@ColumnIndex
var someString: String = "";
@ColumnIndex
@ColumnOrdered(0)
var someNum: Int = 0;
constructor(obj: TestObject, customInt: Int? = null) : this() {
someString = obj.someStr;
someNum = customInt ?: obj.someNum;
}
}
@Serializable
class TestObject {
var someStr = UUID.randomUUID().toString();
var someNum = random.nextInt();
}
companion object {
val random = Random();
}
}