From ce4e7c07fb03096a7d5d36118d9887578a774c52 Mon Sep 17 00:00:00 2001 From: Christopher Jenkins <chris.mark.jenkins@gmail.com> Date: Tue, 14 Jan 2025 14:15:41 -0800 Subject: [PATCH] added delete by keys (#7) added delete by read/write methods --- .../com/mercury/sqkon/db/EntityQueries.kt | 24 ++++++--- .../com/mercury/sqkon/db/KeyValueStorage.kt | 50 +++++++++++++++++-- .../com/mercury/sqkon/db/metadata.sq | 3 +- .../sqkon/db/KeyValueStorageStaleTest.kt | 8 +-- .../mercury/sqkon/db/KeyValueStorageTest.kt | 14 ++++++ 5 files changed, 82 insertions(+), 17 deletions(-) diff --git a/library/src/commonMain/kotlin/com/mercury/sqkon/db/EntityQueries.kt b/library/src/commonMain/kotlin/com/mercury/sqkon/db/EntityQueries.kt index ad48e25..5f68ddc 100644 --- a/library/src/commonMain/kotlin/com/mercury/sqkon/db/EntityQueries.kt +++ b/library/src/commonMain/kotlin/com/mercury/sqkon/db/EntityQueries.kt @@ -197,7 +197,7 @@ class EntityQueries( suspend fun delete( entityName: String, - entityKey: String? = null, + entityKeys: Collection<String>? = null, where: Where<*>? = null, ) { val queries = buildList { @@ -206,19 +206,29 @@ class EntityQueries( parameters = 1, bindArgs = { bindString(entityName) } )) - if (entityKey != null) add(SqlQuery( - where = "entity_key = ?", - parameters = 1, - bindArgs = { bindString(entityKey) } - )) + when(entityKeys?.size) { + null, 0 -> {} + 1 -> add(SqlQuery( + where = "entity_key = ?", + parameters = 1, + bindArgs = { bindString(entityKeys.first()) } + )) + else -> add(SqlQuery( + where = "entity_key IN (${entityKeys.joinToString(",") { "?" }})", + parameters = entityKeys.size, + bindArgs = { entityKeys.forEach { bindString(it) } } + )) + } + addAll(listOfNotNull(where?.toSqlQuery(increment = 1))) } val identifier = identifier("delete", queries.identifier().toString()) val whereSubQuerySql = if (queries.size <= 1) "" else """ - AND entity_key = (SELECT entity_key FROM entity${queries.buildFrom()} ${queries.buildWhere()}) + AND entity_key IN (SELECT entity_key FROM entity${queries.buildFrom()} ${queries.buildWhere()}) """.trimIndent() val sql = "DELETE FROM entity WHERE entity_name = ? $whereSubQuerySql" + println("SQL: $sql") try { driver.execute( identifier = identifier, diff --git a/library/src/commonMain/kotlin/com/mercury/sqkon/db/KeyValueStorage.kt b/library/src/commonMain/kotlin/com/mercury/sqkon/db/KeyValueStorage.kt index cb10291..b4d244d 100644 --- a/library/src/commonMain/kotlin/com/mercury/sqkon/db/KeyValueStorage.kt +++ b/library/src/commonMain/kotlin/com/mercury/sqkon/db/KeyValueStorage.kt @@ -197,12 +197,14 @@ open class KeyValueStorage<T : Any>( fun selectByKeys( keys: Collection<String>, orderBy: List<OrderBy<T>> = emptyList(), + expiresAfter: Instant? = null, ): Flow<List<T>> { return entityQueries .select( entityName = entityName, entityKeys = keys, orderBy = orderBy, + expiresAt = expiresAfter, ) .asFlow() .mapToList(config.dispatcher) @@ -344,8 +346,22 @@ open class KeyValueStorage<T : Any>( * @see delete * @see deleteAll */ - suspend fun deleteByKey(key: String) = transaction { - entityQueries.delete(entityName, entityKey = key) + suspend fun deleteByKey(key: String) { + deleteByKeys(key) + } + + /** + * Delete by keys. + * + * If you need to delete all rows, use [deleteAll]. + * If you need to specify which rows to delete, use [delete] with a [Where]. Note, using where + * will be less performant than deleting by key. + * + * @see delete + * @see deleteAll + */ + suspend fun deleteByKeys(vararg key: String) = transaction { + entityQueries.delete(entityName, entityKeys = key.toSet()) updateWriteAt(currentCoroutineContext()[RequestHash.Key]?.hash ?: key.hashCode()) } @@ -388,9 +404,33 @@ open class KeyValueStorage<T : Any>( * * @see deleteExpired */ - suspend fun deleteStale(instant: Instant = Clock.System.now()) = transaction { - metadataQueries.purgeStale(entityName, instant.toEpochMilliseconds()) - updateWriteAt(currentCoroutineContext()[RequestHash.Key]?.hash ?: instant.hashCode()) + suspend fun deleteStale( + writeInstant: Instant = Clock.System.now(), + readInstant: Instant = Clock.System.now() + ) = transaction { + metadataQueries.purgeStale( + entity_name = entityName, + writeInstant = writeInstant.toEpochMilliseconds(), + readInstant = readInstant.toEpochMilliseconds() + ) + updateWriteAt( + currentCoroutineContext()[RequestHash.Key]?.hash + ?: (writeInstant.hashCode() + readInstant.hashCode()) + ) + } + + /** + * Unlike [deleteExpired], this will clean up rows that have not been touched (read/written) + * before the passed in time. + * + * For example, you want to clean up rows that have not been read or written to in the last 24 + * hours. You would call this function with `Clock.System.now().minus(1.days)`. This is not the same as + * [deleteExpired] which is based on the `expires_at` field. + * + * @see deleteExpired + */ + suspend fun deleteState(instant: Instant = Clock.System.now()) { + deleteStale(instant, instant) } fun count( diff --git a/library/src/commonMain/sqldelight/com/mercury/sqkon/db/metadata.sq b/library/src/commonMain/sqldelight/com/mercury/sqkon/db/metadata.sq index 327fa9a..b67b769 100644 --- a/library/src/commonMain/sqldelight/com/mercury/sqkon/db/metadata.sq +++ b/library/src/commonMain/sqldelight/com/mercury/sqkon/db/metadata.sq @@ -36,4 +36,5 @@ DELETE FROM entity purgeStale: DELETE FROM entity WHERE entity_name = :entity_name - AND write_at < :instant AND (read_at IS NULL OR read_at < :instant); + AND write_at < :writeInstant + AND (read_at IS NULL OR read_at < :readInstant); diff --git a/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageStaleTest.kt b/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageStaleTest.kt index 774d52e..48ebc04 100644 --- a/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageStaleTest.kt +++ b/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageStaleTest.kt @@ -34,7 +34,7 @@ class KeyValueStorageStaleTest { .toSortedMap() testObjectStorage.insertAll(expected) // Clean up older than now - testObjectStorage.deleteStale(instant = now) + testObjectStorage.deleteStale(writeInstant = now, readInstant = now) val actualAfterDelete = testObjectStorage.selectAll().first() assertEquals(expected.size, actualAfterDelete.size) } @@ -48,7 +48,7 @@ class KeyValueStorageStaleTest { sleep(1) val now = Clock.System.now() // Clean up older than now - testObjectStorage.deleteStale(instant = now) + testObjectStorage.deleteStale(writeInstant = now, readInstant = now) val actualAfterDelete = testObjectStorage.selectAll().first() assertEquals(0, actualAfterDelete.size) } @@ -65,7 +65,7 @@ class KeyValueStorageStaleTest { // write again so read is in the past testObjectStorage.updateAll(expected) // Read in the past write is after now - testObjectStorage.deleteStale(instant = now) + testObjectStorage.deleteStale(writeInstant = now, readInstant = now) val actualAfterDelete = testObjectStorage.selectAll().first() assertEquals(expected.size, actualAfterDelete.size) } @@ -81,7 +81,7 @@ class KeyValueStorageStaleTest { sleep(10) val now = Clock.System.now() // Clean write and read are in the past - testObjectStorage.deleteStale(instant = now) + testObjectStorage.deleteStale(writeInstant = now, readInstant = now) val actualAfterDelete = testObjectStorage.selectResult().first() assertEquals(0, actualAfterDelete.size) } diff --git a/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageTest.kt b/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageTest.kt index 3dc393b..5130386 100644 --- a/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageTest.kt +++ b/library/src/commonTest/kotlin/com/mercury/sqkon/db/KeyValueStorageTest.kt @@ -316,6 +316,20 @@ class KeyValueStorageTest { assertEquals(expected.size - 1, actualAfterDelete.size) } + @Test + fun delete_byKeys() = runTest { + val expected = (0..10).map { TestObject() }.associateBy { it.id } + testObjectStorage.insertAll(expected) + val actual = testObjectStorage.selectAll().first() + assertEquals(expected.size, actual.size) + + val key1 = expected.keys.toList()[5] + val key2 = expected.keys.toList()[6] + testObjectStorage.deleteByKeys(key1, key2) + val actualAfterDelete = testObjectStorage.selectAll().first() + assertEquals(expected.size - 2, actualAfterDelete.size) + } + @Test fun delete_byEntityId() = runTest { val expected = (0..10).map { TestObject() }.associateBy { it.id }