From 625aa64ba0f739226da8d81492b0a0881c46a78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 10 Aug 2026 22:56:08 +0200 Subject: [PATCH] feat(driver-sync-api): Duplicate the unified API without coroutines --- .../kotlin/MongoAggregationPipeline.kt | 167 ++++++++ .../src/commonMain/kotlin/MongoClient.kt | 71 +++ .../src/commonMain/kotlin/MongoCollection.kt | 160 +++++++ .../src/commonMain/kotlin/MongoDatabase.kt | 95 +++++ .../src/commonMain/kotlin/MongoIterable.kt | 126 ++++++ .../operations/AggregationOperations.kt | 49 +++ .../kotlin/operations/BaseOperations.kt | 36 ++ .../operations/ClientSideViewOperations.kt | 111 +++++ .../kotlin/operations/CollectionOperations.kt | 48 +++ .../kotlin/operations/CountOperations.kt | 139 ++++++ .../kotlin/operations/DeleteOperations.kt | 80 ++++ .../kotlin/operations/FindOperations.kt | 104 +++++ .../kotlin/operations/InsertOperations.kt | 122 ++++++ .../kotlin/operations/UpdateOperations.kt | 403 ++++++++++++++++++ .../operations/UpdatePipelineOperations.kt | 153 +++++++ 15 files changed, 1864 insertions(+) create mode 100644 driver-sync-api/src/commonMain/kotlin/MongoAggregationPipeline.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/MongoClient.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/MongoCollection.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/MongoDatabase.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/MongoIterable.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/AggregationOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/BaseOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/ClientSideViewOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/CollectionOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/CountOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/DeleteOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/FindOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/InsertOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/UpdateOperations.kt create mode 100644 driver-sync-api/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt diff --git a/driver-sync-api/src/commonMain/kotlin/MongoAggregationPipeline.kt b/driver-sync-api/src/commonMain/kotlin/MongoAggregationPipeline.kt new file mode 100644 index 00000000..683698c3 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/MongoAggregationPipeline.kt @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026, 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.sync.api + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.AggregationPipeline +import opensavvy.ktmongo.dsl.aggregation.stages.HasUnionWithCompatibility +import opensavvy.ktmongo.dsl.aggregation.stages.ProjectStageOperators +import opensavvy.ktmongo.dsl.aggregation.stages.SetStageOperators +import opensavvy.ktmongo.dsl.aggregation.stages.UnsetStageOperators +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A multi-stage aggregation pipeline that transforms documents from a MongoDB collection. + * + * Pipelines are immutable. Each stage method returns a new pipeline with the stage appended. + * + * To obtain a pipeline, use [MongoCollection.aggregate][opensavvy.ktmongo.sync.api.operations.AggregationOperations.aggregate]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * users.aggregate() + * .match { User::age gt 18 } + * .sort { ascending(User::name) } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/) + */ +interface MongoAggregationPipeline : AggregationPipeline { + + // region Iterable + + /** + * Access the data of this pipeline as a [MongoIterable]. + * + * The methods of [MongoIterable] are available directly on this type + * as extension methods, there is no need to convert to a [MongoIterable] yourself. + * + * If [type] doesn't match [Document], the behavior is unspecified. + */ + @LowLevelApi + fun asIterable(type: KType): MongoIterable + + // endregion + // region Stages + + override fun limit(amount: Long): MongoAggregationPipeline + + override fun limit(amount: Int): MongoAggregationPipeline + + override fun match(filter: FilterQuery.() -> Unit): MongoAggregationPipeline + + override fun sample(size: Int): MongoAggregationPipeline + + override fun set(block: SetStageOperators.() -> Unit): MongoAggregationPipeline + + override fun skip(amount: Long): MongoAggregationPipeline + + override fun skip(amount: Int): MongoAggregationPipeline + + override fun sort(block: SortOptionDsl.() -> Unit): MongoAggregationPipeline + + override fun unset(block: UnsetStageOperators.() -> Unit): MongoAggregationPipeline + + override fun project(block: ProjectStageOperators.() -> Unit): MongoAggregationPipeline + + override fun unionWith(other: HasUnionWithCompatibility): MongoAggregationPipeline + + override fun group(block: AccumulationOperators.() -> Unit): MongoAggregationPipeline + + override fun countTo(field: Field): MongoAggregationPipeline + + override fun countTo(field: KProperty1): MongoAggregationPipeline + + // endregion +} + +/** + * Returns the first document found by this query, or throws an exception. + * + * @throws NoSuchElementException If this query returned no results. + * @see firstOrNull Return `null` instead of throwing an exception. + */ +@OptIn(LowLevelApi::class) +inline fun MongoAggregationPipeline.first(): Document = + asIterable(typeOf()).first() + +/** + * Returns the first document found by this query, or returns `null`. + * + * @see first Throw an exception instead of returning `null`. + */ +@OptIn(LowLevelApi::class) +inline fun MongoAggregationPipeline.firstOrNull(): Document? = + asIterable(typeOf()).firstOrNull() + +/** + * Executes [action] for each document returned by this query. + * + * This method streams all returned documents into the [action] function. + * The entire response set is not loaded at once into memory. + * + * MongoDB cursors are batched: a batch is queried, processed, then another batch is requested, etc. + * The batch size can be configured in the operation creating this iterable. + * + * If the operation contains a sort without an index, MongoDB will load all results + * into memory. The driver will still stream the results. + * + * @see toList Store all results in a [List]. + * @see toSet Store all results in a [Set]. + */ +@OptIn(LowLevelApi::class) +inline fun MongoAggregationPipeline.forEach(noinline action: (Document) -> Unit) = + asIterable(typeOf()).forEach(action) + +/** + * Reads the entirety of this iterable into a [List]. + * + * Since lists are in-memory, this method loads the entirety of the results into memory. + * + * @see forEach Execute an action for each result. + * @see toSet Store all results in a [Set]. + */ +@OptIn(LowLevelApi::class) +inline fun MongoAggregationPipeline.toList(): List = + asIterable(typeOf()).toList() + +/** + * Reads the entirety of this iterable into a [Set]. + * + * Since sets are in-memory, this method loads the entirety of the results into memory. + * + * @see forEach Execute an action for each result. + * @see toList Store all results in a [List]. + */ +@OptIn(LowLevelApi::class) +inline fun MongoAggregationPipeline.toSet(): Set = + asIterable(typeOf()).toSet() diff --git a/driver-sync-api/src/commonMain/kotlin/MongoClient.kt b/driver-sync-api/src/commonMain/kotlin/MongoClient.kt new file mode 100644 index 00000000..50f96186 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/MongoClient.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, 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.sync.api + +/** + * Entry-point to the KtMongo drivers. + * + * This interface exists to unify the API of the different KtMongo drivers. + * Each implementation may add its own specificities. + * + * Each implementation provides its own way to obtain an instance of this interface. + * + * ### Organizing data + * + * Accessing MongoDB data happens in three steps: + * - [MongoClient]: represents the connection to the MongoDB application, handles + * the lifecycle and the configuration. + * - [MongoDatabase] (accessed with [MongoClient.database]): each database groups data together. + * This allows deploying multiple applications (or the same application multiple times) + * without name collisions. + * - [MongoCollection] (accessed with [MongoDatabase.collection]): each collection stores data together. + * Documents in a collection may have a different structure. + * + * ### Example + * + * ```kotlin + * @Serializable + * class User( + * val _id: ObjectId, + * val name: String, + * val age: Int, + * ) + * + * fun main() = runBlocking { + * val client: MongoClient = // Implementation-dependent accessor + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("The database contains ${users.count()} users.") + * } + * ``` + */ +interface MongoClient : AutoCloseable { + + /** + * Creates a [MongoDatabase] object. + * + * This method is purely a client-side operation, it does nothing in the MongoDB server. + * In MongoDB, databases and collections are created implicitly on the first insert. + * + * To learn more about the restrictions on the database [name], see [MongoDatabase.name]. + * + * For an example, see [MongoClient]. + */ + fun database(name: String): MongoDatabase +} diff --git a/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt b/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt new file mode 100644 index 00000000..2278bccb --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2026, 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.sync.api + +import opensavvy.ktmongo.bson.BsonFactory +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.sync.api.operations.* +import kotlin.reflect.KType + +/** + * A collection stores related documents together. + * + * Usually, all documents in a collection have the same shape (the same fields). + * However, heterogeneous structure can be achieved by using: + * - Kotlin collections, like [List] and [Set], the embed an arbitrary number of items. + * - Polymorphism, for example with `sealed class`, to have different fields based on a discriminator. + * + * To avoid name collisions, collections are grouped into [databases][MongoDatabase]. + * + * To obtain a collection, see [MongoDatabase.collection]. + * + * ### Size limit + * + * A MongoDB document cannot exceed 16 MiB. + * + * You can measure the size of a document with [opensavvy.ktmongo.bson.BsonDocument.toByteArray] + * followed by [ByteArray.size]. + * + * The maximum nesting is 100 levels. + * Each document or array adds a level. + * + * ### Operations + * + * The following lists the available operations using the `mongosh` equivalent: + * + * - [aggregate][AggregationOperations.aggregate] + * - [bulkWrite][UpdateOperations.bulkWrite] + * - [countDocuments][CountOperations.count] + * - [deleteOne][DeleteOperations.deleteOne] + * - [deleteMany][DeleteOperations.deleteMany] + * - [drop][CollectionOperations.drop] + * - [estimatedDocumentCount][CountOperations.countEstimated] + * - [find][FindOperations.find] + * - [findOne][FindOperations.findOne] + * - [findOneAndUpdate][UpdateOperations.findOneAndUpdate] + * - [insertOne][InsertOperations.insertOne] + * - [insertMany][InsertOperations.insertMany] + * - [updateOne][UpdateOperations.updateOne] + * - [updateOne][UpdatePipelineOperations.updateOneWithPipeline] with an aggregation pipeline + * - [updateOne][UpdateOperations.upsertOne] with `upsert: true` + * - [updateOne][UpdatePipelineOperations.upsertOneWithPipeline] with `upsert: true` and an aggregation pipeline + * - [updateMany][UpdateOperations.updateMany] + * - [updateMany][UpdatePipelineOperations.updateManyWithPipeline] with an aggregation pipeline + * - [replaceOne][UpdateOperations.replaceOne] + * - [replaceOne][UpdateOperations.repsertOne] with `upsert: true` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/databases-and-collections/) + * - [Size limits](https://www.mongodb.com/docs/manual/reference/limits/#bson-documents) + */ +interface MongoCollection : ObjectIdGenerator, + AggregationOperations, + ClientSideViewOperations, + CollectionOperations, + CountOperations, + DeleteOperations, + FindOperations, + InsertOperations, + UpdateOperations, + UpdatePipelineOperations { + + /** + * THe name of this collection. + * + * The collection name must be unique within a single [MongoDatabase] (otherwise, the two instances refer to the same data). + * + * - The name should begin with a letter or an underscore (`_`). + * - The name cannot be empty. + * - The name cannot contain the null character nor the `$` character. + * - The name cannot being with `system.`. + * - The name cannot contain `.system.`. + * - It is recommended to avoid names longer than 171 bytes. + * + * ### External resources + * + * - [Name restrictions](https://www.mongodb.com/docs/manual/reference/limits/#mongodb-limit-Restriction-on-Collection-Names) + */ + val name: String + + /** + * The concatenation of the database's [name][MongoDatabase.name] and the collection's [name], + * separated by a dot (`.`). + */ + val fullyQualifiedName: String + + /** + * The [BsonFactory] used to serialize and deserialize values stored in this collection. + * + * This property stores all serialization configurations and allows creating custom BSON objects. + * + * For more information, see [BsonFactory]. + */ + val factory: BsonFactory + + /** + * The strategy used to convert property names to BSON document keys. + * + * For more information, see [PropertyNameStrategy]. + */ + val propertyNameStrategy: PropertyNameStrategy + + /** + * The algorithm used to generate new [ObjectId] instances for this collection. + * + * For more information, see [ObjectIdGenerator]. + * + * You can also directly call [newId] on the collection itself. + */ + val objectIdGenerator: ObjectIdGenerator + + override fun newId(): ObjectId = + objectIdGenerator.newId() + + /** + * The [KType] instance that corresponds to the collection's document type. + * + * This property is used by serialization libraries to know the exact type to deserialize, + * especially in the presence of type parameters. + * + * Everyday users should not need to interact with this property directly. + */ + @LowLevelApi + val type: KType + + // region Specializations + + override fun filter(filter: FilterQuery.() -> Unit): MongoCollection + + // endregion + +} diff --git a/driver-sync-api/src/commonMain/kotlin/MongoDatabase.kt b/driver-sync-api/src/commonMain/kotlin/MongoDatabase.kt new file mode 100644 index 00000000..08bd73ea --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/MongoDatabase.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026, 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.sync.api + +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A grouping of collections with the same theme. + * + * ### What is a database? + * + * [Collections][MongoCollection] are grouped into databases to avoid name collisions. + * Databases are similar to Kotlin packages. + * If multiple applications are deployed in the same MongoDB instance in their own database, + * they can use the same collection names (e.g. `users`) without conflicts. + * + * Each database has a [name] that must be unique within a MongoDB deployment. + * + * ### Access + * + * To obtain a database, see [MongoClient.database]. + * + * To obtain a collection, see [collection]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/databases-and-collections/) + */ +interface MongoDatabase { + + /** + * The unique name of this database. + * + * This name must be unique within a MongoDB deployment. + * + * - Two databases cannot have a name that only differs by case (e.g. `salesData` and `SalesData` cannot coexist). + * - Once a database is created, you must always access it with the same case as when it was created. + * The creation of a database happens on the first write operation in one of its collections. + * - It is recommended to avoid the following characters: `/\. "$*<>:|?`. Depending on the platform MongoDB is running on, some of them may be forbidden. + * - The name cannot be empty. + * - The name cannot be longer than 64 bytes. + * + * ### External resources + * + * - [Name restrictions](https://www.mongodb.com/docs/manual/reference/limits/?atlas-provider=aws&atlas-class=general#naming-restrictions) + */ + val name: String + + /** + * Creates a [MongoCollection] object. + * + * This method is purely a client-side operation, it does nothing in the MongoDB server. + * In MongoDB, databases and collections are created implicitly on the first insert. + * + * For an example, see [MongoClient]. + * + * Prefer using the overload that doesn't have a [type] argument. + * If [type] is specified, it must match [Document]. + * Otherwise, the behavior is unspecified. + */ + @LowLevelApi + fun collection(name: String, type: KType): MongoCollection + + /** + * Creates a [MongoCollection] object. + * + * This method is purely a client-side operation, it does nothing in the MongoDB server. + * In MongoDB, databases and collections are created implicitly on the first insert. + * + * For an example, see [MongoClient]. + */ + @OptIn(LowLevelApi::class) + @Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION") + // name: CharSequence instead of String to allow implementations to define a more specific overload + // to customize the return type + final inline fun collection(name: CharSequence): MongoCollection = + collection(name.toString(), type = typeOf()) + +} diff --git a/driver-sync-api/src/commonMain/kotlin/MongoIterable.kt b/driver-sync-api/src/commonMain/kotlin/MongoIterable.kt new file mode 100644 index 00000000..52351cf2 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/MongoIterable.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026, 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.sync.api + +/** + * Streaming-capable iterable cursor to read data from the database. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface MongoIterable { + + /** + * Returns the first document found by this query, or throws an exception. + * + * @throws NoSuchElementException If this query returned no results. + * @see firstOrNull Return `null` instead of throwing an exception. + */ + fun first(): Document + + /** + * Returns the first document found by this query, or returns `null`. + * + * @see first Throw an exception instead of returning `null`. + */ + fun firstOrNull(): Document? + + /** + * Executes [action] for each document returned by this query. + * + * This method streams all returned documents into the [action] function. + * The entire response set is not loaded at once into memory. + * + * MongoDB cursors are batched: a batch is queried, processed, then another batch is requested, etc. + * The batch size can be configured in the operation creating this iterable. + * + * If the operation contains a sort without an index, MongoDB will load all results + * into memory. The driver will still stream the results. + * + * @see toList Store all results in a [List]. + * @see toSet Store all results in a [Set]. + */ + fun forEach(action: (Document) -> Unit) + + /** + * Reads the entirety of this iterable into a [List]. + * + * Since lists are in-memory, this method loads the entirety of the results into memory. + * + * @see forEach Execute an action for each result. + * @see toSet Store all results in a [Set]. + */ + fun toList(): List { + val list = ArrayList() + forEach { list.add(it) } + return list + } + + /** + * Reads the entirety of this iterable into a [Set]. + * + * Since sets are in-memory, this method loads the entirety of the results into memory. + * + * @see forEach Execute an action for each result. + * @see toList Store all results in a [List]. + */ + fun toSet(): Set { + val set = HashSet() + forEach { set.add(it) } + return set + } + + /** + * This method always throws an exception. + * + * Streaming a [MongoIterable] into a [Sequence] is not supported, because iterables + * must be closed, and sequences cannot detect when iteration finishes. + * + * If you want to use a [Sequence] data structure for convenience, and the volume of data + * is low, use [toList] followed by [asSequence][List.asSequence]. + * All data will be loaded in memory at once. + * + * If streaming is important, either use [forEach], `stream` (Java-only). + * + * @see forEach Execute an action for each result. + * @see toList Store all results in a [List]. + * @see toSet Store all results in a [Set]. + */ + @Deprecated("Kotlin Sequences are not capable of closing a resource after they are done. Using sequences with a MongoIterable will create memory leaks. Instead, use toList, forEach, stream (Java only) or the coroutines driver's asFlow", ReplaceWith("this.toList().asSequence()"), level = DeprecationLevel.ERROR) + fun asSequence(): Sequence = throw UnsupportedOperationException("Sequences are not supported because they create memory lists. Use lists, streams, or simply forEach instead.") + + /** + * This method always throws an exception. + * + * Streaming a [MongoIterable] into a [Sequence] is not supported, because iterables + * must be closed, and sequences cannot detect when iteration finishes. + * + * If you want to use a [Sequence] data structure for convenience, and the volume of data + * is low, use [toList] followed by [asSequence][List.asSequence]. + * All data will be loaded in memory at once. + * + * If streaming is important, either use [forEach], `stream` (Java-only). + * + * @see forEach Execute an action for each result. + * @see toList Store all results in a [List]. + * @see toSet Store all results in a [Set]. + */ + @Deprecated("Kotlin Sequences are not capable of closing a resource after they are done. Using sequences with a MongoIterable will create memory leaks. Instead, use toList, forEach, stream (Java only) or the coroutines driver's asFlow", ReplaceWith("this.toList().asSequence()"), level = DeprecationLevel.ERROR) + fun toSequence(): Sequence = throw UnsupportedOperationException("Sequences are not supported because they create memory lists. Use lists, streams, or simply forEach instead.") + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/AggregationOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/AggregationOperations.kt new file mode 100644 index 00000000..9fe43323 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/AggregationOperations.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.sync.api.MongoAggregationPipeline + +/** + * The different MongoDB operations related to aggregation pipelines. + */ +interface AggregationOperations : BaseOperations { + + /** + * Starts an aggregation pipeline on this collection. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * users.aggregate() + * .match { User::age gt 18 } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/) + */ + fun aggregate(): MongoAggregationPipeline + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/BaseOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/BaseOperations.kt new file mode 100644 index 00000000..f002489e --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/BaseOperations.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi + +/** + * The common interface to all operations interfaces. + * + * This interface provides no useful value to end-users. + */ +interface BaseOperations { + + /** + * The full BSON configuration, used by the DSL to generate queries. + * + * For more information, see [BsonContext]. + */ + @LowLevelApi + val context: BsonContext +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/ClientSideViewOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/ClientSideViewOperations.kt new file mode 100644 index 00000000..5924169c --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/ClientSideViewOperations.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.sync.api.MongoCollection + +/** + * The different MongoDB operations related to client-side views. + */ +interface ClientSideViewOperations : BaseOperations { + + /** + * Creates a client-side view containing all the documents that match [filter]. + * + * ### Client-side views + * + * MongoDB has a concept of [views](https://www.mongodb.com/docs/manual/core/views/): read-only results of aggregation + * pipelines useful to avoid repeating the same queries in multiple places. + * + * This function **does not create a MongoDB view**. + * Instead, it creates a logical view, which is purely syntax sugar in the KtMongo library and doesn't exist + * in MongoDB itself. + * The database is never aware of client-side views. + * + * Client-side views do not have the limitations of real MongoDB views: they can be mutable and support all operators. + * + * Essentially, this method returns a [MongoCollection] implementation that combines the [filter] with every filter + * provided by any other operation, using a [`$and`][FilterQuery.and]. + * + * ### Example + * + * Let's imagine you want to implement logical deletion of items: + * ```kotlin + * class Parcel( + * val _id: ObjectId, + * val owner: ObjectId, + * val isActive: Boolean = true, + * ) + * ``` + * In that situation, you will need to remember to apply a filter in almost all methods you implement: + * ```kotlin + * // Find the user's active parcels + * parcels.find({ sort { descending(Parcel::_id) } }) { + * Parcel::owner eq currentUserId() + * Parcel::isActive ne false // ⚠ Don't forget! + * } + * + * // An owner transfers all active parcels to another one + * parcels.updateMany( + * filter = { + * Parcel::owner eq currentUserId() + * Parcel::isActive ne false // ⚠ Don't forget! + * }, + * update = { + * Parcel::owner set transferDestinationUserId + * } + * ) + * ``` + * + * To avoid worrying about specifying the same filter each time, you can use client-side logical views to + * factor it out into a subset collection: + * ```kotlin + * val activeParcels = parcels.filter { Parcel::isActive ne false } + * + * // Find the user's active parcels + * activeParcels.find({ sort { descending(Parcel::_id) } }) { + * Parcel::owner eq currentUserId() + * } + * + * // An owner transfers all active parcels to another one + * activeParcels.updateMany( + * filter = { Parcel::owner eq currentUserId() }, + * update = { Parcel::owner set transferDestinationUserId } + * ) + * ``` + * This example is strictly identical to the previous one: the driver combines the client-side view's + * and the operation's filters. + * + * A client-side view can be created from another one, which allows to further shorten the update: + * ```kotlin + * // An owner transfers all active parcels to another one + * activeParcels.filter { Parcel::owner eq currentUserId() } + * .updateMany { Parcel::owner set transferDestinationUserId } + * ``` + * This style, using an explicit `filter` function instead of using the operation's own filter, allows using + * Kotlin's trailing syntax. We encourage its usage, there is no performance impact. + * + * Learn more in the [KtMongo feature page](https://ktmongo.opensavvy.dev/features/filtered-collections.html). + */ + fun filter( + filter: FilterQuery.() -> Unit, + ): ClientSideViewOperations + // ↑ Each implementation should override with a more specific type + // This is an emulated self-type + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/CollectionOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/CollectionOperations.kt new file mode 100644 index 00000000..b2465fe6 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/CollectionOperations.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.DropOptions + +/** + * Interface grouping MongoDB operations relating to collection administration. + */ +interface CollectionOperations : BaseOperations { + + /** + * Removes an entire collection from the database. + * + * All documents within the collection are deleted. + * + * Indexes attached to this collection are also deleted. + * + * ### Example + * + * ```kotlin + * collection.drop() + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/drop/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.drop/) + */ + fun drop( + options: DropOptions.() -> Unit = {}, + ) + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/CountOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/CountOperations.kt new file mode 100644 index 00000000..381b5549 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/CountOperations.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.CountOptions +import opensavvy.ktmongo.dsl.query.FilterQuery + +/** + * The different MongoDB operations related to counting documents. + */ +interface CountOperations : BaseOperations { + + /** + * Counts how many documents exist in the collection. + * + * ### Implementation + * + * This method, just like in `mongosh`, in syntax sugar for an aggregation pipeline using + * the [countTo][opensavvy.ktmongo.dsl.aggregation.stages.HasCount.countTo] stage. + * + * ### External resources + * + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/) + * + * @see countEstimated Faster alternative when the result doesn't need to be exact. + */ + fun count(): Long + + /** + * Counts how many documents match [predicate] in the collection. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.count { + * User::name eq "foo" + * User::age eq 10 + * } + * ``` + * + * ### Implementation + * + * This method, just like in `mongosh`, in syntax sugar for an aggregation pipeline using + * the [match][opensavvy.ktmongo.dsl.aggregation.stages.HasMatch.match] and + * the [countTo][opensavvy.ktmongo.dsl.aggregation.stages.HasCount.countTo] stages. + * + * ### External resources + * + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/) + */ + fun count( + options: CountOptions.() -> Unit = {}, + predicate: FilterQuery.() -> Unit, + ): Long + + /** + * Tests if there exists a document that matches [predicate] in the collection. + * + * This method is a convenience function for calling [count] with a [limit][CountOptions.limit] of 1. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.exists { + * User::name eq "foo" + * User::age eq 10 + * } + * ``` + */ + fun exists( + options: CountOptions.() -> Unit = {}, + predicate: FilterQuery.() -> Unit, + ): Boolean = count( + options = { + options() + limit(1) + }, + predicate = predicate, + ) == 1L + + /** + * Counts all documents in the collection. + * + * ### Implementation + * + * This function reads collection metadata instead of actually counting through all documents. + * This makes it much more performant (almost no CPU nor RAM usage), but the count may be slightly out of date. + * + * In particular, it may become inaccurate when: + * - there are orphaned documents in a shared cluster, + * - an unclean shutdown happened. + * + * Views do not possess the required metadata. + * When this function is called on a view (either a MongoDB view or a [filter] logical view), a regular [count] is executed instead. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.countEstimated() + * ``` + * + * ### External resources + * + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.estimatedDocumentCount/) + * + * @see count Perform the count for real. + */ + fun countEstimated(): Long + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/DeleteOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/DeleteOperations.kt new file mode 100644 index 00000000..e50e9edf --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/DeleteOperations.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.DeleteManyOptions +import opensavvy.ktmongo.dsl.command.DeleteOneOptions +import opensavvy.ktmongo.dsl.query.FilterQuery + +/** + * The different MongoDB operations related to deleting documents. + */ +interface DeleteOperations : BaseOperations { + + /** + * Deletes the first document found that matches [filter]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int + * ) + * + * collection.deleteOne { + * User::name eq "Bob" + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/delete/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteOne) + */ + fun deleteOne( + options: DeleteOneOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit, + ) + + /** + * Deletes all documents that match [filter]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int + * ) + * + * collection.deleteMany { + * User::age lt 18 + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/delete/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteMany/) + */ + fun deleteMany( + options: DeleteManyOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit, + ) + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/FindOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/FindOperations.kt new file mode 100644 index 00000000..6fb8fb4d --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/FindOperations.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.FindOptions +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.sync.api.MongoIterable + +/** + * The different MongoDB operations related to finding documents. + */ +interface FindOperations : BaseOperations { + + /** + * Finds all documents in this collection. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/find/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.find/) + * + * @see find When a filter is needed. + */ + fun find(): MongoIterable + + /** + * Finds all documents in this collection that satisfy [filter]. + * + * If multiple predicates are specified, an [and][FilterQuery.and] operator is implied. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.find { + * User::name eq "foo" + * User::age eq 10 + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/find/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.find/) + * + * @see findOne When only one result is expected. + */ + fun find( + options: FindOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit, + ): MongoIterable + + /** + * Finds a document in this collection that satisfies [filter]. + * + * If multiple predicates are specified, an [and][FilterQuery.and] operator is implied. + * + * This function doesn't check that there is exactly one value in the collection. + * It simply returns the first matching document it finds. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.findOne { + * User::name eq "foo" + * User::age eq 10 + * } + * ``` + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/find/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.findOne/) + * + * @see find When multiple results are expected. + */ + fun findOne( + options: FindOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit, + ): Document? = + find(options, filter).firstOrNull() + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/InsertOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/InsertOperations.kt new file mode 100644 index 00000000..8fb09318 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/InsertOperations.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.InsertManyOptions +import opensavvy.ktmongo.dsl.command.InsertOneOptions + +/** + * The different MongoDB operations related to inserting documents. + */ +interface InsertOperations : BaseOperations { + + /** + * Inserts a [document]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.insertOne(User(name = "Bob", age = 18)) + * ``` + * + * ### Filtered collections + * + * Insert operations ignore the configured [filter][ClientSideViewOperations.filter]: the document will be inserted even if it does not match the filter. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/insert/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/) + * + * @see insertMany Insert multiple documents. + */ + fun insertOne( + document: Document, + options: InsertOneOptions.() -> Unit = {}, + ) + + /** + * Inserts multiple [documents] in a single operation. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.insertMany(users) + * ``` + * + * ### Filtered collections + * + * Insert operations ignore the configured [filter][ClientSideViewOperations.filter]: the document will be inserted even if it does not match the filter. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/insert/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertMany/) + * + * @see insertOne Insert a single document. + */ + fun insertMany( + documents: Iterable, + options: InsertManyOptions.() -> Unit = {}, + ) + + /** + * Inserts multiple [documents] in a single operation. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.insertMany( + * User(name = "Bob", age = 18), + * User(name = "Alice", age = 17) + * ) + * ``` + * + * ### Filtered collections + * + * Insert operations ignore the configured [filter][ClientSideViewOperations.filter]: the document will be inserted even if it does not match the filter. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/insert/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertMany/) + * + * @see insertOne Insert a single document. + */ + fun insertMany( + vararg documents: Document, + options: InsertManyOptions.() -> Unit = {}, + ) { + insertMany(documents.asList(), options) + } + +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/UpdateOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/UpdateOperations.kt new file mode 100644 index 00000000..d9c830d1 --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/UpdateOperations.kt @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.bson.BsonValue +import opensavvy.ktmongo.dsl.command.BulkWrite +import opensavvy.ktmongo.dsl.command.BulkWriteOptions +import opensavvy.ktmongo.dsl.command.ReplaceOptions +import opensavvy.ktmongo.dsl.command.UpdateOptions +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery + +/** + * The different MongoDB operations related to updating documents. + */ +interface UpdateOperations : BaseOperations { + + /** + * Updates all documents that match [filter] according to [update]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.updateMany( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * }, + * ) + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateMany/) + * + * @param filter Optional filter to select which documents are updated. + * If no filter is specified, all documents are updated. + * @see updateOne + * @see UpdatePipelineOperations.updateManyWithPipeline Identical operation using aggregation operators. + */ + @IgnorableReturnValue + fun updateMany( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateQuery.() -> Unit, + ): UpdateResult + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * }, + * ) + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/) + * + * @param filter Optional filter to select which document is updated. + * If no filter is specified, the first document found is updated. + * @see updateMany Update more than one document. + * @see findOneAndUpdate Also returns the result of the update. + * @see UpdatePipelineOperations.updateOneWithPipeline Identical operation using aggregation operators. + */ + @IgnorableReturnValue + fun updateOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateQuery.() -> Unit, + ): UpdateResult + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one is updated. + * + * If no documents match [filter], a new one is created. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.upsertOne( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * }, + * ) + * ``` + * + * If a document exists that has the `name` of "Patrick", its age is set to 15. + * If none exists, a document with `name` "Patrick" and `age` 15 is created. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists--upsert-) + * + * @see updateOne + * @see UpdatePipelineOperations.upsertOneWithPipeline Identical operation using aggregation operators. + */ + @IgnorableReturnValue + fun upsertOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpsertQuery.() -> Unit, + ): UpsertResult + + /** + * Replaces a document that matches [filter] by [document]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Data races + * + * This operator is often used by first reading a document, processing it, and replacing it. + * This can be dangerous in distributed systems because another replica of the server could have updated + * the document between the read and the write. + * + * If this is a concern, it is recommended to use [updateOne] with explicit operators on the data that has changed, + * allowing to do the modification in a single operation. Doing the update that way, MongoDB is responsible + * for ensuring the read and the write are atomic. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.replaceOne( + * filter = { + * User::name eq "Patrick" + * }, + * document = User("Bob", 15) + * ) + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.replaceOne/) + * + * @param filter Optional filter to select which document is updated. + * If no filter is specified, the first document found is updated. + * @see updateOne Updates an existing document. + * @see updateMany Update more than one document. + * @see repsertOne Replaces a document, or inserts it if it doesn't exist. + * @see findOneAndUpdate Also returns the result of the update. + */ + fun replaceOne( + options: ReplaceOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + document: Document, + ) + + /** + * Replaces a document that matches [filter] by [document]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * If no documents match [filter], [document] is [inserted][InsertOperations.insertOne]. + * + * ### Data races + * + * This operator is often used by first reading a document, processing it, and replacing it. + * This can be dangerous in distributed systems because another replica of the server could have updated + * the document between the read and the write. + * + * If this is a concern, it is recommended to use [updateOne] with explicit operators on the data that has changed, + * allowing to do the modification in a single operation. Doing the update that way, MongoDB is responsible + * for ensuring the read and the write are atomic. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.repsertOne( + * filter = { + * User::name eq "Patrick" + * }, + * document = User("Bob", 15) + * ) + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.replaceOne/) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.replaceOne/#upsert) + * + * @param filter Optional filter to select which document is updated. + * If no filter is specified, the first document found is updated. + * @see updateOne Updates an existing document. + * @see replaceOne Replaces an existing document. + * @see findOneAndUpdate Also returns the result of the update. + */ + fun repsertOne( + options: ReplaceOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + document: Document, + ) + + /** + * Updates one element that matches [filter] according to [update] and returns it, atomically. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.findOneAndUpdate( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * }, + * ) + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/findAndModify/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndUpdate/) + * + * @param filter Optional filter to select which document is updated. + * If no filter is specified, the first document found is updated. + * @see updateMany Update more than one document. + * @see updateOne Do not return the value. + */ + fun findOneAndUpdate( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateQuery.() -> Unit, + ): Document? + + /** + * Performs multiple update operations in a single request. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * upsertOne( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * } + * ) + * + * updateMany { + * User::age inc 1 + * } + * } + * ``` + * + * To see which operations are available and their respective syntax, see [BulkWrite]. + * + * ### Using filtered writes + * + * We can group operations by the filter they apply on: + * ```kotlin + * collection.bulkWrite { + * filtered(filter = { User::isAlive eq true }) { + * updateOne(…) + * updateOne(…) + * updateMany(…) + * } + * + * updateOne(…) + * } + * ``` + * + * To learn more, see [filtered][BulkWrite.filtered]. + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/bulkWrite/) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite) + */ + fun bulkWrite( + options: BulkWriteOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + operations: BulkWrite.() -> Unit, + ) + + /** + * The return value of [updateMany] and [updateOne]. + */ + interface UpdateResult { + + /** + * `true` if the update was acknowledged. + * + * To control whether the update is acknowledged, see [UpdateOptions.writeConcern]. + * + * If the update was not acknowledged, this property returns `false` and all properties throw [UnsupportedOperationException]. + */ + val acknowledged: Boolean + + /** + * The number of matched documents. + * + * @throws UnsupportedOperationException If the update was not [acknowledged]. + */ + val matchedCount: Long + + /** + * The number of modified documents. + * + * If this update created new documents (e.g., with [upsertOne]), they are not counted + * by this field: they did not already exist, so they were not modified. + * + * @throws UnsupportedOperationException If the update was not [acknowledged]. + */ + val modifiedCount: Long + } + + /** + * The return value of [upsertOne]. + */ + interface UpsertResult : UpdateResult { + + /** + * The `_id` of the upserted document, if any. + * + * If this request modified an existing document, contains `null`. + * + * @throws UnsupportedOperationException If the update was not [acknowledged]. + */ + val upsertedId: BsonValue? + + /** + * The number of upserted documents. + * + * @throws UnsupportedOperationException If the update was not [acknowledged]. + */ + val upsertedCount: Int + } +} diff --git a/driver-sync-api/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt b/driver-sync-api/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt new file mode 100644 index 00000000..e98004bc --- /dev/null +++ b/driver-sync-api/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, 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.sync.api.operations + +import opensavvy.ktmongo.dsl.command.UpdateOptions +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.sync.api.operations.UpdateOperations.UpdateResult +import opensavvy.ktmongo.sync.api.operations.UpdateOperations.UpsertResult + +/** + * Interface grouping MongoDB operations allowing to update existing information using aggregation pipelines. + */ +interface UpdatePipelineOperations : BaseOperations { + + /** + * Updates all documents that match [filter] according to the [update] pipeline. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.updateManyWithPipeline( + * filter = { + * User::name eq "Patrick" + * } + * ) { + * set { + * User::age set 15 + * } + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-aggregation-pipeline) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateMany/#std-label-updateMany-behavior-aggregation-pipeline) + * + * @param filter Optional filter to select which documents are updated. + * If no filter is specified, all documents are updated. + * @see updateOneWithPipeline Update a single document. + * @see UpdateOperations.updateMany Identical operation using update query operators. + */ + @IgnorableReturnValue + fun updateManyWithPipeline( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateResult + + /** + * Updates a single document that matches [filter] according to the [update] pipeline. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.updateOneWithPipeline( + * filter = { + * User::name eq "Patrick" + * } + * ) { + * set { + * User::age set 15 + * } + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-aggregation-pipeline) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/#std-label-updateOne-behavior-aggregation-pipeline) + * + * @param filter Optional filter to select which document is updated. + * If no filter is specified, the first document found is updated. + * @see updateManyWithPipeline Update multiple documents. + * @see upsertOneWithPipeline Update a document, creating it if it doesn't exist. + * @see UpdateOperations.updateOne Identical operation using update query operators. + */ + @IgnorableReturnValue + fun updateOneWithPipeline( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateResult + + /** + * Updates a single document that matches [filter] according to the [update] pipeline. + * + * If multiple documents match [filter], only the first one is updated. + * + * If no documents match [filter], a new one is created. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.upsertOneWithPipeline( + * filter = { + * User::name eq "Patrick" + * } + * ) { + * set { + * User::age set 15 + * } + * } + * ``` + * + * ### External resources + * + * - [Protocol documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-aggregation-pipeline) + * - [`mongosh` documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/#std-label-updateOne-behavior-aggregation-pipeline) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists--upsert-) + * + * @see updateOneWithPipeline Do nothing if the document doesn't already exist. + * @see UpdateOperations.upsertOne Identical operation using update query operators. + */ + @IgnorableReturnValue + fun upsertOneWithPipeline( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpsertResult + +} -- 2.51.2