commonMain.expr.UpdateExpression.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dsl Show documentation
Show all versions of dsl Show documentation
Kotlin-first DSL for writing expressive and typesafe MongoDB queries
The newest version!
/*
* Copyright (c) 2024, OpenSavvy and contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package opensavvy.ktmongo.dsl.expr
import opensavvy.ktmongo.bson.BsonContext
import opensavvy.ktmongo.bson.BsonFieldWriter
import opensavvy.ktmongo.dsl.DangerousMongoApi
import opensavvy.ktmongo.dsl.KtMongoDsl
import opensavvy.ktmongo.dsl.LowLevelApi
import opensavvy.ktmongo.dsl.expr.common.AbstractCompoundExpression
import opensavvy.ktmongo.dsl.expr.common.AbstractExpression
import opensavvy.ktmongo.dsl.expr.common.Expression
import opensavvy.ktmongo.dsl.path.Field
import opensavvy.ktmongo.dsl.path.FieldDsl
import opensavvy.ktmongo.dsl.path.Path
import opensavvy.ktmongo.dsl.tree.acceptAll
import kotlin.reflect.KClass
/**
* Implementation of [UpdateOperators].
*/
@KtMongoDsl
class UpdateExpression(
context: BsonContext,
) : AbstractCompoundExpression(context),
UpsertOperators,
FieldDsl {
// region Low-level operations
private class OperatorCombinator(
val type: KClass,
val combinator: (List, BsonContext) -> T
) {
@Suppress("UNCHECKED_CAST") // This is a private class, it should not be used incorrectly
operator fun invoke(sources: List, context: BsonContext) =
combinator(sources as List, context)
}
@LowLevelApi
override fun simplify(children: List): AbstractExpression? {
if (children.isEmpty())
return null
val simplifiedChildren = combinators.fold(children) { newChildren, combinator ->
@Suppress("UNCHECKED_CAST") // safe because of the filter
val matching = newChildren
.filter { it::class == combinator.type }
as List
if (matching.size <= 1)
// At least two elements are required to combine them into a single one!
return@fold newChildren
val childrenWithoutMatching = newChildren - matching.toSet()
childrenWithoutMatching + combinator(matching, context)
}
@OptIn(DangerousMongoApi::class)
if (simplifiedChildren != children)
return UpdateExpression(context).apply {
acceptAll(simplifiedChildren)
}
return this
}
@LowLevelApi
private sealed class UpdateExpressionNode(context: BsonContext) : AbstractExpression(context)
// endregion
// region $set
@OptIn(LowLevelApi::class, DangerousMongoApi::class)
@Suppress("INVISIBLE_REFERENCE")
@KtMongoDsl
override infix fun <@kotlin.internal.OnlyInputTypes V> Field.set(value: V) {
accept(SetExpressionNode(listOf(this.path to value), context))
}
@LowLevelApi
private class SetExpressionNode(
val mappings: List>,
context: BsonContext,
) : UpdateExpressionNode(context) {
override fun simplify() =
this.takeUnless { mappings.isEmpty() }
override fun write(writer: BsonFieldWriter) = with(writer) {
writeDocument("\$set") {
for ((field, value) in mappings) {
writeObjectSafe(field.toString(), value, context)
}
}
}
}
// endregion
// region $setOnInsert
@OptIn(LowLevelApi::class, DangerousMongoApi::class)
@Suppress("INVISIBLE_REFERENCE")
@KtMongoDsl
override infix fun <@kotlin.internal.OnlyInputTypes V> Field.setOnInsert(value: V) {
accept(SetOnInsertExpressionNode(listOf(this.path to value), context))
}
@LowLevelApi
private class SetOnInsertExpressionNode(
val mappings: List>,
context: BsonContext,
) : UpdateExpressionNode(context) {
override fun simplify(): AbstractExpression? =
this.takeUnless { mappings.isEmpty() }
override fun write(writer: BsonFieldWriter) = with(writer) {
writeDocument("\$setOnInsert") {
for ((field, value) in mappings) {
writeObjectSafe(field.toString(), value, context)
}
}
}
}
// endregion
// region $inc
@OptIn(LowLevelApi::class, DangerousMongoApi::class)
@Suppress("INVISIBLE_REFERENCE")
@KtMongoDsl
override infix fun <@kotlin.internal.OnlyInputTypes V : Number> Field.inc(amount: V) {
accept(IncrementExpressionNode(listOf(this.path to amount), context))
}
@LowLevelApi
private class IncrementExpressionNode(
val mappings: List>,
context: BsonContext,
) : UpdateExpressionNode(context) {
override fun simplify(): AbstractExpression? =
this.takeUnless { mappings.isEmpty() }
override fun write(writer: BsonFieldWriter) = with(writer) {
writeDocument("\$inc") {
for ((field, value) in mappings) {
writeObjectSafe(field.toString(), value, context)
}
}
}
}
// endregion
// region $unset
@OptIn(LowLevelApi::class, DangerousMongoApi::class)
@Suppress("INVISIBLE_REFERENCE")
@KtMongoDsl
override fun <@kotlin.internal.OnlyInputTypes V> Field.unset() {
accept(UnsetExpressionNode(listOf(this.path), context))
}
@LowLevelApi
private class UnsetExpressionNode(
val fields: List,
context: BsonContext,
) : UpdateExpressionNode(context) {
override fun simplify(): AbstractExpression? =
this.takeUnless { fields.isEmpty() }
override fun write(writer: BsonFieldWriter) = with(writer) {
writeDocument("\$unset") {
for (field in fields) {
writeBoolean(field.toString(), true)
}
}
}
}
// endregion
// region $rename
@OptIn(LowLevelApi::class, DangerousMongoApi::class)
@Suppress("INVISIBLE_REFERENCE")
@KtMongoDsl
override infix fun <@kotlin.internal.OnlyInputTypes V> Field.renameTo(newName: Field) {
accept(RenameExpressionNode(listOf(this.path to newName.path), context))
}
@LowLevelApi
private class RenameExpressionNode(
val fields: List>,
context: BsonContext,
) : UpdateExpressionNode(context) {
override fun simplify(): AbstractExpression? =
this.takeUnless { fields.isEmpty() }
override fun write(writer: BsonFieldWriter) = with(writer) {
writeDocument("\$rename") {
for ((before, after) in fields) {
writeString(before.toString(), after.toString())
}
}
}
}
// endregion
companion object {
@OptIn(LowLevelApi::class)
private val combinators = listOf(
OperatorCombinator(SetExpressionNode::class) { sources, context ->
SetExpressionNode(sources.flatMap { it.mappings }, context)
},
OperatorCombinator(SetOnInsertExpressionNode::class) { sources, context ->
SetOnInsertExpressionNode(sources.flatMap { it.mappings }, context)
},
OperatorCombinator(IncrementExpressionNode::class) { sources, context ->
IncrementExpressionNode(sources.flatMap { it.mappings }, context)
},
OperatorCombinator(UnsetExpressionNode::class) { sources, context ->
UnsetExpressionNode(sources.flatMap { it.fields }, context)
},
OperatorCombinator(RenameExpressionNode::class) { sources, context ->
RenameExpressionNode(sources.flatMap { it.fields }, context)
},
)
}
}