Skip to content

Commit

Permalink
added delete by keys (#7)
Browse files Browse the repository at this point in the history
added delete by read/write methods
  • Loading branch information
chrisjenx authored Jan 14, 2025
1 parent 880ebe6 commit ce4e7c0
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class EntityQueries(

suspend fun delete(
entityName: String,
entityKey: String? = null,
entityKeys: Collection<String>? = null,
where: Where<*>? = null,
) {
val queries = buildList {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down

0 comments on commit ce4e7c0

Please sign in to comment.