com.lightningkite.lightningdb.InMemoryFieldCollection.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of server-core Show documentation
Show all versions of server-core Show documentation
A set of tools to fill in/replace what Ktor is lacking in.
The newest version!
package com.lightningkite.lightningdb
import com.lightningkite.lightningserver.exceptions.BadRequestException
import io.ktor.util.reflect.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.KSerializer
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* A FieldCollection who's underlying implementation is actually manipulating a MutableList.
* This is useful for times that an actual database is not needed, and you need to move fast, such as during Unit Tests.
*/
open class InMemoryFieldCollection(
val data: MutableList = ArrayList(),
val serializer: KSerializer
) :
AbstractSignalFieldCollection() {
private val lock = ReentrantLock()
private val uniqueIndexChecks = ConcurrentLinkedQueue<(List>) -> Unit>()
private fun uniqueCheck(changed: EntryChange) = uniqueCheck(listOf(changed))
private fun uniqueCheck(changes: List>) = uniqueIndexChecks.forEach { it(changes) }
init {
serializer.descriptor.indexes().forEach { index: NeededIndex ->
if (index.unique) {
val fields =
serializer.attemptGrabFields().filterKeys { index.fields.contains(it) }.values
uniqueIndexChecks.add { changes: List> ->
val fieldChanges = changes.mapNotNull { entryChange ->
if (
(entryChange.old == null && entryChange.new != null) ||
(entryChange.old != null &&
entryChange.new != null &&
fields.any { it.get(entryChange.old!!) != it.get(entryChange.new!!) })
)
fields.map { it to it.get(entryChange.new!!) }
else
null
}
fieldChanges.forEach { fieldValues ->
if (data.any { fromDb -> fieldValues.all { (property, value) -> property.get(fromDb) == value } }) {
throw BadRequestException("Unique Index Violation. The following fields are already in the database: ${fieldValues.joinToString { (property, value) -> "${property.name}: $value" }}")
}
}
}
}
}
}
override suspend fun find(
condition: Condition,
orderBy: List>,
skip: Int,
limit: Int,
maxQueryMs: Long,
): Flow = flow {
val result = lock.withLock {
data.asSequence()
.filter { condition(it) }
.let {
orderBy.comparator?.let { c ->
it.sortedWith(c)
} ?: it
}
.drop(skip)
.take(limit)
.toList()
}
result
.forEach {
emit(it)
}
}
override suspend fun count(condition: Condition): Int = data.count { condition(it) }
@Suppress("UNCHECKED_CAST")
override suspend fun groupCount(
condition: Condition,
groupBy: DataClassPath,
): Map = data.filter { condition(it) }.groupingBy { groupBy.get(it) }.eachCount().minus(null) as Map
override suspend fun aggregate(
aggregate: Aggregate,
condition: Condition,
property: DataClassPath,
): Double? =
data.asSequence().filter { condition(it) }.mapNotNull { property.get(it)?.toDouble() }.aggregate(aggregate)
override suspend fun groupAggregate(
aggregate: Aggregate,
condition: Condition,
groupBy: DataClassPath,
property: DataClassPath,
): Map = data.asSequence().filter { condition(it) }
.mapNotNull { (groupBy.get(it) ?: return@mapNotNull null) to (property.get(it)?.toDouble() ?: return@mapNotNull null) }.aggregate(aggregate)
override suspend fun insertImpl(models: Iterable): List = lock.withLock {
uniqueCheck(models.map { EntryChange(null, it) })
data.addAll(models)
return models.toList()
}
override suspend fun replaceOneImpl(
condition: Condition,
model: Model,
orderBy: List>,
): EntryChange = lock.withLock {
for (it in sortIndices(orderBy)) {
val old = data[it]
if (condition(old)) {
val changed = EntryChange(old, model)
uniqueCheck(changed)
data[it] = model
return changed
}
}
return EntryChange(null, null)
}
private fun sortIndices(orderBy: List>): Iterable {
return data.indices.let {
orderBy.comparator?.let { c ->
it.sortedWith { a, b -> c.compare(data[a], data[b]) }
} ?: it
}
}
override suspend fun upsertOneImpl(
condition: Condition,
modification: Modification,
model: Model,
): EntryChange = lock.withLock {
for (it in data.indices) {
val old = data[it]
if (condition(old)) {
val new = modification(old)
val changed = EntryChange(old, new)
uniqueCheck(changed)
data[it] = new
return changed
}
}
data.add(model)
return EntryChange(null, model)
}
override suspend fun updateOneImpl(
condition: Condition,
modification: Modification,
orderBy: List>,
): EntryChange = lock.withLock {
for (it in sortIndices(orderBy)) {
val old = data[it]
if (condition(old)) {
val new = modification(old)
val changed = EntryChange(old, new)
uniqueCheck(changed)
data[it] = new
return changed
}
}
return EntryChange(null, null)
}
override suspend fun updateManyImpl(
condition: Condition,
modification: Modification,
): CollectionChanges = lock.withLock {
return data.indices
.mapNotNull {
val old = data[it]
if (condition(old)) {
val new = modification(old)
it to EntryChange(old, new)
} else null
}
.let {
val changes = it.map { it.second }
uniqueCheck(changes)
it.forEach { (index, change) -> data[index] = change.new!! }
CollectionChanges(changes = changes)
}
}
override suspend fun deleteOneImpl(condition: Condition, orderBy: List>): Model? =
lock.withLock {
for (it in sortIndices(orderBy)) {
val old = data[it]
if (condition(old)) {
data.removeAt(it)
return old
}
}
return null
}
override suspend fun deleteManyImpl(condition: Condition): List = lock.withLock {
val removed = ArrayList()
data.removeAll {
if (condition(it)) {
removed.add(it)
true
} else {
false
}
}
return removed
}
override suspend fun replaceOneIgnoringResultImpl(
condition: Condition,
model: Model,
orderBy: List>,
): Boolean = replaceOne(
condition,
model,
orderBy
).new != null
override suspend fun upsertOneIgnoringResultImpl(
condition: Condition,
modification: Modification,
model: Model,
): Boolean = upsertOne(condition, modification, model).old != null
override suspend fun updateOneIgnoringResultImpl(
condition: Condition,
modification: Modification,
orderBy: List>,
): Boolean = updateOne(condition, modification, orderBy).new != null
override suspend fun updateManyIgnoringResultImpl(
condition: Condition,
modification: Modification,
): Int = updateMany(condition, modification).changes.size
override suspend fun deleteOneIgnoringOldImpl(
condition: Condition,
orderBy: List>,
): Boolean = deleteOne(condition, orderBy) != null
override suspend fun deleteManyIgnoringOldImpl(condition: Condition): Int = deleteMany(condition).size
fun drop() {
data.clear()
}
}