diff --git a/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt b/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt new file mode 100644 index 00000000..ff606e8f --- /dev/null +++ b/app/src/androidTest/java/com/futo/platformplayer/ManagedDBStoreTests.kt @@ -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(); + 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(); + 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, 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() { + override val table_name: String = "testing"; + override fun indexClass(): KClass = DBTOs.TestIndex::class; + override fun dbClass(): KClass = DBTOs.DB::class; + override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index 1dea8560..b75e8240 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -58,7 +58,7 @@ class StatePlaylists { val historyIndex: ConcurrentMap = 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"); diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBDAOBase.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBDAOBase.kt index a679bb16..05c19390 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBDAOBase.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBDAOBase.kt @@ -15,6 +15,8 @@ interface ManagedDBDAOBase> { @RawQuery fun get(query: SupportSQLiteQuery): I; @RawQuery + fun getNullable(query: SupportSQLiteQuery): I?; + @RawQuery fun getMultiple(query: SupportSQLiteQuery): List; @RawQuery diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBIndex.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBIndex.kt index b111ff59..93562aca 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBIndex.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBIndex.kt @@ -13,5 +13,12 @@ open class ManagedDBIndex { 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; + } } \ No newline at end of file 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 a5677b9e..4eacb6a4 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 @@ -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, T, D: ManagedDBDatabase, DA private val _columnInfo: List; 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, T, D: ManagedDBDatabase, DA val name: String; - private val _indexes: ArrayListAny, ConcurrentMap>> = arrayListOf(); + private val _indexes: ArrayList> = arrayListOf(); private val _indexCollection = ConcurrentHashMap(); private var _withUnique: Pair<(I)->Any, ConcurrentMap>? = null; @@ -76,6 +75,7 @@ class ManagedDBStore, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, DA } } - fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap, withUnique: Boolean = false): ManagedDBStore { + fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap, allowChange: Boolean = false, withUnique: Boolean = false): ManagedDBStore { 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, T, D: ManagedDBDatabase, DA return this; } - fun load(): ManagedDBStore { - _db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass().java, _name) + fun load(context: Context? = null, inMemory: Boolean = false): ManagedDBStore { + _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, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, 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 = getAll(*id).map { it.obj!! }; fun getAll(vararg id: Long): List { @@ -217,19 +245,20 @@ class ManagedDBStore, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, 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, T, D: ManagedDBDatabase, DA = ManagedDBStore(name, descriptor, kotlin.reflect.typeOf(), JsonStoreSerializer.create(serializer)); } + //Pair<(I)->Any, ConcurrentMap> + class IndexDescriptor( + val keySelector: (I) -> Any, + val collection: ConcurrentMap, + val checkChange: Boolean + ) + class ColumnMetadata( val field: Field, val info: ColumnIndex, diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt index 77256688..8007f393 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt @@ -24,7 +24,6 @@ class DBChannelCache { constructor(sCache: SerializedPlatformContent) { id = null; serialized = null; - obj = sCache; channelUrl = sCache.author.url; } } diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBHistory.kt b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBHistory.kt index 89cd8fbc..efc91eac 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBHistory.kt @@ -66,7 +66,6 @@ class DBHistory { url = historyVideo.video.url; position = historyVideo.position; date = historyVideo.date.toEpochSecond(); - obj = historyVideo; } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt b/app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt new file mode 100644 index 00000000..0af725db --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt @@ -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 {} + @Database(entities = [TestIndex::class], version = 2) + abstract class DB: ManagedDBDatabase() { + abstract override fun base(): DBDAO; + } + + + @Entity("testing") + class TestIndex(): ManagedDBIndex() { + + @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(); + } +} \ No newline at end of file