diff --git a/driver-sync-api/build.gradle.kts b/driver-sync-api/build.gradle.kts new file mode 100644 index 00000000..957d32bd --- /dev/null +++ b/driver-sync-api/build.gradle.kts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025-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. + */ + +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.library) + alias(libsCommon.plugins.kotlinx.serialization) + alias(libsCommon.plugins.testBalloon) +} + +kotlin { + jvm() + js { + nodejs() + } + // linuxX64() + // linuxArm64() + // macosX64() + // macosArm64() + // iosArm64() + // iosX64() + // iosSimulatorArm64() + // watchosX64() + // watchosArm32() + // watchosArm64() + // watchosSimulatorArm64() + // tvosX64() + // tvosArm64() + // tvosSimulatorArm64() + // mingwX64() + // wasmJs { + // nodejs() + // } + + sourceSets.commonMain.dependencies { + api(projects.dsl) + } + + sourceSets.commonTest.dependencies { + implementation(libsCommon.opensavvy.prepared.testBalloon) + implementation(libsCommon.kotlin.test) + } +} + +library { + name.set("KtMongo: MongoDB driver for Kotlin • Synchronous API") + description.set("Kotlin-first MongoDB driver: shared API between the various driver implementations") + homeUrl.set("https://ktmongo.opensavvy.dev") + + license.set { + name.set("Apache 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + + coverage.set(50) // TODO: Increase in the future +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 016a5bb5..7cbe9b01 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -88,6 +88,7 @@ include( "driver-api", "driver-shared-official", "driver-shared-kmongo", + "driver-sync-api", "driver-sync", "driver-sync-java", "driver-sync-kmongo", -- 2.51.2 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 02/10] 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 From 751e6ad884c03b94678dd2126c0f6828798af9c8 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 23:22:29 +0200 Subject: [PATCH 03/10] feat(driver-coroutines): Missing specialization of filter() --- .../src/jvmMain/kotlin/CoroutineFilteredMongoCollectionImpl.kt | 3 +-- .../src/jvmMain/kotlin/CoroutineMongoCollection.kt | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineFilteredMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineFilteredMongoCollectionImpl.kt index 93183cdf..9fa479f7 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineFilteredMongoCollectionImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineFilteredMongoCollectionImpl.kt @@ -16,7 +16,6 @@ package opensavvy.ktmongo.coroutines -import opensavvy.ktmongo.api.MongoCollection import opensavvy.ktmongo.api.MongoIterable import opensavvy.ktmongo.api.operations.UpdateOperations import opensavvy.ktmongo.bson.official.BsonFactory @@ -52,7 +51,7 @@ private class CoroutineFilteredMongoCollectionImpl( override val type: KType get() = upstream.type - override fun filter(filter: FilterQuery.() -> Unit): MongoCollection = + override fun filter(filter: FilterQuery.() -> Unit): CoroutineMongoCollection = upstream.filter { globalFilter() filter() diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt index 6e2e1f59..8057bd2c 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt @@ -93,4 +93,6 @@ interface CoroutineMongoCollection : MongoCollection { ): UpsertResult override fun aggregate(): CoroutineMongoAggregationPipeline + + override fun filter(filter: FilterQuery.() -> Unit): CoroutineMongoCollection } -- 2.51.2 From 236c03b2dc41b04fb710c79701162f693abf9622 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 23:30:48 +0200 Subject: [PATCH 04/10] feat(driver-sync): Implement the unified API --- driver-sync/build.gradle.kts | 1 + .../src/jvmMain/kotlin/JvmMongoCollection.kt | 4 +- .../src/jvmMain/kotlin/JvmMongoIterable.kt | 6 +- .../kotlin/SyncFilteredMongoCollectionImpl.kt | 248 ++++++++ .../kotlin/SyncMongoAggregationPipeline.kt | 92 +++ .../SyncMongoAggregationPipelineImpl.kt | 174 ++++++ .../src/jvmMain/kotlin/SyncMongoClient.kt | 71 +++ .../src/jvmMain/kotlin/SyncMongoClientImpl.kt | 87 +++ .../src/jvmMain/kotlin/SyncMongoCollection.kt | 102 ++++ .../jvmMain/kotlin/SyncMongoCollectionImpl.kt | 556 ++++++++++++++++++ .../src/jvmMain/kotlin/SyncMongoDatabase.kt | 77 +++ .../jvmMain/kotlin/SyncMongoDatabaseImpl.kt | 69 +++ .../src/jvmMain/kotlin/SyncMongoIterable.kt | 67 +++ .../kotlin/SyncMongoIterableImpl.aggregate.kt | 45 ++ .../kotlin/SyncMongoIterableImpl.find.kt | 55 ++ 15 files changed, 1649 insertions(+), 5 deletions(-) create mode 100644 driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index 9b75e6e1..a450d80d 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.dsl) + api(projects.driverSyncApi) api(projects.driverSharedOfficial) } diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 50b8ca74..88fb6393 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -56,7 +56,7 @@ import kotlin.reflect.typeOf * * To access the inner iterable, see [asKotlinClient]. * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongo]. + * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. */ class JvmMongoCollection internal constructor( inner: com.mongodb.kotlin.client.MongoCollection, @@ -404,7 +404,7 @@ class JvmMongoCollection internal constructor( inner.aggregate( pipeline = pipeline.chain.toBsonList().map { it.toJava() }, resultClass = documentType, - ).asKtMongo() + ).asKtMongoLegacy() } ) diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt index 6aee5243..4468244a 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025, OpenSavvy and contributors. + * Copyright (c) 2024-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. @@ -29,7 +29,7 @@ import java.util.stream.StreamSupport * * To access the inner iterable, see [asKotlinMongoIterable]. * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongo]. + * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. */ class JvmMongoIterable internal constructor( private val inner: com.mongodb.kotlin.client.MongoIterable, @@ -99,5 +99,5 @@ class JvmMongoIterable internal constructor( * Converts a [MongoDB MongoIterable][com.mongodb.kotlin.client.MongoIterable] into a * [KtMongo MongoIterable][JvmMongoIterable]. */ -fun com.mongodb.kotlin.client.MongoIterable.asKtMongo(): JvmMongoIterable = +fun com.mongodb.kotlin.client.MongoIterable.asKtMongoLegacy(): JvmMongoIterable = JvmMongoIterable(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt new file mode 100644 index 00000000..70f8e586 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt @@ -0,0 +1,248 @@ +/* + * 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 + +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.sync.api.operations.UpdateOperations +import kotlin.reflect.KType + +private class SyncFilteredMongoCollectionImpl( + private val upstream: SyncMongoCollection, + private val globalFilter: FilterQuery.() -> Unit, +) : SyncMongoCollection { + + override val name: String + get() = upstream.name + + override val fullyQualifiedName: String + get() = upstream.fullyQualifiedName + + override val propertyNameStrategy: PropertyNameStrategy + get() = upstream.propertyNameStrategy + + override val objectIdGenerator: ObjectIdGenerator + get() = upstream.objectIdGenerator + + @LowLevelApi + override val type: KType + get() = upstream.type + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection = + upstream.filter { + globalFilter() + filter() + } + + override fun asOfficial(): com.mongodb.kotlin.client.MongoCollection = + upstream.asOfficial() + + override val factory: BsonFactory + get() = upstream.factory + + override fun upsertOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpsertQuery.() -> Unit): SyncMongoCollection.UpsertResult = + upstream.upsertOne( + options = options, + filter = { + globalFilter() + filter() + }, + update = update + ) + + override fun upsertOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): SyncMongoCollection.UpsertResult = + upstream.upsertOneWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update + ) + + override fun aggregate(): SyncMongoAggregationPipeline = + upstream.aggregate() + .match { globalFilter() } + + @LowLevelApi + override val context: BsonContext + get() = upstream.context + + override fun drop(options: DropOptions.() -> Unit) = + upstream.drop(options) + + override fun count(): Long = + upstream.count { + globalFilter() + } + + override fun count(options: CountOptions.() -> Unit, predicate: FilterQuery.() -> Unit): Long = + upstream.count( + options = options, + predicate = { + globalFilter() + predicate() + } + ) + + override fun countEstimated(): Long = + count() + + override fun deleteOne(options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit) = + upstream.deleteOne( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun deleteMany(options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit) = + upstream.deleteMany( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun find(): SyncMongoFindIterable = + upstream.find { globalFilter() } + + override fun find(options: FindOptions.() -> Unit, filter: FilterQuery.() -> Unit): SyncMongoFindIterable = + upstream.find( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) = + upstream.insertOne( + document = document, + options = options, + ) + + override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) = + upstream.insertMany( + documents = documents, + options = options, + ) + + override fun updateMany(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateMany( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun updateOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateOne( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun replaceOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = + upstream.replaceOne( + options = options, + filter = { + globalFilter() + filter() + }, + document = document, + ) + + override fun repsertOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = + upstream.repsertOne( + options = options, + filter = { + globalFilter() + filter() + }, + document = document, + ) + + override fun findOneAndUpdate(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): Document? = + upstream.findOneAndUpdate( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun bulkWrite(options: BulkWriteOptions.() -> Unit, filter: FilterQuery.() -> Unit, operations: BulkWrite.() -> Unit) = + upstream.bulkWrite( + options = options, + filter = { + globalFilter() + filter() + }, + operations = operations, + ) + + override fun updateManyWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateManyWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun updateOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateOneWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + @OptIn(LowLevelApi::class) + override fun toString(): String { + val filter = FilterQuery(context) + globalFilter(filter) + + return "$upstream.filter($filter)" + } +} + +internal fun createFilteredCollection( + upstream: SyncMongoCollection, + globalFilter: FilterQuery.() -> Unit, +): SyncMongoCollection = + SyncFilteredMongoCollectionImpl(upstream, globalFilter) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt new file mode 100644 index 00000000..b4b80feb --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt @@ -0,0 +1,92 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.aggregation.stages.* +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.sync.api.MongoAggregationPipeline +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * An aggregation pipeline built on top of the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * To start a pipeline, call [MongoCollection.aggregate][SyncMongoCollection.aggregate]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/) + */ +interface SyncMongoAggregationPipeline : MongoAggregationPipeline { + + @LowLevelApi + override fun asIterable(type: KType): SyncMongoAggregateIterable + + override fun limit(amount: Long): SyncMongoAggregationPipeline + + override fun limit(amount: Int): SyncMongoAggregationPipeline + + override fun match(filter: FilterQuery.() -> Unit): SyncMongoAggregationPipeline + + override fun matchExpr(filter: AggregationOperators.() -> Value): SyncMongoAggregationPipeline + + override fun sample(size: Int): SyncMongoAggregationPipeline + + override fun set(block: SetStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun skip(amount: Long): SyncMongoAggregationPipeline + + override fun skip(amount: Int): SyncMongoAggregationPipeline + + override fun sort(block: SortOptionDsl.() -> Unit): SyncMongoAggregationPipeline + + override fun unset(block: UnsetStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun project(block: ProjectStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun unionWith(other: HasUnionWithCompatibility): SyncMongoAggregationPipeline + + override fun lookup(block: LookupStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun group(block: AccumulationOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun countTo(field: Field): SyncMongoAggregationPipeline + + override fun countTo(field: KProperty1): SyncMongoAggregationPipeline + +} + +/** + * 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. + */ +@OptIn(LowLevelApi::class) +inline fun SyncMongoAggregationPipeline.asIterable(): SyncMongoAggregateIterable = + this.asIterable(typeOf()) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt new file mode 100644 index 00000000..344548a9 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt @@ -0,0 +1,174 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.* +import opensavvy.ktmongo.dsl.aggregation.stages.* +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.official.toJava +import org.bson.conversions.Bson +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType + +private class SyncMongoAggregationPipelineImpl @OptIn(LowLevelApi::class) constructor( + private val collection: SyncMongoCollection<*>, + context: BsonContext, + chain: PipelineChainLink, + private val executeAggregate: (List, Class) -> SyncMongoAggregateIterable, +) : AbstractPipeline(context, chain), + AggregationPipeline, + SyncMongoAggregationPipeline { + + // region Execution + + @LowLevelApi + @Suppress("UNCHECKED_CAST") + override fun asIterable(type: KType): SyncMongoAggregateIterable = + executeAggregate(chain.toBsonList().map { it.toJava() }, (type.classifier as KClass).java) + + // endregion + // region Pipeline + + @LowLevelApi + @DangerousMongoApi + override fun withStage(stage: BsonNode): SyncMongoAggregationPipelineImpl = + SyncMongoAggregationPipelineImpl(collection, context, chain.withStage(stage), executeAggregate) + + @Suppress("UNCHECKED_CAST") + @LowLevelApi + @DangerousMongoApi + override fun reinterpret(): SyncMongoAggregationPipelineImpl = + this as SyncMongoAggregationPipelineImpl + + // endregion + // region Stages + + @KtMongoDsl + override fun limit(amount: Long): SyncMongoAggregationPipelineImpl = + super.limit(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun limit(amount: Int): SyncMongoAggregationPipelineImpl = + super.limit(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun match(filter: FilterQuery.() -> Unit): SyncMongoAggregationPipelineImpl = + super.match(filter) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun matchExpr(filter: AggregationOperators.() -> Value): SyncMongoAggregationPipelineImpl = + super.matchExpr(filter) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun sample(size: Int): SyncMongoAggregationPipelineImpl = + super.sample(size) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun set(block: SetStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.set(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun skip(amount: Long): SyncMongoAggregationPipelineImpl = + super.skip(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun skip(amount: Int): SyncMongoAggregationPipelineImpl = + super.skip(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun sort(block: SortOptionDsl.() -> Unit): SyncMongoAggregationPipelineImpl = + super.sort(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun unset(block: UnsetStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.unset(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun project(block: ProjectStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.project(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun unionWith(other: HasUnionWithCompatibility): SyncMongoAggregationPipelineImpl = + super.unionWith(other) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun lookup(block: LookupStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.lookup(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun group(block: AccumulationOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.group(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun countTo(field: Field): SyncMongoAggregationPipelineImpl = + super.countTo(field) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun countTo(field: KProperty1): SyncMongoAggregationPipelineImpl = + super.countTo(field) as SyncMongoAggregationPipelineImpl + + // endregion + // region $unionWith support + + @OptIn(LowLevelApi::class) + override fun embedInUnionWith(writer: BsonFieldWriter) = with(writer) { + writeString("coll", collection.name) + writeArray("pipeline") { + this@SyncMongoAggregationPipelineImpl.writeTo(this) + } + } + + // endregion + // region $lookup support + + @LowLevelApi + override fun embedInLookup(writer: BsonFieldWriter) = with(writer) { + writeString("from", collection.name) + + if (chain.isNotEmpty()) { + writeArray("pipeline") { + this@SyncMongoAggregationPipelineImpl.writeTo(this) + } + } + } + + // endregion + + override fun toString(): String = + "$collection.aggregate(${super.toString()})" +} + +@LowLevelApi +internal fun SyncMongoAggregationPipeline( + collection: SyncMongoCollection<*>, + context: BsonContext, + chain: PipelineChainLink, + executeAggregate: (List, Class) -> SyncMongoAggregateIterable, +): SyncMongoAggregationPipeline = + SyncMongoAggregationPipelineImpl(collection, context, chain, executeAggregate) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt new file mode 100644 index 00000000..89c966a2 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoClient.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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.sync.api.MongoClient + +/** + * Entry-point to the KtMongo Coroutines driver. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * ### Organizing data + * + * Accessing MongoDB data happens in three steps: + * - [SyncMongoClient]: represents the connection to the MongoDB application, handles + * the lifecycle and the configuration. + * - [SyncMongoDatabase] (accessed with [SyncMongoClient.database]): each database groups data together. + * This allows deploying multiple applications (or the same application multiple times) + * without name collisions. + * - [SyncMongoCollection] (accessed with [SyncMongoDatabase.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 = SyncMongoClient("mongodb://localhost:27017") + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("The database contains ${users.count()} users.") + * } + * ``` + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoClient : MongoClient { + + /** + * Obtains the underlying MongoDB client from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoClient + + override fun database(name: String): SyncMongoDatabase +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt new file mode 100644 index 00000000..fece084d --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt @@ -0,0 +1,87 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.MongoClient + +private class SyncMongoClientImpl( + private val inner: MongoClient, +) : SyncMongoClient { + + override fun asOfficial(): MongoClient = + inner + + override fun database(name: String): SyncMongoDatabase = + inner.getDatabase(name).asKtMongo() + + override fun close() { + inner.close() + } + + override fun toString(): String = + "SyncMongoClient()" +} + +/** + * Instantiates a KtMongo [SyncMongoClient] using an existing client from the official Kotlin driver. + * + * This method allows taking advantage of the full configuration power of the official client. + * + * ### Example + * + * ```kotlin + * fun main() = runBlocking { + * val client = SyncMongoClient() + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun SyncMongoClient( + connectionString: String = "mongodb://localhost:27017", +): SyncMongoClient = + SyncMongoClientImpl(MongoClient.create(connectionString)) + +/** + * Instantiates a KtMongo [SyncMongoClient] using an existing client from the official Kotlin driver. + * + * This method allows taking advantage of the full configuration power of the official client. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * .asKtMongo() + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoClient.asKtMongo(): SyncMongoClient = + SyncMongoClientImpl(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt new file mode 100644 index 00000000..34e11762 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt @@ -0,0 +1,102 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import opensavvy.ktmongo.dsl.command.FindOptions +import opensavvy.ktmongo.dsl.command.UpdateOptions +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.sync.api.MongoCollection +import opensavvy.ktmongo.sync.api.operations.UpdateOperations + +/** + * A collection stores related documents together. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * 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. + * + * ### 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) + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoCollection : MongoCollection { + + /** + * Obtains the underlying MongoDB collection from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoCollection + + override val factory: BsonFactory + + /** + * The return value of [upsertOne] and [upsertOneWithPipeline]. + */ + interface UpsertResult : UpdateOperations.UpsertResult { + + override val upsertedId: BsonValue? + } + + @IgnorableReturnValue + override fun upsertOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpsertQuery.() -> Unit, + ): UpsertResult + + @IgnorableReturnValue + override fun upsertOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpsertResult + + override fun aggregate(): SyncMongoAggregationPipeline + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection + + override fun find(): SyncMongoFindIterable + + override fun find(options: FindOptions.() -> Unit, filter: FilterQuery.() -> Unit): SyncMongoFindIterable +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt new file mode 100644 index 00000000..342281b0 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt @@ -0,0 +1,556 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.client.model.DeleteOptions +import com.mongodb.client.model.FindOneAndUpdateOptions +import com.mongodb.kotlin.client.MongoCollection +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import opensavvy.ktmongo.bson.official.types.Jvm +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink +import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.options.ArrayFiltersOption +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.options.WriteConcernOption +import opensavvy.ktmongo.dsl.options.option +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.official.command.toJava +import opensavvy.ktmongo.official.options.* +import opensavvy.ktmongo.official.options.toJava +import opensavvy.ktmongo.official.toJava +import opensavvy.ktmongo.sync.api.operations.UpdateOperations +import java.util.concurrent.TimeUnit +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import com.mongodb.client.model.ReplaceOptions as MongoReplaceOptions +import com.mongodb.client.model.UpdateOptions as MongoUpdateOptions + +private class SyncMongoCollectionImpl( + inner: MongoCollection, + override val factory: BsonFactory, + override val propertyNameStrategy: PropertyNameStrategy, + override val objectIdGenerator: ObjectIdGenerator, + @property:LowLevelApi + override val type: KType, +) : SyncMongoCollection { + + private val inner = inner + .withCodecRegistry(factory.codecRegistry) + + override fun asOfficial(): MongoCollection = + inner + + override val name: String + get() = inner.namespace.collectionName + + override val fullyQualifiedName: String + get() = inner.namespace.fullName + + private inner class CoroutineBsonContext : BsonContext, + opensavvy.ktmongo.bson.BsonFactory by factory, + ObjectIdGenerator by objectIdGenerator, + PropertyNameStrategy by propertyNameStrategy + + @LowLevelApi + override val context: BsonContext = CoroutineBsonContext() + + // region Count + + override fun count(): Long = + inner.countDocuments() + + @OptIn(LowLevelApi::class) + override fun count( + options: CountOptions.() -> Unit, + predicate: FilterQuery.() -> Unit, + ): Long { + val model = Count(context) + + model.options.options() + model.filter.predicate() + + return inner.countDocuments( + factory.buildDocument(model.filter).raw, + model.options.toJava() + ) + } + + override fun countEstimated(): Long = + inner.estimatedDocumentCount() + + // endregion + // region Find + + override fun find(): SyncMongoFindIterable = + inner.find().asKtMongo(lazyStringRepresentation = { "$this.find({})" }) + + @OptIn(LowLevelApi::class) + override fun find( + options: FindOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ): SyncMongoFindIterable { + val model = Find(context) + + model.options.options() + model.filter.filter() + + return inner + .withReadConcern(model.options.readReadConcern()) + .withReadPreference(model.options.readReadPreference()) + .find(factory.buildDocument(model.filter).raw) + .limit(model.options.readLimit()) + .skip(model.options.readSkip()) + .maxTime(model.options.readMaxTimeMS().toLong(), TimeUnit.MILLISECONDS) + .sort(model.options.readSortDocument()) + .asKtMongo(lazyStringRepresentation = { "$this.find($model)" }) + } + + // endregion + // region Insert + + @OptIn(LowLevelApi::class) + override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) { + val model = InsertOne(context, document, type) + + model.options.options() + + inner.withWriteConcern(model.options).insertOne( + model.document, + com.mongodb.client.model.InsertOneOptions() + ) + } + + @OptIn(LowLevelApi::class) + override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) { + val model = InsertMany(context, documents.toList(), type) + + model.options.options() + + inner.withWriteConcern(model.options).insertMany( + model.documents, + com.mongodb.client.model.InsertManyOptions() + ) + } + + // endregion + // region Delete + + @OptIn(LowLevelApi::class) + override fun deleteOne( + options: DeleteOneOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ) { + val model = DeleteOne(context) + + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteOne( + filter = factory.buildDocument(model.filter).raw, + options = DeleteOptions() + ) + } + + @OptIn(LowLevelApi::class) + override fun deleteMany( + options: DeleteManyOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ) { + val model = DeleteMany(context) + + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteMany( + filter = factory.buildDocument(model.filter).raw, + options = DeleteOptions() + ) + } + + // endregion + // region Collection + + @OptIn(LowLevelApi::class) + override fun drop(options: DropOptions.() -> Unit) { + val model = Drop(context) + + model.options.options() + + inner.withWriteConcern(model.options).drop() + } + + // endregion + // region Update + + @OptIn(LowLevelApi::class) + override fun updateMany( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateMany(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateMany( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun updateOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun upsertOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpsertQuery.() -> Unit, + ): SyncMongoCollection.UpsertResult { + val model = UpsertOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .upsert(true) + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun replaceOne( + options: ReplaceOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + document: Document, + ) { + val model = ReplaceOne(context, document, type) + + model.options.options() + model.filter.filter() + + inner.withWriteConcern(model.options).replaceOne( + factory.buildDocument(model.filter).raw, + document, + MongoReplaceOptions(), + ) + } + + @OptIn(LowLevelApi::class) + override fun repsertOne( + options: ReplaceOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + document: Document, + ) { + val model = RepsertOne(context, document, type) + + model.options.options() + model.filter.filter() + + inner.withWriteConcern(model.options).replaceOne( + factory.buildDocument(model.filter).raw, + document, + MongoReplaceOptions().upsert(true), + ) + } + + @OptIn(LowLevelApi::class) + override fun findOneAndUpdate( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): Document? { + val model = UpdateOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + return inner.withWriteConcern(model.options).findOneAndUpdate( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + FindOneAndUpdateOptions(), + ) + } + + @OptIn(LowLevelApi::class) + override fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val model = BulkWrite(context, type, filter) + + model.options.options() + model.operations() + + inner.withWriteConcern(model.options).bulkWrite( + model.operations.map { it.toJava() }.toList(), + options = com.mongodb.client.model.BulkWriteOptions(), + ) + } + + // endregion + // region UpdatePipeline + + @OptIn(LowLevelApi::class) + override fun updateManyWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateManyWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateMany( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions(), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun updateOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateOneWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions(), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun upsertOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): SyncMongoCollection.UpsertResult { + val model = UpsertOneWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions().upsert(true), + ) + return CoroutineUpdateResult(result, factory) + } + + // endregion + // region Aggregation + + @OptIn(LowLevelApi::class) + override fun aggregate(): SyncMongoAggregationPipeline = + SyncMongoAggregationPipeline( + collection = this, + context = context, + chain = PipelineChainLink(context), + executeAggregate = { pipeline, documentClass -> + inner.aggregate(pipeline, documentClass) + .asKtMongo() + }, + ) + + // endregion + // region Filter + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection = + createFilteredCollection(this, filter) + + // endregion + + override fun toString(): String = + "SyncMongoCollection($fullyQualifiedName)" +} + +private class CoroutineUpdateResult( + private val inner: com.mongodb.client.result.UpdateResult, + private val factory: BsonFactory, +) : SyncMongoCollection.UpsertResult { + override val acknowledged: Boolean + get() = inner.wasAcknowledged() + + override val matchedCount: Long + get() = inner.matchedCount + + override val modifiedCount: Long + get() = inner.modifiedCount + + override val upsertedId: BsonValue? + get() = inner.upsertedId?.let { factory.readValue(it) } + + override val upsertedCount: Int + get() = if (inner.upsertedId == null) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CoroutineUpdateResult) return false + + if (inner != other.inner) return false + if (factory != other.factory) return false + + return true + } + + override fun hashCode(): Int { + var result = inner.hashCode() + result = 31 * result + factory.hashCode() + return result + } + + override fun toString(): String = + if (acknowledged) "UpdateResult(acknowledged=true, matchedCount=$matchedCount, modifiedCount=$modifiedCount, upsertedCount=$upsertedCount, upsertedId=$upsertedId)" + else "UpdateResult(acknowledged=false)" +} + +/** + * Instantiates a KtMongo [SyncMongoCollection] using an existing collection from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * val users = database.collection("users") + * .asKtMongo() + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoCollection.asKtMongo( + factory: BsonFactory = BsonFactory(this.codecRegistry), + propertyNameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, + objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Jvm(), + type: KType, +): SyncMongoCollection = + SyncMongoCollectionImpl( + inner = this, + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = type, + ) + +/** + * Instantiates a KtMongo [SyncMongoCollection] using an existing collection from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * val users = database.collection("users") + * .asKtMongo() + * + * println("Users: ${users.count()}") + * } + * ``` + */ +inline fun MongoCollection.asKtMongo( + factory: BsonFactory = BsonFactory(this.codecRegistry), + propertyNameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, + objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Jvm(), +): SyncMongoCollection = + asKtMongo( + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = typeOf(), + ) + +@LowLevelApi +private fun MongoCollection.withWriteConcern(option: WithWriteConcern): MongoCollection { + val concern = option.option()?.concern + ?: return this + + return this.withWriteConcern(concern.toJava()) +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt new file mode 100644 index 00000000..b8daeff2 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt @@ -0,0 +1,77 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.sync.api.MongoDatabase +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A grouping of collections with the same theme. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * ### 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/) + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoDatabase : MongoDatabase { + + /** + * Obtains the underlying MongoDB database from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoDatabase + + @LowLevelApi + override fun collection(name: String, type: KType): SyncMongoCollection + + /** + * 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") + final inline fun collection(name: String): SyncMongoCollection = + collection(name, type = typeOf()) + +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt new file mode 100644 index 00000000..f851b9e6 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt @@ -0,0 +1,69 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.MongoDatabase +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KClass +import kotlin.reflect.KType + +private class SyncMongoDatabaseImpl( + private val inner: MongoDatabase, +) : SyncMongoDatabase { + + override fun asOfficial(): MongoDatabase = + inner + + @LowLevelApi + @Suppress("UNCHECKED_CAST") + override fun collection(name: String, type: KType): SyncMongoCollection = + inner.getCollection(name, (type.classifier as KClass).java) + .asKtMongo(type = type) + + override val name: String + get() = inner.name + + override fun toString(): String = + "SyncMongoDatabase($name)" +} + +/** + * Instantiates a KtMongo [SyncMongoDatabase] using an existing client from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * .asKtMongo() + * + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoDatabase.asKtMongo(): SyncMongoDatabase = + SyncMongoDatabaseImpl( + inner = this, + ) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt new file mode 100644 index 00000000..c9b0a875 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt @@ -0,0 +1,67 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.AggregateIterable +import com.mongodb.kotlin.client.FindIterable + +/** + * Streaming-capable iterable cursor to read data from the database. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * This type wraps a [FindFlow] from the official driver. + * See also [SyncMongoAggregateIterable]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface SyncMongoFindIterable : opensavvy.ktmongo.sync.api.MongoIterable { + + /** + * Obtains the underlying MongoDB flow from the official Kotlin driver. + */ + fun asOfficial(): FindIterable + +} + +/** + * Streaming-capable iterable cursor to read data from the database. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * This type wraps a [AggregateFlow] from the official driver. + * See also [SyncMongoFindIterable]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface SyncMongoAggregateIterable : opensavvy.ktmongo.sync.api.MongoIterable { + + /** + * Obtains the underlying MongoDB flow from the official Kotlin driver. + */ + fun asOfficial(): AggregateIterable + +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt new file mode 100644 index 00000000..de7763cd --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt @@ -0,0 +1,45 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.AggregateIterable + +private class SyncMongoAggregateIterableImpl( + private val inner: AggregateIterable, +) : SyncMongoAggregateIterable { + + override fun asOfficial(): AggregateIterable = + inner + + override fun first(): Document = + inner.first() + + override fun firstOrNull(): Document? = + inner.firstOrNull() + + override fun forEach(action: (Document) -> Unit): Unit = + inner.forEach(action) +} + +/** + * Instantiates a KtMongo [SyncMongoAggregateIterable] using an existing flow from the official Kotlin driver. + */ +fun AggregateIterable.asKtMongo(): SyncMongoAggregateIterable = + SyncMongoAggregateIterableImpl(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt new file mode 100644 index 00000000..14e5454f --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt @@ -0,0 +1,55 @@ +/* + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.FindIterable + +private class SyncMongoFindIterableImpl( + private val inner: FindIterable, + private val lazyStringRepresentation: (() -> String)?, +) : SyncMongoFindIterable { + + override fun asOfficial(): FindIterable = + inner + + override fun first(): Document = + inner.first() + + override fun firstOrNull(): Document? = + inner.firstOrNull() + + override fun forEach(action: (Document) -> Unit): Unit = + inner.forEach(action) + + override fun toString(): String = lazyStringRepresentation?.invoke() + ?: super.toString() +} + +/** + * Instantiates a KtMongo [SyncMongoFindIterable] using an existing flow from the official Kotlin driver. + */ +fun FindIterable.asKtMongo(): SyncMongoFindIterable = + SyncMongoFindIterableImpl(this, lazyStringRepresentation = { "$this.asKtMongo()" }) + +// Same but allows customizing the toString() +internal fun FindIterable.asKtMongo( + lazyStringRepresentation: (() -> String)?, +): SyncMongoFindIterable = + SyncMongoFindIterableImpl(this, lazyStringRepresentation) -- 2.51.2 From aa0600546cbe469f83c4fbddea8c1047b34e6dc3 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 23:45:03 +0200 Subject: [PATCH 05/10] breaking(driver-sync): Remove the non-unified API --- driver-sync-java/src/main/kotlin/KtMongo.kt | 14 +- .../src/jvmMain/kotlin/KMongoExt.kt | 6 +- .../commonMain/kotlin/FilteredCollection.kt | 297 ----------- .../kotlin/MongoAggregationPipeline.kt | 140 ----- .../src/commonMain/kotlin/MongoCollection.kt | 56 -- .../src/commonMain/kotlin/MongoIterable.kt | 108 ---- .../operations/AggregationOperations.kt | 48 -- .../kotlin/operations/BaseOperations.kt | 26 - .../kotlin/operations/CollectionOperations.kt | 49 -- .../kotlin/operations/CountOperations.kt | 115 ----- .../kotlin/operations/DeleteOperations.kt | 78 --- .../kotlin/operations/FindOperations.kt | 97 ---- .../kotlin/operations/InsertOperations.kt | 120 ----- .../kotlin/operations/UpdateOperations.kt | 484 ------------------ .../operations/UpdatePipelineOperations.kt | 146 ------ driver-sync/src/jvmMain/kotlin/JvmExt.kt | 22 - .../src/jvmMain/kotlin/JvmMongoCollection.kt | 480 ----------------- .../src/jvmMain/kotlin/JvmMongoIterable.kt | 103 ---- 18 files changed, 14 insertions(+), 2375 deletions(-) delete mode 100644 driver-sync/src/commonMain/kotlin/FilteredCollection.kt delete mode 100644 driver-sync/src/commonMain/kotlin/MongoAggregationPipeline.kt delete mode 100644 driver-sync/src/commonMain/kotlin/MongoCollection.kt delete mode 100644 driver-sync/src/commonMain/kotlin/MongoIterable.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/AggregationOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/BaseOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/CollectionOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/CountOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/DeleteOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/FindOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/InsertOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt delete mode 100644 driver-sync/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt delete mode 100644 driver-sync/src/jvmMain/kotlin/JvmExt.kt delete mode 100644 driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt delete mode 100644 driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt diff --git a/driver-sync-java/src/main/kotlin/KtMongo.kt b/driver-sync-java/src/main/kotlin/KtMongo.kt index 0a2ffa69..60ba9a38 100644 --- a/driver-sync-java/src/main/kotlin/KtMongo.kt +++ b/driver-sync-java/src/main/kotlin/KtMongo.kt @@ -17,6 +17,9 @@ package opensavvy.ktmongo.sync import com.mongodb.client.MongoCollection +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.types.Jvm +import opensavvy.ktmongo.bson.types.ObjectIdGenerator import opensavvy.ktmongo.dsl.path.PropertyNameStrategy import kotlin.reflect.KClass import kotlin.reflect.KClassifier @@ -58,7 +61,7 @@ object KtMongo { driver: MongoCollection, documentType: KType, nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, - ): JvmMongoCollection = + ): SyncMongoCollection = from(com.mongodb.kotlin.client.MongoCollection(driver), documentType, nameStrategy) /** @@ -85,8 +88,13 @@ object KtMongo { driver: com.mongodb.kotlin.client.MongoCollection, documentType: KType, nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, - ): JvmMongoCollection = - driver.asKtMongo(nameStrategy, documentType) + ): SyncMongoCollection = + driver.asKtMongo( + propertyNameStrategy = nameStrategy, + type = documentType, + factory = BsonFactory(driver.codecRegistry), + objectIdGenerator = ObjectIdGenerator.Jvm(), + ) private val typeOfNull: KType = kotlin.reflect.typeOf() diff --git a/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt b/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt index 76c5022f..1728deb8 100644 --- a/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt +++ b/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt @@ -18,7 +18,7 @@ package opensavvy.ktmongo.sync.kmongo import com.mongodb.kotlin.client.MongoCollection import opensavvy.ktmongo.dsl.path.PropertyNameStrategy -import opensavvy.ktmongo.sync.JvmMongoCollection +import opensavvy.ktmongo.sync.SyncMongoCollection import opensavvy.ktmongo.sync.asKtMongo import opensavvy.ktmongo.utils.kmongo.KMongoNameStrategy @@ -27,5 +27,5 @@ import opensavvy.ktmongo.utils.kmongo.KMongoNameStrategy */ inline fun com.mongodb.client.MongoCollection.asKtMongo( nameStrategy: PropertyNameStrategy = KMongoNameStrategy(), -): JvmMongoCollection = - MongoCollection(this).asKtMongo(nameStrategy = nameStrategy) +): SyncMongoCollection = + MongoCollection(this).asKtMongo(propertyNameStrategy = nameStrategy) diff --git a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt b/driver-sync/src/commonMain/kotlin/FilteredCollection.kt deleted file mode 100644 index b497a843..00000000 --- a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (c) 2024-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 - -import opensavvy.ktmongo.bson.types.ObjectId -import opensavvy.ktmongo.dsl.BsonContext -import opensavvy.ktmongo.dsl.LowLevelApi -import opensavvy.ktmongo.dsl.command.* -import opensavvy.ktmongo.dsl.query.FilterQuery -import opensavvy.ktmongo.dsl.query.UpdateQuery -import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery -import opensavvy.ktmongo.dsl.query.UpsertQuery -import opensavvy.ktmongo.sync.operations.UpdateOperations.UpdateResult -import opensavvy.ktmongo.sync.operations.UpdateOperations.UpsertResult - -private class FilteredCollection( - private val upstream: MongoCollection, - private val globalFilter: FilterQuery.() -> Unit, -) : MongoCollection { - - override fun find(): MongoIterable = - upstream.find(filter = globalFilter) - - override fun find( - options: FindOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - ): MongoIterable = - upstream.find(options) { - globalFilter() - filter() - } - - @LowLevelApi - override val context: BsonContext - get() = upstream.context - - override fun count(): Long = - upstream.count(predicate = globalFilter) - - override fun count( - options: CountOptions.() -> Unit, - predicate: FilterQuery.() -> Unit, - ): Long = - upstream.count( - options = options, - predicate = { - globalFilter() - predicate() - } - ) - - override fun countEstimated(): Long = - count() - - override fun updateMany( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): UpdateResult { - return upstream.updateMany( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun updateManyWithPipeline( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpdateResult { - return upstream.updateManyWithPipeline( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun updateOne( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): UpdateResult { - return upstream.updateOne( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun updateOneWithPipeline( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpdateResult { - return upstream.updateOneWithPipeline( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun upsertOne( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpsertQuery.() -> Unit, - ): UpsertResult { - return upstream.upsertOne( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun replaceOne( - options: ReplaceOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - document: Document, - ) { - upstream.replaceOne( - options = options, - filter = { - globalFilter() - filter() - }, - document = document, - ) - } - - override fun repsertOne( - options: ReplaceOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - document: Document, - ) { - upstream.repsertOne( - options = options, - filter = { - globalFilter() - filter() - }, - document = document, - ) - } - - override fun upsertOneWithPipeline( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpsertResult { - return upstream.upsertOneWithPipeline( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - } - - override fun findOneAndUpdate( - options: UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): Document? = - upstream.findOneAndUpdate( - options = options, - filter = { - globalFilter() - filter() - }, - update = update, - ) - - override fun bulkWrite( - options: BulkWriteOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - operations: BulkWrite.() -> Unit, - ) = upstream.bulkWrite( - options = options, - filter = { - globalFilter() - filter() - }, - operations = operations, - ) - - override fun deleteOne(options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit) { - upstream.deleteOne( - options = options, - filter = { - globalFilter() - filter() - } - ) - } - - override fun deleteMany(options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit) { - upstream.deleteMany( - options = options, - filter = { - globalFilter() - filter() - } - ) - } - - override fun drop(options: DropOptions.() -> Unit) { - deleteMany( - options = { - }, - filter = { - globalFilter() - } - ) - } - - override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) = - upstream.insertOne(document, options) - - override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) = - upstream.insertMany(documents, options) - - override fun aggregate(): MongoAggregationPipeline = - upstream.aggregate().match(globalFilter) - - @OptIn(LowLevelApi::class) - override fun toString(): String { - val filter = FilterQuery(context) - .apply(globalFilter) - .let(context::buildDocument) - - return "$upstream.filter $filter" - } - - override fun newId(): ObjectId = - upstream.newId() -} - -/** - * Returns a filtered collection that only contains the elements that match [filter]. - * - * This function creates a logical view of the collection: by itself, this function does nothing, and MongoDB is never - * aware of the existence of this logical view. However, operations invoked on the returned collection will only affect - * elements from the original that match the [filter]. - * - * Unlike actual MongoDB views, which are read-only, collections returned by this function can also be used for write operations. - * - * ### Example - * - * A typical usage of this function is to reuse filters for multiple operations. - * For example, if you have a concept of logical deletion, this function can be used to hide deleted values. - * - * ```kotlin - * class Order( - * val id: String, - * val date: Instant, - * val deleted: Boolean, - * ) - * - * val allOrders = database.getCollection("orders").asKtMongo() - * val activeOrders = allOrders.filter { Order::deleted ne true } - * - * allOrders.find() // Returns all orders, deleted or not - * activeOrders.find() // Only returns orders that are not logically deleted - * ``` - */ -fun MongoCollection.filter(filter: FilterQuery.() -> Unit): MongoCollection = - FilteredCollection(this, filter) diff --git a/driver-sync/src/commonMain/kotlin/MongoAggregationPipeline.kt b/driver-sync/src/commonMain/kotlin/MongoAggregationPipeline.kt deleted file mode 100644 index d3c70bd6..00000000 --- a/driver-sync/src/commonMain/kotlin/MongoAggregationPipeline.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2025-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 - -import opensavvy.ktmongo.bson.BsonFieldWriter -import opensavvy.ktmongo.dsl.BsonContext -import opensavvy.ktmongo.dsl.DangerousMongoApi -import opensavvy.ktmongo.dsl.KtMongoDsl -import opensavvy.ktmongo.dsl.LowLevelApi -import opensavvy.ktmongo.dsl.aggregation.* -import opensavvy.ktmongo.dsl.aggregation.stages.* -import opensavvy.ktmongo.dsl.options.SortOptionDsl -import opensavvy.ktmongo.dsl.query.FilterQuery -import opensavvy.ktmongo.dsl.tree.BsonNode - -class MongoAggregationPipeline @OptIn(LowLevelApi::class) internal constructor( - private val collection: String, - context: BsonContext, - chain: PipelineChainLink, - private val iterableBuilder: (MongoAggregationPipeline<*>, Class) -> MongoIterable<*>, -) : AbstractPipeline(context, chain), AggregationPipeline, LazyMongoIterable { - - // region Pipeline methods - - @LowLevelApi - @DangerousMongoApi - override fun withStage(stage: BsonNode): MongoAggregationPipeline = - MongoAggregationPipeline(collection, context, chain.withStage(stage), iterableBuilder) - - @Suppress("UNCHECKED_CAST") // The type is phantom, the cast is guaranteed to succeed - @LowLevelApi - @DangerousMongoApi - override fun reinterpret(): MongoAggregationPipeline = - this as MongoAggregationPipeline - - // endregion - // region Lazy iterable - - @Suppress("UNCHECKED_CAST") - override fun asIterable(documentType: Class): MongoIterable = - iterableBuilder(this, documentType) as MongoIterable - - // endregion - // region Stages - - @KtMongoDsl - override fun limit(amount: Long): MongoAggregationPipeline = - super.limit(amount) as MongoAggregationPipeline - - @KtMongoDsl - override fun limit(amount: Int): MongoAggregationPipeline = - super.limit(amount) as MongoAggregationPipeline - - @KtMongoDsl - override fun match(filter: FilterQuery.() -> Unit): MongoAggregationPipeline = - super.match(filter) as MongoAggregationPipeline - - @KtMongoDsl - override fun matchExpr(filter: AggregationOperators.() -> Value): MongoAggregationPipeline = - super.matchExpr(filter) as MongoAggregationPipeline - - @KtMongoDsl - override fun sample(size: Int): MongoAggregationPipeline = - super.sample(size) as MongoAggregationPipeline - - @KtMongoDsl - override fun set(block: SetStageOperators.() -> Unit): MongoAggregationPipeline = - super.set(block) as MongoAggregationPipeline - - @KtMongoDsl - override fun skip(amount: Long): MongoAggregationPipeline = - super.skip(amount) as MongoAggregationPipeline - - @KtMongoDsl - override fun skip(amount: Int): MongoAggregationPipeline = - super.skip(amount) as MongoAggregationPipeline - - @KtMongoDsl - override fun sort(block: SortOptionDsl.() -> Unit): MongoAggregationPipeline = - super.sort(block) as MongoAggregationPipeline - - @KtMongoDsl - override fun unset(block: UnsetStageOperators.() -> Unit): MongoAggregationPipeline = - super.unset(block) as MongoAggregationPipeline - - @KtMongoDsl - override fun project(block: ProjectStageOperators.() -> Unit): MongoAggregationPipeline = - super.project(block) as MongoAggregationPipeline - - @KtMongoDsl - override fun lookup( - block: LookupStageOperators.() -> Unit, - ): MongoAggregationPipeline = - super.lookup(block) as MongoAggregationPipeline - - override fun unionWith(other: HasUnionWithCompatibility): MongoAggregationPipeline = - super.unionWith(other) as MongoAggregationPipeline - - // endregion - // region $lookup compatibility - - @LowLevelApi - override fun embedInLookup(writer: BsonFieldWriter): Unit = with(writer) { - writeString("from", collection) - - if (chain.isNotEmpty()) { - writeArray("pipeline") { - this@MongoAggregationPipeline.writeTo(this) - } - } - } - - // endregion - // region $unionWith compatibility - - @LowLevelApi - override fun embedInUnionWith(writer: BsonFieldWriter): Unit = with(writer) { - writeString("coll", collection) - writeArray("pipeline") { - this@MongoAggregationPipeline.writeTo(this) - } - } - - // endregion - -} diff --git a/driver-sync/src/commonMain/kotlin/MongoCollection.kt b/driver-sync/src/commonMain/kotlin/MongoCollection.kt deleted file mode 100644 index 2a41575b..00000000 --- a/driver-sync/src/commonMain/kotlin/MongoCollection.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2024-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 - -import opensavvy.ktmongo.bson.types.ObjectIdGenerator -import opensavvy.ktmongo.sync.operations.* - -/** - * Methods to interact with a MongoDB collection. - * - * ### Operations - * - * - [aggregate][AggregationOperations.aggregate] - * - [bulkWrite][UpdateOperations.bulkWrite] - * - [count][CountOperations.count] - * - [countEstimated][CountOperations.countEstimated] - * - [deleteOne][DeleteOperations.deleteOne] - * - [deleteMany][DeleteOperations.deleteMany] - * - [drop][CollectionOperations.drop] - * - [find][FindOperations.find] - * - [findOne][FindOperations.findOne] - * - [findOneAndUpdate][UpdateOperations.findOneAndUpdate] - * - [insertOne][InsertOperations.insertOne] - * - [insertMany][InsertOperations.insertMany] - * - [updateOne][UpdateOperations.updateOne] - * - [updateMany][UpdateOperations.updateMany] - * - [upsertOne][UpdateOperations.upsertOne] - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/tutorial/query-documents) - */ -interface MongoCollection : - ObjectIdGenerator, - FindOperations, - CountOperations, - UpdateOperations, - DeleteOperations, - CollectionOperations, - InsertOperations, - AggregationOperations, - UpdatePipelineOperations diff --git a/driver-sync/src/commonMain/kotlin/MongoIterable.kt b/driver-sync/src/commonMain/kotlin/MongoIterable.kt deleted file mode 100644 index 06f8a228..00000000 --- a/driver-sync/src/commonMain/kotlin/MongoIterable.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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 - -/** - * Streaming-ready iterable client to read data from the database. - */ -interface MongoIterable { - - // region Extract data - - /** - * Returns the first document found by this query, or throws an exception. - * - * @throws NoSuchElementException If this query returned no results. - * @see firstOrNull - */ - fun first(): Document = - firstOrNull() ?: throw NoSuchElementException("No element was returned by this query") - - /** - * Returns the first document found by this query, or `null` if none were found. - * - * @see first - */ - fun firstOrNull(): Document? - - /** - * Executes [action] for each document returned by this query. - * - * This method streams all returned elements into the [action] function. - * The entire response is not loaded at once into memory. - */ - fun forEach(action: (Document) -> Unit) - - // endregion - // region To another data structure - - /** - * Reads the entirety of this response into a [List]. - * - * Since lists are in-memory, this will load the entirety of the results of this query into memory. - * - * @see toSet - */ - fun toList(): List { - val list = ArrayList() - forEach { list.add(it) } - return list - } - - /** - * Reads the entirety of this response into a [Set]. - * - * Since sets are in-memory, this will load the entirety of the results of this query into memory. - * - * @see toList - */ - fun toSet(): Set { - val set = LinkedHashSet() - forEach { set.add(it) } - return 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, asStream (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, flows, or simply forEach instead.") - - @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, asStream (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, flows, or simply forEach instead.") - - // endregion -} - -interface LazyMongoIterable { - fun asIterable(documentType: Class): MongoIterable -} - -inline fun LazyMongoIterable.asIterable(): MongoIterable = - asIterable(Document::class.java) - -inline fun LazyMongoIterable.first(): Document = - asIterable().first() - -inline fun LazyMongoIterable.firstOrNull(): Document? = - asIterable().firstOrNull() - -inline fun LazyMongoIterable.forEach(noinline action: (Document) -> Unit): Unit = - asIterable().forEach(action) - -inline fun LazyMongoIterable.toList(): List = - asIterable().toList() - -inline fun LazyMongoIterable.toSet(): Set = - asIterable().toSet() diff --git a/driver-sync/src/commonMain/kotlin/operations/AggregationOperations.kt b/driver-sync/src/commonMain/kotlin/operations/AggregationOperations.kt deleted file mode 100644 index f89100af..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/AggregationOperations.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025, 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.operations - -import opensavvy.ktmongo.sync.MongoAggregationPipeline - -/** - * Interface grouping MongoDB operations relating to aggregation pipelines - */ -interface AggregationOperations : BaseOperations { - - /** - * Start an aggregation pipeline. - * - * ### 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/) - */ - fun aggregate(): MongoAggregationPipeline - -} diff --git a/driver-sync/src/commonMain/kotlin/operations/BaseOperations.kt b/driver-sync/src/commonMain/kotlin/operations/BaseOperations.kt deleted file mode 100644 index e7f9c9ce..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/BaseOperations.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.BsonContext -import opensavvy.ktmongo.dsl.LowLevelApi - -interface BaseOperations { - - @LowLevelApi - val context: BsonContext -} diff --git a/driver-sync/src/commonMain/kotlin/operations/CollectionOperations.kt b/driver-sync/src/commonMain/kotlin/operations/CollectionOperations.kt deleted file mode 100644 index 250e3a40..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/CollectionOperations.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.command.DropOptions -import opensavvy.ktmongo.sync.filter - -/** - * Interface grouping MongoDB operations relating to collection administration. - */ -interface CollectionOperations : BaseOperations { - - /** - * Removes an entire collection from the database. - * - * ### Example - * - * ```kotlin - * collection.drop() - * ``` - * - * ### Using with filtered collections - * - * When using [filtered collections][opensavvy.ktmongo.sync.MongoCollection.filter], all elements matching the filter - * are removed. Other documents are not impacted, and the collection is not deleted. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.drop/) - */ - fun drop( - options: DropOptions.() -> Unit = {} - ) - -} diff --git a/driver-sync/src/commonMain/kotlin/operations/CountOperations.kt b/driver-sync/src/commonMain/kotlin/operations/CountOperations.kt deleted file mode 100644 index 5e663f96..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/CountOperations.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.command.CountOptions -import opensavvy.ktmongo.dsl.query.FilterQuery - -/** - * Interface grouping MongoDB operations relating to counting documents. - */ -interface CountOperations : BaseOperations { - - /** - * Counts how many documents exist in the collection. - * - * ### External resources - * - * - [Official 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 - * } - * ``` - * - * ### External resources - * - * - [Official 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. - * - * 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. - * - * ### External resources - * - * - [Official 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/src/commonMain/kotlin/operations/DeleteOperations.kt b/driver-sync/src/commonMain/kotlin/operations/DeleteOperations.kt deleted file mode 100644 index afbd1f5a..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/DeleteOperations.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.command.DeleteManyOptions -import opensavvy.ktmongo.dsl.command.DeleteOneOptions -import opensavvy.ktmongo.dsl.query.FilterQuery - -/** - * Interface grouping MongoDB operations relating 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 - * - * - [Official 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 - * - * - [Official 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/src/commonMain/kotlin/operations/FindOperations.kt b/driver-sync/src/commonMain/kotlin/operations/FindOperations.kt deleted file mode 100644 index 2b2e0bff..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/FindOperations.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.command.FindOptions -import opensavvy.ktmongo.dsl.query.FilterQuery -import opensavvy.ktmongo.sync.MongoIterable - -/** - * Interface grouping MongoDB operations allowing to search for information. - */ -interface FindOperations : BaseOperations { - - /** - * Finds all documents in this collection. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.find/) - */ - 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 - * - * - [Official 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, and [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 - * } - * ``` - * - * @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/src/commonMain/kotlin/operations/InsertOperations.kt b/driver-sync/src/commonMain/kotlin/operations/InsertOperations.kt deleted file mode 100644 index ad57f123..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/InsertOperations.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2024-2025, 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.operations - -import opensavvy.ktmongo.dsl.command.InsertManyOptions -import opensavvy.ktmongo.dsl.command.InsertOneOptions -import opensavvy.ktmongo.sync.filter - -/** - * Interface grouping MongoDB operations relating to inserting new documents. - */ -interface InsertOperations : BaseOperations { - - /** - * Inserts a [document]. - * - * ### Example - * - * ```kotlin - * class User( - * val name: String, - * val age: Int, - * ) - * - * collection.insertOne(User(name = "Bob", age = 18)) - * ``` - * - * Note that `insertOne` ignores [filtered collection][opensavvy.ktmongo.sync.MongoCollection.filter]. - * That is, `insertOne` on a filtered collection behaves exactly the same as the same `insertOne` on the underlying - * real collection. - * - * ### External resources - * - * - [Official 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) - * ``` - * - * Note that `insertOne` ignores [filtered collection][opensavvy.ktmongo.sync.MongoCollection.filter]. - * That is, `insertOne` on a filtered collection behaves exactly the same as the same `insertOne` on the underlying - * real collection. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#insertone) - * - * @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) - * ) - * ``` - * - * Note that `insertOne` ignores [filtered collection][opensavvy.ktmongo.sync.MongoCollection.filter]. - * That is, `insertOne` on a filtered collection behaves exactly the same as the same `insertOne` on the underlying - * real collection. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#insertone) - * - * @see insertOne Insert a single document. - */ - fun insertMany( - vararg documents: Document, - options: InsertManyOptions.() -> Unit = {}, - ) { - insertMany(documents.asList(), options) - } - -} diff --git a/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt b/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt deleted file mode 100644 index c92ed12d..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (c) 2024-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.operations - -import opensavvy.ktmongo.bson.BsonValue -import opensavvy.ktmongo.dsl.LowLevelApi -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 -import opensavvy.ktmongo.sync.MongoCollection -import opensavvy.ktmongo.sync.filter - -/** - * Interface grouping MongoDB operations allowing to update existing information. - */ -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 - * }, - * ) - * ``` - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.updateMany { - * User::age set 15 - * } - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) - * - * @param filter Optional filter to select which documents are updated. - * If no filter is specified, all documents are updated. - * @see updateOne - */ - @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 - * }, - * ) - * ``` - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.updateOne { - * User::age set 15 - * } - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) - * - * @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. - */ - @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. - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.upsertOne { - * User::age set 15 - * } - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [The update operation](https://www.mongodb.com/docs/manual/reference/command/update/) - * - [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 - */ - @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) - * ) - * ``` - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.replaceOne(User("Patrick", 15)) - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) - * - * @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) - * ) - * ``` - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.repsertOne(User("Patrick", 15)) - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) - * - * @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 - * }, - * ) - * ``` - * - * ### Using filtered collections - * - * The following code is equivalent: - * ```kotlin - * collection.filter { - * User::name eq "Patrick" - * }.findOneAndUpdate { - * User::age set 15 - * } - * ``` - * - * To learn more, see [filter][MongoCollection.filter]. - * - * ### External resources - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/findAndModify/) - * - * @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]. - * - * ### Using filtered collections - * - * If we want all operations to use the same filter, we can declare it before calling - * the operation: - * ```kotlin - * collection.filter { - * User::isAlive eq true - * }.bulkWrite { - * updateOne(…) - * updateOne(…) - * updateMany(…) - * } - * ``` - * - * ### External resources - * - * - [Official 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]. - */ - @OptIn(LowLevelApi::class) - val upsertedId: BsonValue? - - /** - * The number of upserted documents. - * - * @throws UnsupportedOperationException If the update was not [acknowledged]. - */ - val upsertedCount: Int - } -} diff --git a/driver-sync/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt b/driver-sync/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt deleted file mode 100644 index 93f76f04..00000000 --- a/driver-sync/src/commonMain/kotlin/operations/UpdatePipelineOperations.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2025-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.operations - -import opensavvy.ktmongo.dsl.command.UpdateOptions -import opensavvy.ktmongo.dsl.query.FilterQuery -import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery -import opensavvy.ktmongo.sync.operations.UpdateOperations.UpdateResult -import opensavvy.ktmongo.sync.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 - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-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. - */ - @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 - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-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. - */ - @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 - * - * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-an-aggregation-pipeline) - * - * @see updateOneWithPipeline Do nothing if the document doesn't already exist. - */ - @IgnorableReturnValue - fun upsertOneWithPipeline( - options: UpdateOptions.() -> Unit = {}, - filter: FilterQuery.() -> Unit = {}, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpsertResult - -} diff --git a/driver-sync/src/jvmMain/kotlin/JvmExt.kt b/driver-sync/src/jvmMain/kotlin/JvmExt.kt deleted file mode 100644 index ac13f1d3..00000000 --- a/driver-sync/src/jvmMain/kotlin/JvmExt.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2025, 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 - -import org.bson.BsonDocument - -internal fun opensavvy.ktmongo.bson.BsonDocument.toJava(): BsonDocument = - (this as opensavvy.ktmongo.bson.official.BsonDocument).raw diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt deleted file mode 100644 index 88fb6393..00000000 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ /dev/null @@ -1,480 +0,0 @@ -/* - * Copyright (c) 2024-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 - -import com.mongodb.client.model.* -import com.mongodb.client.model.ReplaceOptions -import com.mongodb.client.model.UpdateOptions -import opensavvy.ktmongo.bson.BsonValue -import opensavvy.ktmongo.bson.official.BsonFactory -import opensavvy.ktmongo.bson.official.types.Jvm -import opensavvy.ktmongo.bson.types.ObjectId -import opensavvy.ktmongo.bson.types.ObjectIdGenerator -import opensavvy.ktmongo.dsl.LowLevelApi -import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink -import opensavvy.ktmongo.dsl.command.* -import opensavvy.ktmongo.dsl.command.BulkWriteOptions -import opensavvy.ktmongo.dsl.command.CountOptions -import opensavvy.ktmongo.dsl.command.InsertManyOptions -import opensavvy.ktmongo.dsl.command.InsertOneOptions -import opensavvy.ktmongo.dsl.options.ArrayFiltersOption -import opensavvy.ktmongo.dsl.options.WithWriteConcern -import opensavvy.ktmongo.dsl.options.WriteConcernOption -import opensavvy.ktmongo.dsl.options.option -import opensavvy.ktmongo.dsl.path.PropertyNameStrategy -import opensavvy.ktmongo.dsl.query.FilterQuery -import opensavvy.ktmongo.dsl.query.UpdateQuery -import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery -import opensavvy.ktmongo.dsl.query.UpsertQuery -import opensavvy.ktmongo.official.JvmBsonContext -import opensavvy.ktmongo.official.command.toJava -import opensavvy.ktmongo.official.options.* -import opensavvy.ktmongo.official.options.toJava -import opensavvy.ktmongo.official.toJava -import opensavvy.ktmongo.sync.operations.UpdateOperations.UpdateResult -import opensavvy.ktmongo.sync.operations.UpdateOperations.UpsertResult -import java.util.concurrent.TimeUnit -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -/** - * Implementation of [MongoCollection] based on [MongoDB's MongoCollection][com.mongodb.kotlin.client.MongoCollection]. - * - * To access the inner iterable, see [asKotlinClient]. - * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. - */ -class JvmMongoCollection internal constructor( - inner: com.mongodb.kotlin.client.MongoCollection, - nameStrategy: PropertyNameStrategy, - private val documentType: KType, -) : MongoCollection { - - @LowLevelApi - fun asKotlinClient() = inner - - @LowLevelApi - override val context = JvmBsonContext( - bsonFactory = BsonFactory(inner.codecRegistry), - objectIdGenerator = ObjectIdGenerator.Jvm(), - nameStrategy = nameStrategy, - ) - - @OptIn(LowLevelApi::class) - private val inner = inner.withCodecRegistry(context.bsonFactory.codecRegistry) - - @OptIn(LowLevelApi::class) - override fun newId(): ObjectId = - context.newId() - - // region Find - - override fun find(): JvmMongoIterable = - JvmMongoIterable(inner.find(), repr = { "$this.find()" }) - - @OptIn(LowLevelApi::class) - override fun find( - options: FindOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - ): JvmMongoIterable { - val model = Find(context) - - model.options.options() - model.filter.filter() - - return JvmMongoIterable( - inner.withReadConcern(model.options.readReadConcern()) - .withReadPreference(model.options.readReadPreference()) - .find(context.bsonFactory.buildDocument(model.filter).raw) - .limit(model.options.readLimit()) - .skip(model.options.readSkip()) - .maxTime(model.options.readMaxTimeMS().toLong(), TimeUnit.MILLISECONDS) - .sort(model.options.readSortDocument()), - repr = { "$this.find($model)" } - ) - } - - // endregion - // region Count - - override fun count(): Long = - inner.countDocuments() - - @OptIn(LowLevelApi::class) - override fun count( - options: CountOptions.() -> Unit, - predicate: FilterQuery.() -> Unit, - ): Long { - val model = Count(context) - - model.options.options() - model.filter.predicate() - - return inner.countDocuments( - context.bsonFactory.buildDocument(model.filter).raw, - model.options.toJava() - ) - } - - override fun countEstimated(): Long = - inner.estimatedDocumentCount() - - // endregion - // region Update - - @OptIn(LowLevelApi::class) - override fun updateMany( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): UpdateResult { - val model = UpdateMany(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner - .withWriteConcern(model.options) - .updateMany( - context.bsonFactory.buildDocument(model.filter).raw, - context.bsonFactory.buildDocument(model.update).raw, - UpdateOptions() - .arrayFilters(model.options.option()?.filters.orEmpty().map { context.bsonFactory.readDocument(it).raw }) - ) - return JvmUpdateResult(result, context) - } - - @OptIn(LowLevelApi::class) - override fun updateOne( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): UpdateResult { - val model = UpdateOne(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner - .withWriteConcern(model.options) - .updateOne( - context.bsonFactory.buildDocument(model.filter).raw, - context.bsonFactory.buildDocument(model.update).raw, - UpdateOptions() - .arrayFilters(model.options.option()?.filters.orEmpty().map { context.bsonFactory.readDocument(it).raw }) - ) - return JvmUpdateResult(result, context) - } - - @OptIn(LowLevelApi::class) - override fun upsertOne( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpsertQuery.() -> Unit, - ): UpsertResult { - val model = UpsertOne(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner - .withWriteConcern(model.options) - .updateOne( - context.bsonFactory.buildDocument(model.filter).raw, - context.bsonFactory.buildDocument(model.update).raw, - UpdateOptions() - .upsert(true) - .arrayFilters(model.options.option()?.filters.orEmpty().map { context.bsonFactory.readDocument(it).raw }) - ) - return JvmUpdateResult(result, context) - } - - @OptIn(LowLevelApi::class) - override fun replaceOne( - options: opensavvy.ktmongo.dsl.command.ReplaceOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - document: Document, - ) { - val model = ReplaceOne(context, document, documentType) - - model.options.options() - model.filter.filter() - - inner.withWriteConcern(model.options).replaceOne(context.bsonFactory.buildDocument(model.filter).raw, document, ReplaceOptions()) - } - - @OptIn(LowLevelApi::class) - override fun repsertOne( - options: opensavvy.ktmongo.dsl.command.ReplaceOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - document: Document, - ) { - val model = RepsertOne(context, document, documentType) - - model.options.options() - model.filter.filter() - - inner.withWriteConcern(model.options).replaceOne(context.bsonFactory.buildDocument(model.filter).raw, document, ReplaceOptions().upsert(true)) - } - - @OptIn(LowLevelApi::class) - override fun findOneAndUpdate( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateQuery.() -> Unit, - ): Document? { - val model = UpdateOne(context) - - model.options.options() - model.filter.filter() - model.update.update() - - return inner.withWriteConcern(model.options).findOneAndUpdate(context.bsonFactory.buildDocument(model.filter).raw, context.bsonFactory.buildDocument(model.update).raw, FindOneAndUpdateOptions()) - } - - @OptIn(LowLevelApi::class) - override fun bulkWrite( - options: BulkWriteOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - operations: BulkWrite.() -> Unit, - ) { - val model = BulkWrite(context, documentType, filter) - - model.options.options() - model.operations() - - inner.withWriteConcern(model.options).bulkWrite( - model.operations.map { it.toJava() }.toList(), - options = com.mongodb.client.model.BulkWriteOptions() - ) - } - - // endregion - // region Update with pipeline - - @OptIn(LowLevelApi::class) - override fun updateManyWithPipeline( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpdateResult { - val model = UpdateManyWithPipeline(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner.withWriteConcern(model.options).updateMany(context.bsonFactory.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) - return JvmUpdateResult(result, context) - } - - @OptIn(LowLevelApi::class) - override fun updateOneWithPipeline( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpdateResult { - val model = UpdateOneWithPipeline(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner.withWriteConcern(model.options).updateOne(context.bsonFactory.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) - return JvmUpdateResult(result, context) - } - - @OptIn(LowLevelApi::class) - override fun upsertOneWithPipeline( - options: opensavvy.ktmongo.dsl.command.UpdateOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - update: UpdateWithPipelineQuery.() -> Unit, - ): UpsertResult { - val model = UpsertOneWithPipeline(context) - - model.options.options() - model.filter.filter() - model.update.update() - - val result = inner.withWriteConcern(model.options).updateOne(context.bsonFactory.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions().upsert(true)) - return JvmUpdateResult(result, context) - } - - // endregion - // region Insert - - @OptIn(LowLevelApi::class) - override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) { - val model = InsertOne(context, document, documentType) - - model.options.options() - - inner.withWriteConcern(model.options).insertOne( - model.document, - com.mongodb.client.model.InsertOneOptions() - ) - } - - @OptIn(LowLevelApi::class) - override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) { - val model = InsertMany(context, documents.toList(), documentType) - - model.options.options() - - inner.withWriteConcern(model.options).insertMany( - model.documents, - com.mongodb.client.model.InsertManyOptions() - ) - } - - // endregion - // region Delete - - @OptIn(LowLevelApi::class) - override fun deleteOne( - options: DeleteOneOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - ) { - val model = DeleteOne(context) - - model.filter.filter() - model.options.options() - - inner.withWriteConcern(model.options).deleteOne( - filter = context.bsonFactory.buildDocument(model.filter).raw, - options = DeleteOptions() - ) - } - - @OptIn(LowLevelApi::class) - override fun deleteMany( - options: DeleteManyOptions.() -> Unit, - filter: FilterQuery.() -> Unit, - ) { - val model = DeleteMany(context) - - model.filter.filter() - model.options.options() - - inner.withWriteConcern(model.options).deleteOne( - filter = context.bsonFactory.buildDocument(model.filter).raw, - options = DeleteOptions() - ) - } - - // endregion - // region Collection administration - - @OptIn(LowLevelApi::class) - override fun drop(options: DropOptions.() -> Unit) { - val model = Drop(context) - - model.options.options() - - inner.withWriteConcern(model.options).drop(DropCollectionOptions()) - } - - // endregion - // region Aggregation - - @OptIn(LowLevelApi::class) - override fun aggregate(): MongoAggregationPipeline = - MongoAggregationPipeline( - collection = inner.namespace.collectionName, - context = context, - chain = PipelineChainLink(context), - iterableBuilder = { pipeline, documentType -> - inner.aggregate( - pipeline = pipeline.chain.toBsonList().map { it.toJava() }, - resultClass = documentType, - ).asKtMongoLegacy() - } - ) - - // endregion - - override fun toString(): String = - "MongoCollection(${inner.namespace})" - -} - -/** - * Converts a [MongoDB collection][com.mongodb.kotlin.client.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. - */ -fun com.mongodb.kotlin.client.MongoCollection.asKtMongo( - nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, - documentType: KType, -): JvmMongoCollection = - JvmMongoCollection(this, nameStrategy, documentType) - -/** - * Converts a [MongoDB collection][com.mongodb.kotlin.client.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. - */ -inline fun com.mongodb.kotlin.client.MongoCollection.asKtMongo( - nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, -): JvmMongoCollection = - asKtMongo(nameStrategy, typeOf()) - -@LowLevelApi -private fun com.mongodb.kotlin.client.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.MongoCollection { - val concern = option.option()?.concern - ?: return this - - return this.withWriteConcern(concern.toJava()) -} - -private class JvmUpdateResult( - private val inner: com.mongodb.client.result.UpdateResult, - private val context: JvmBsonContext, -) : UpsertResult { // The official driver doesn't differentiate between UpdateResult & UpsertResult - override val acknowledged: Boolean - get() = inner.wasAcknowledged() - override val matchedCount: Long - get() = inner.matchedCount - override val modifiedCount: Long - get() = inner.modifiedCount - - @OptIn(LowLevelApi::class) - override val upsertedId: BsonValue? - get() = inner.upsertedId?.let { context.bsonFactory.readValue(it) } - override val upsertedCount: Int - get() = if (inner.upsertedId == null) 0 else 1 - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is JvmUpdateResult) return false - - if (inner != other.inner) return false - if (context != other.context) return false - - return true - } - - override fun hashCode(): Int { - var result = inner.hashCode() - result = 31 * result + context.hashCode() - return result - } - - @OptIn(LowLevelApi::class) - override fun toString(): String = - if (acknowledged) "UpdateResult(acknowledged=true, matchedCount=$matchedCount, modifiedCount=$modifiedCount, upsertedCount=$upsertedCount, upsertedId=$upsertedId)" - else "UpdateResult(acknowledged=false)" -} diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt deleted file mode 100644 index 4468244a..00000000 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2024-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 - -import com.mongodb.kotlin.client.MongoCursor -import opensavvy.ktmongo.dsl.LowLevelApi -import java.util.* -import java.util.Spliterator.* -import java.util.function.Consumer -import java.util.stream.Stream -import java.util.stream.StreamSupport - -/** - * Implementation of [MongoIterable] based on [MongoDB's MongoIterable][com.mongodb.kotlin.client.MongoIterable]. - * - * To access the inner iterable, see [asKotlinMongoIterable]. - * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. - */ -class JvmMongoIterable internal constructor( - private val inner: com.mongodb.kotlin.client.MongoIterable, - private val repr: (() -> String)? = null, -) : MongoIterable { - - /** - * Converts a KtMongo [MongoIterable] into a [MongoDB MongoIterable][com.mongodb.kotlin.client.MongoIterable]. - */ - @LowLevelApi - fun asKotlinMongoIterable() = inner - - override fun first(): Document = - inner.first() - - override fun firstOrNull(): Document? = - inner.firstOrNull() - - override fun forEach(action: (Document) -> Unit) { - inner.forEach(action) - } - - override fun toList(): List = - inner.toList() - - /** - * Streams the results of this query into a Java [Stream]. - */ - fun asStream(): Stream { - val cursor = inner.cursor() - - return StreamSupport.stream( - /* spliterator = */ MongoSpliterator(cursor), - /* parallel = */ false, - ).onClose { cursor.close() } - } - - private class MongoSpliterator( - private val cursor: MongoCursor, - ) : Spliterator { - override fun tryAdvance(action: Consumer): Boolean { - if (cursor.hasNext()) { - action.accept(cursor.next()) - return true - } else { - return false - } - } - - override fun trySplit(): Spliterator? { - return null - } - - override fun estimateSize(): Long { - return Long.MAX_VALUE - } - - override fun characteristics(): Int = - ORDERED + NONNULL + IMMUTABLE - } - - override fun toString(): String = - repr?.invoke() ?: super.toString() -} - -/** - * Converts a [MongoDB MongoIterable][com.mongodb.kotlin.client.MongoIterable] into a - * [KtMongo MongoIterable][JvmMongoIterable]. - */ -fun com.mongodb.kotlin.client.MongoIterable.asKtMongoLegacy(): JvmMongoIterable = - JvmMongoIterable(this) -- 2.51.2 From d36b39b34c431f2b2a782ce277fae46ffa8bf795 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 23:54:07 +0200 Subject: [PATCH 06/10] breaking(driver-sync-java): Move into a subpackage --- driver-sync-java/src/main/kotlin/JavaField.kt | 4 ++-- driver-sync-java/src/main/kotlin/KtMongo.kt | 4 +++- driver-sync-java/src/main/kotlin/Query.kt | 4 ++-- .../opensavvy/ktmongo/sync/{ => java}/SimpleJavaTest.java | 6 +++--- .../java/opensavvy/ktmongo/sync/{ => java}/Utilisateur.java | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) rename driver-sync-java/src/test/java/opensavvy/ktmongo/sync/{ => java}/SimpleJavaTest.java (94%) rename driver-sync-java/src/test/java/opensavvy/ktmongo/sync/{ => java}/Utilisateur.java (89%) diff --git a/driver-sync-java/src/main/kotlin/JavaField.kt b/driver-sync-java/src/main/kotlin/JavaField.kt index 27f351fa..5d93a77d 100644 --- a/driver-sync-java/src/main/kotlin/JavaField.kt +++ b/driver-sync-java/src/main/kotlin/JavaField.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package opensavvy.ktmongo.sync +package opensavvy.ktmongo.sync.java import com.github.meanbeanlib.mirror.Executables import com.github.meanbeanlib.mirror.SerializableLambdas diff --git a/driver-sync-java/src/main/kotlin/KtMongo.kt b/driver-sync-java/src/main/kotlin/KtMongo.kt index 60ba9a38..3b7d0564 100644 --- a/driver-sync-java/src/main/kotlin/KtMongo.kt +++ b/driver-sync-java/src/main/kotlin/KtMongo.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package opensavvy.ktmongo.sync +package opensavvy.ktmongo.sync.java import com.mongodb.client.MongoCollection import opensavvy.ktmongo.bson.official.BsonFactory import opensavvy.ktmongo.bson.official.types.Jvm import opensavvy.ktmongo.bson.types.ObjectIdGenerator import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.sync.SyncMongoCollection +import opensavvy.ktmongo.sync.asKtMongo import kotlin.reflect.KClass import kotlin.reflect.KClassifier import kotlin.reflect.KType diff --git a/driver-sync-java/src/main/kotlin/Query.kt b/driver-sync-java/src/main/kotlin/Query.kt index f1054e42..1cda7348 100644 --- a/driver-sync-java/src/main/kotlin/Query.kt +++ b/driver-sync-java/src/main/kotlin/Query.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-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. @@ -16,7 +16,7 @@ @file:JvmName("Query") -package opensavvy.ktmongo.sync +package opensavvy.ktmongo.sync.java import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.SortOptionDsl diff --git a/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/SimpleJavaTest.java b/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/SimpleJavaTest.java similarity index 94% rename from driver-sync-java/src/test/java/opensavvy/ktmongo/sync/SimpleJavaTest.java rename to driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/SimpleJavaTest.java index e934b60c..bb4d298b 100644 --- a/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/SimpleJavaTest.java +++ b/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/SimpleJavaTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package opensavvy.ktmongo.sync; +package opensavvy.ktmongo.sync.java; import com.mongodb.MongoTimeoutException; import com.mongodb.client.MongoClient; @@ -23,8 +23,8 @@ import kotlin.Unit; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; -import static opensavvy.ktmongo.sync.Query.filter; -import static opensavvy.ktmongo.sync.Query.options; +import static opensavvy.ktmongo.sync.java.Query.filter; +import static opensavvy.ktmongo.sync.java.Query.options; public class SimpleJavaTest { diff --git a/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/Utilisateur.java b/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/Utilisateur.java similarity index 89% rename from driver-sync-java/src/test/java/opensavvy/ktmongo/sync/Utilisateur.java rename to driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/Utilisateur.java index c1fc06b9..075cabfc 100644 --- a/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/Utilisateur.java +++ b/driver-sync-java/src/test/java/opensavvy/ktmongo/sync/java/Utilisateur.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package opensavvy.ktmongo.sync; +package opensavvy.ktmongo.sync.java; import org.bson.codecs.pojo.annotations.BsonId; import org.bson.types.ObjectId; -- 2.51.2 From b5ea2da0c219a267fd4c096d42a96c123157ad3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Aug 2026 10:46:15 +0200 Subject: [PATCH 07/10] feat(driver-sync-java-adapter): Create Blocking versions that adapt the Sync API into the Coroutine API This module will not be published, and is only used internally to use the same code base to test both drivers. --- driver-sync-api-adapter/build.gradle.kts | 32 ++++ .../BlockingMongoAggregationPipeline.kt | 115 ++++++++++++ .../commonMain/kotlin/BlockingMongoClient.kt | 33 ++++ .../kotlin/BlockingMongoCollection.kt | 171 ++++++++++++++++++ .../kotlin/BlockingMongoDatabase.kt | 36 ++++ .../kotlin/BlockingMongoIterable.kt | 46 +++++ .../src/commonMain/kotlin/BlockingUtils.kt | 26 +++ settings.gradle.kts | 1 + 8 files changed, 460 insertions(+) create mode 100644 driver-sync-api-adapter/build.gradle.kts create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoAggregationPipeline.kt create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoClient.kt create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoCollection.kt create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoDatabase.kt create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoIterable.kt create mode 100644 driver-sync-api-adapter/src/commonMain/kotlin/BlockingUtils.kt diff --git a/driver-sync-api-adapter/build.gradle.kts b/driver-sync-api-adapter/build.gradle.kts new file mode 100644 index 00000000..15efe097 --- /dev/null +++ b/driver-sync-api-adapter/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024-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. + */ + +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.internal) + alias(libsCommon.plugins.kotlinx.serialization) + alias(libsCommon.plugins.testBalloon) +} + +kotlin { + jvm() + + sourceSets.commonMain.dependencies { + api(projects.driverApi) + api(projects.driverSyncApi) + implementation(libs.kotlinx.coroutines) + } +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoAggregationPipeline.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoAggregationPipeline.kt new file mode 100644 index 00000000..cdce5afb --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoAggregationPipeline.kt @@ -0,0 +1,115 @@ +/* + * 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.blocking + +import opensavvy.ktmongo.api.MongoAggregationPipeline +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +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 opensavvy.ktmongo.dsl.tree.BsonNode +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import opensavvy.ktmongo.sync.api.MongoAggregationPipeline as SyncMongoAggregationPipeline + +class BlockingMongoAggregationPipeline( + private val inner: SyncMongoAggregationPipeline, +) : MongoAggregationPipeline { + @LowLevelApi + override fun asIterable(type: KType): BlockingMongoIterable = + BlockingMongoIterable(inner.asIterable(type)) + + override fun limit(amount: Long): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.limit(amount)) + + override fun limit(amount: Int): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.limit(amount)) + + override fun match(filter: FilterQuery.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.match(filter)) + + override fun sample(size: Int): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.sample(size)) + + override fun set(block: SetStageOperators.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.set(block)) + + override fun skip(amount: Long): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.skip(amount)) + + override fun skip(amount: Int): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.skip(amount)) + + override fun sort(block: SortOptionDsl.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.sort(block)) + + override fun unset(block: UnsetStageOperators.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.unset(block)) + + override fun project(block: ProjectStageOperators.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.project(block)) + + override fun unionWith(other: HasUnionWithCompatibility): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.unionWith(other)) + + override fun group(block: AccumulationOperators.() -> Unit): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.group(block)) + + override fun countTo(field: Field): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.countTo(field)) + + override fun countTo(field: KProperty1): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.countTo(field)) + + @LowLevelApi + override val context: BsonContext + get() = inner.context + + @DangerousMongoApi + @LowLevelApi + override fun withStage(stage: BsonNode): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.withStage(stage) as SyncMongoAggregationPipeline) + + @DangerousMongoApi + @LowLevelApi + override fun reinterpret(): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.reinterpret() as SyncMongoAggregationPipeline) + + @LowLevelApi + override fun writeTo(writer: BsonValueWriter) = + inner.writeTo(writer) + + override fun toString(): String = + inner.toString() + + @LowLevelApi + override fun embedInLookup(writer: BsonFieldWriter) = + inner.embedInLookup(writer) + + @LowLevelApi + override fun embedInUnionWith(writer: BsonFieldWriter) = + inner.embedInUnionWith(writer) +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoClient.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoClient.kt new file mode 100644 index 00000000..27cfe528 --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoClient.kt @@ -0,0 +1,33 @@ +/* + * 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.blocking + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.sync.api.MongoClient as SyncMongoClient + +class BlockingMongoClient( + private val inner: SyncMongoClient, +) : MongoClient { + override fun database(name: String): BlockingMongoDatabase = + BlockingMongoDatabase(inner.database(name)) + + override fun close() = + inner.close() + + override fun toString(): String = + inner.toString() +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoCollection.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoCollection.kt new file mode 100644 index 00000000..205558b8 --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoCollection.kt @@ -0,0 +1,171 @@ +/* + * 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.blocking + +import opensavvy.ktmongo.api.MongoCollection +import opensavvy.ktmongo.api.MongoIterable +import opensavvy.ktmongo.api.operations.UpdateOperations +import opensavvy.ktmongo.bson.BsonFactory +import opensavvy.ktmongo.bson.BsonValue +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import kotlin.reflect.KType +import opensavvy.ktmongo.sync.api.MongoCollection as SyncMongoCollection + +class BlockingMongoCollection( + private val inner: SyncMongoCollection, +) : MongoCollection { + override val name: String + get() = inner.name + + override val fullyQualifiedName: String + get() = inner.fullyQualifiedName + + override val factory: BsonFactory + get() = inner.factory + + override val propertyNameStrategy: PropertyNameStrategy + get() = inner.propertyNameStrategy + + override val objectIdGenerator: ObjectIdGenerator + get() = inner.objectIdGenerator + + @LowLevelApi + override val type: KType + get() = inner.type + + override fun filter(filter: FilterQuery.() -> Unit): BlockingMongoCollection = + BlockingMongoCollection(inner.filter(filter)) + + override fun aggregate(): BlockingMongoAggregationPipeline = + BlockingMongoAggregationPipeline(inner.aggregate()) + + @LowLevelApi + override val context: BsonContext + get() = inner.context + + override suspend fun drop(options: DropOptions.() -> Unit) = wrapBlocking { + inner.drop(options) + } + + override suspend fun count(): Long = wrapBlocking { + inner.count() + } + + override suspend fun count(options: CountOptions.() -> Unit, predicate: FilterQuery.() -> Unit): Long = wrapBlocking { + inner.count(options, predicate) + } + + override suspend fun countEstimated(): Long = wrapBlocking { + inner.countEstimated() + } + + override suspend fun deleteOne(options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit) = wrapBlocking { + inner.deleteOne(options, filter) + } + + override suspend fun deleteMany(options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit) = wrapBlocking { + inner.deleteMany(options, filter) + } + + override fun find(): MongoIterable = + BlockingMongoIterable(inner.find()) + + override fun find(options: FindOptions.() -> Unit, filter: FilterQuery.() -> Unit): MongoIterable = + BlockingMongoIterable(inner.find(options, filter)) + + override suspend fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) = wrapBlocking { + inner.insertOne(document, options) + } + + override suspend fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) = wrapBlocking { + inner.insertMany(documents, options) + } + + override suspend fun updateMany(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): BlockingUpdateResult = wrapBlocking { + BlockingUpdateResult(inner.updateMany(options, filter, update)) + } + + override suspend fun updateOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): BlockingUpdateResult = wrapBlocking { + BlockingUpdateResult(inner.updateOne(options, filter, update)) + } + + override suspend fun upsertOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpsertQuery.() -> Unit): BlockingUpsertResult = wrapBlocking { + BlockingUpsertResult(inner.upsertOne(options, filter, update)) + } + + class BlockingUpdateResult( + private val inner: opensavvy.ktmongo.sync.api.operations.UpdateOperations.UpdateResult, + ) : UpdateOperations.UpdateResult { + override val acknowledged: Boolean + get() = inner.acknowledged + + override val matchedCount: Long + get() = inner.matchedCount + + override val modifiedCount: Long + get() = inner.modifiedCount + } + + class BlockingUpsertResult( + private val inner: opensavvy.ktmongo.sync.api.operations.UpdateOperations.UpsertResult, + ) : UpdateOperations.UpsertResult, UpdateOperations.UpdateResult by BlockingUpdateResult(inner) { + override val upsertedId: BsonValue? + get() = inner.upsertedId + + override val upsertedCount: Int + get() = inner.upsertedCount + } + + override suspend fun replaceOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = wrapBlocking { + inner.replaceOne(options, filter, document) + } + + override suspend fun repsertOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = wrapBlocking { + inner.repsertOne(options, filter, document) + } + + override suspend fun findOneAndUpdate(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): Document? = wrapBlocking { + inner.findOneAndUpdate(options, filter, update) + } + + override suspend fun bulkWrite(options: BulkWriteOptions.() -> Unit, filter: FilterQuery.() -> Unit, operations: BulkWrite.() -> Unit) = wrapBlocking { + inner.bulkWrite(options, filter, operations) + } + + override suspend fun updateManyWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): BlockingUpdateResult = wrapBlocking { + BlockingUpdateResult(inner.updateManyWithPipeline(options, filter, update)) + } + + override suspend fun updateOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): BlockingUpdateResult = wrapBlocking { + BlockingUpdateResult(inner.updateOneWithPipeline(options, filter, update)) + } + + override suspend fun upsertOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): BlockingUpsertResult = wrapBlocking { + BlockingUpsertResult(inner.upsertOneWithPipeline(options, filter, update)) + } + + override fun toString(): String = + inner.toString() +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoDatabase.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoDatabase.kt new file mode 100644 index 00000000..70d417d8 --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoDatabase.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.blocking + +import opensavvy.ktmongo.api.MongoDatabase +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KType +import opensavvy.ktmongo.sync.api.MongoDatabase as SyncMongoDatabase + +class BlockingMongoDatabase( + private val inner: SyncMongoDatabase, +) : MongoDatabase { + override val name: String + get() = inner.name + + @LowLevelApi + override fun collection(name: String, type: KType): BlockingMongoCollection = + BlockingMongoCollection(inner.collection(name, type)) + + override fun toString(): String = + inner.toString() +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoIterable.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoIterable.kt new file mode 100644 index 00000000..7f5e944f --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingMongoIterable.kt @@ -0,0 +1,46 @@ +/* + * 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.blocking + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import opensavvy.ktmongo.api.MongoIterable +import opensavvy.ktmongo.sync.api.MongoIterable as SyncMongoIterable + +class BlockingMongoIterable( + private val inner: SyncMongoIterable, +) : MongoIterable { + override suspend fun first(): Document = wrapBlocking { + inner.first() + } + + override suspend fun firstOrNull(): Document? = wrapBlocking { + inner.firstOrNull() + } + + override suspend fun forEach(action: suspend (Document) -> Unit) = + wrapBlocking { + asFlow().collect(action) + } + + override fun asFlow(): Flow = flow { + emitAll(inner.toList().asFlow()) + }.flowOn(Dispatchers.IO) + + override fun toString(): String = + inner.toString() +} diff --git a/driver-sync-api-adapter/src/commonMain/kotlin/BlockingUtils.kt b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingUtils.kt new file mode 100644 index 00000000..6f6a7a6a --- /dev/null +++ b/driver-sync-api-adapter/src/commonMain/kotlin/BlockingUtils.kt @@ -0,0 +1,26 @@ +/* + * 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.blocking + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal suspend fun wrapBlocking( + block: suspend () -> T, +): T = withContext(Dispatchers.IO) { + block() +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7cbe9b01..afb12fa4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -89,6 +89,7 @@ include( "driver-shared-official", "driver-shared-kmongo", "driver-sync-api", + "driver-sync-api-adapter", "driver-sync", "driver-sync-java", "driver-sync-kmongo", -- 2.51.2 From 85e3d76592acc253f33de9edde750518b5cc195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Aug 2026 11:00:43 +0200 Subject: [PATCH 08/10] test(driver-sync): Run the integration tests using KotlinX.Serialization --- settings.gradle.kts | 1 + test-sync-kotlinx/build.gradle.kts | 38 +++++++++++++++++++ .../jvmTest/kotlin/SyncMongoClient.kotlinx.kt | 33 ++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 test-sync-kotlinx/build.gradle.kts create mode 100644 test-sync-kotlinx/src/jvmTest/kotlin/SyncMongoClient.kotlinx.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index afb12fa4..aaa74bda 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -102,6 +102,7 @@ include( "test", "test-coroutines-kotlinx", "test-coroutines-reflection", + "test-sync-kotlinx", "docs:website", "gradle:templates:template-app", diff --git a/test-sync-kotlinx/build.gradle.kts b/test-sync-kotlinx/build.gradle.kts new file mode 100644 index 00000000..e3e1f42d --- /dev/null +++ b/test-sync-kotlinx/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-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. + */ + +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.internal) + alias(libsCommon.plugins.kotlinx.serialization) + alias(libsCommon.plugins.testBalloon) + id("org.jetbrains.kotlinx.kover") +} + +kotlin { + jvm() + + sourceSets.commonMain.dependencies { + api(projects.test) + } + + sourceSets.jvmTest.dependencies { + implementation(projects.driverSync) + implementation(projects.driverSyncApiAdapter) + implementation(libs.mongodb.kotlinx.serialization) + implementation(libsCommon.bundles.testBalloon) + } +} diff --git a/test-sync-kotlinx/src/jvmTest/kotlin/SyncMongoClient.kotlinx.kt b/test-sync-kotlinx/src/jvmTest/kotlin/SyncMongoClient.kotlinx.kt new file mode 100644 index 00000000..6fb90d80 --- /dev/null +++ b/test-sync-kotlinx/src/jvmTest/kotlin/SyncMongoClient.kotlinx.kt @@ -0,0 +1,33 @@ +/* + * 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.tests.sync.serialization + +import opensavvy.ktmongo.sync.SyncMongoClient +import opensavvy.ktmongo.sync.api.blocking.BlockingMongoClient +import opensavvy.ktmongo.tests.api.verifyClient +import opensavvy.prepared.runner.testballoon.preparedSuite + +val IntegrationTests by preparedSuite { + + verifyClient("SyncMongoClient (serialization-based)") { connectionString, _ -> + val client = SyncMongoClient(connectionString) + + // BlockingMongoClient is a wrapper that exposes SyncMongoClient with the unified coroutines API + BlockingMongoClient(client) + } + +} -- 2.51.2 From 3312374d39f7845cd542a1bacb750f2c7f91bb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Aug 2026 17:36:06 +0200 Subject: [PATCH 09/10] test(driver-sync): Run the integration tests using reflection --- settings.gradle.kts | 1 + test-sync-reflection/build.gradle.kts | 38 +++++++++++++++++++ .../kotlin/SyncMongoClient.reflection.kt | 33 ++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 test-sync-reflection/build.gradle.kts create mode 100644 test-sync-reflection/src/jvmTest/kotlin/SyncMongoClient.reflection.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index aaa74bda..6949c554 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -103,6 +103,7 @@ include( "test-coroutines-kotlinx", "test-coroutines-reflection", "test-sync-kotlinx", + "test-sync-reflection", "docs:website", "gradle:templates:template-app", diff --git a/test-sync-reflection/build.gradle.kts b/test-sync-reflection/build.gradle.kts new file mode 100644 index 00000000..c596a131 --- /dev/null +++ b/test-sync-reflection/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-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. + */ + +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.internal) + alias(libsCommon.plugins.kotlinx.serialization) + alias(libsCommon.plugins.testBalloon) + id("org.jetbrains.kotlinx.kover") +} + +kotlin { + jvm() + + sourceSets.commonMain.dependencies { + api(projects.test) + } + + sourceSets.jvmTest.dependencies { + implementation(projects.driverSync) + implementation(projects.driverSyncApiAdapter) + implementation(libs.mongodb.kotlin.reflection) + implementation(libsCommon.bundles.testBalloon) + } +} diff --git a/test-sync-reflection/src/jvmTest/kotlin/SyncMongoClient.reflection.kt b/test-sync-reflection/src/jvmTest/kotlin/SyncMongoClient.reflection.kt new file mode 100644 index 00000000..8770406c --- /dev/null +++ b/test-sync-reflection/src/jvmTest/kotlin/SyncMongoClient.reflection.kt @@ -0,0 +1,33 @@ +/* + * 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.tests.sync.reflection + +import opensavvy.ktmongo.sync.SyncMongoClient +import opensavvy.ktmongo.sync.api.blocking.BlockingMongoClient +import opensavvy.ktmongo.tests.api.verifyClient +import opensavvy.prepared.runner.testballoon.preparedSuite + +val IntegrationTests by preparedSuite { + + verifyClient("SyncMongoClient (reflection-based)") { connectionString, _ -> + val client = SyncMongoClient(connectionString) + + // BlockingMongoClient is a wrapper that exposes SyncMongoClient with the unified coroutines API + BlockingMongoClient(client) + } + +} -- 2.51.2 From f90ff13d07218a9ecd056556abbceaf5d593adb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Aug 2026 17:37:53 +0200 Subject: [PATCH 10/10] build(docker): Increase the maximum number of opened files Our test suite is quite heavy and MongoDB can crash under the default limits. --- docker/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5ed82703..9e759252 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,3 +8,5 @@ services: image: "mongo:7.0.31" ports: - "27017:27017" + ulimits: + nofile: 64000