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

View File

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

View File

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

View File

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

View File

@ -66,7 +66,6 @@ class DBHistory {
url = historyVideo.video.url; url = historyVideo.video.url;
position = historyVideo.position; position = historyVideo.position;
date = historyVideo.date.toEpochSecond(); 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();
}
}