From 012c30923476d626fb5beb25a339a1e22aeefc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 21 Jun 2026 18:12:18 +0200 Subject: [PATCH 01/18] build(driver-api): Create the driver-api module --- build.gradle.kts | 1 + docs/website/build.gradle.kts | 1 + driver-api/build.gradle.kts | 70 +++++++++++++++++++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 73 insertions(+) create mode 100644 driver-api/build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index 5c8dabb4..c775dcd8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { library(projects.bsonMultiplatform) library(projects.bsonTests) library(projects.dsl) + library(projects.driverApi) library(projects.driverSharedOfficial) library(projects.driverSharedKmongo) library(projects.driverSync) diff --git a/docs/website/build.gradle.kts b/docs/website/build.gradle.kts index 6c69946f..10906ebb 100644 --- a/docs/website/build.gradle.kts +++ b/docs/website/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { dokka(projects.bsonOfficial) dokka(projects.bsonMultiplatform) dokka(projects.dsl) + dokka(projects.driverApi) dokka(projects.driverSharedOfficial) dokka(projects.driverSharedKmongo) dokka(projects.driverSync) diff --git a/driver-api/build.gradle.kts b/driver-api/build.gradle.kts new file mode 100644 index 00000000..a633b574 --- /dev/null +++ b/driver-api/build.gradle.kts @@ -0,0 +1,70 @@ +/* + * 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) + implementation(libs.kotlinx.coroutines) + } + + sourceSets.commonTest.dependencies { + implementation(libsCommon.opensavvy.prepared.testBalloon) + implementation(libsCommon.kotlin.test) + } +} + +library { + name.set("KtMongo: MongoDB driver for Kotlin • 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 d94a10b4..ef48450b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,6 +85,7 @@ include( "dsl", "dsl-template", + "driver-api", "driver-shared-official", "driver-shared-kmongo", "driver-sync", -- 2.51.2 From d84afc4b94dabc4c352aa63edcd840c59d20b07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 21 Jun 2026 18:21:20 +0200 Subject: [PATCH 02/18] feat(driver-api): Add MongoClient, MongoDatabase and MongoCollection --- .../src/commonMain/kotlin/MongoClient.kt | 71 ++++++++++ .../src/commonMain/kotlin/MongoCollection.kt | 126 ++++++++++++++++++ .../src/commonMain/kotlin/MongoDatabase.kt | 95 +++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 driver-api/src/commonMain/kotlin/MongoClient.kt create mode 100644 driver-api/src/commonMain/kotlin/MongoCollection.kt create mode 100644 driver-api/src/commonMain/kotlin/MongoDatabase.kt diff --git a/driver-api/src/commonMain/kotlin/MongoClient.kt b/driver-api/src/commonMain/kotlin/MongoClient.kt new file mode 100644 index 00000000..3106b106 --- /dev/null +++ b/driver-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.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-api/src/commonMain/kotlin/MongoCollection.kt b/driver-api/src/commonMain/kotlin/MongoCollection.kt new file mode 100644 index 00000000..b143df74 --- /dev/null +++ b/driver-api/src/commonMain/kotlin/MongoCollection.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.api + +import opensavvy.ktmongo.bson.BsonFactory +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +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. + * + * ### 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 { + + /** + * THe name of this collection. + * + * The collection name must be unique within a single [database] (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 + + /** + * The full BSON configuration, used by the DSL to generate queries. + * + * For more information, see [BsonContext]. + */ + val context: BsonContext + + 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 + +} diff --git a/driver-api/src/commonMain/kotlin/MongoDatabase.kt b/driver-api/src/commonMain/kotlin/MongoDatabase.kt new file mode 100644 index 00000000..4bef561c --- /dev/null +++ b/driver-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.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()) + +} -- 2.51.2 From b2f8fdc6005ea0314f8543053f19f2722086ecad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 19:32:20 +0200 Subject: [PATCH 03/18] feat(driver-api): Add MongoIterable --- .../src/commonMain/kotlin/MongoIterable.kt | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 driver-api/src/commonMain/kotlin/MongoIterable.kt diff --git a/driver-api/src/commonMain/kotlin/MongoIterable.kt b/driver-api/src/commonMain/kotlin/MongoIterable.kt new file mode 100644 index 00000000..adf0eef0 --- /dev/null +++ b/driver-api/src/commonMain/kotlin/MongoIterable.kt @@ -0,0 +1,152 @@ +/* + * 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.api + +import kotlinx.coroutines.flow.Flow + +/** + * 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. + */ + suspend fun first(): Document + + /** + * Returns the first document found by this query, or returns `null`. + * + * @see first Throw an exception instead of returning `null`. + */ + suspend 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]. + * @see asFlow Stream all results in a [Flow]. + */ + suspend fun forEach(action: suspend (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]. + * @see asFlow Stream all results in a [Flow]. + */ + suspend 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]. + * @see asFlow Stream all results in a [Flow]. + */ + suspend fun toSet(): Set { + val set = HashSet() + forEach { set.add(it) } + return set + } + + /** + * Streams the results into a [Flow]. + * + * The flow is lazy: new elements are streamed in when the consumer requests them. + * + * 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 you intend to query a large number of batches and + * perform complex operations on them, we recommend using + * [buffer][kotlinx.coroutines.flow.buffer] with a low capacity, + * to reduce latency between two batches. + * + * @see forEach Execute an action for each result. + * @see toList Store all results in a [List]. + * @see toSet Store all results in a [Set]. + */ + fun asFlow(): Flow + + /** + * 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], [asFlow] or `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]. + * @see asFlow Stream all results in a [Flow]. + */ + @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, flows, 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], [asFlow] or `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]. + * @see asFlow Stream all results in a [Flow]. + */ + @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, flows, or simply forEach instead.") + +} -- 2.51.2 From 5ab9dae1269bdec92920d0e0ee610157ee135d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 19:01:03 +0200 Subject: [PATCH 04/18] feat(driver-coroutines): Add CoroutineMongoClient & co without implementation --- driver-coroutines/build.gradle.kts | 5 ++ .../jvmMain/kotlin/CoroutineMongoClient.kt | 66 +++++++++++++++++ .../kotlin/CoroutineMongoCollection.kt | 62 ++++++++++++++++ .../jvmMain/kotlin/CoroutineMongoDatabase.kt | 73 +++++++++++++++++++ .../jvmMain/kotlin/CoroutineMongoIterable.kt | 39 ++++++++++ 5 files changed, 245 insertions(+) create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt diff --git a/driver-coroutines/build.gradle.kts b/driver-coroutines/build.gradle.kts index 9f5f9435..453026bb 100644 --- a/driver-coroutines/build.gradle.kts +++ b/driver-coroutines/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { jvm() sourceSets.commonMain.dependencies { + api(projects.driverApi) api(projects.dsl) api(projects.driverSharedOfficial) api(libs.kotlinx.coroutines) @@ -37,6 +38,10 @@ kotlin { implementation(libsCommon.kotest.assertions) implementation(libsCommon.kotlin.test) } + + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } } library { diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt new file mode 100644 index 00000000..c7b4ae67 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt @@ -0,0 +1,66 @@ +/* + * 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.coroutines + +import opensavvy.ktmongo.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: + * - [CoroutineMongoClient]: represents the connection to the MongoDB application, handles + * the lifecycle and the configuration. + * - [CoroutineMongoDatabase] (accessed with [CoroutineMongoClient.database]): each database groups data together. + * This allows deploying multiple applications (or the same application multiple times) + * without name collisions. + * - [CoroutineMongoCollection] (accessed with [CoroutineMongoDatabase.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: CoroutineMongoClient = // TODO + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("The database contains ${users.count()} users.") + * } + * ``` + */ +interface CoroutineMongoClient : MongoClient { + + /** + * Obtains the underlying MongoDB client from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.coroutine.MongoClient + + override fun database(name: String): CoroutineMongoDatabase +} diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt new file mode 100644 index 00000000..a2fda928 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt @@ -0,0 +1,62 @@ +/* + * 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.coroutines + +import opensavvy.ktmongo.api.MongoCollection +import opensavvy.ktmongo.api.MongoDatabase +import opensavvy.ktmongo.bson.official.BsonFactory + +/** + * 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) + */ +interface CoroutineMongoCollection : MongoCollection { + + /** + * Obtains the underlying MongoDB collection from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.coroutine.MongoCollection + + override val factory: BsonFactory + +} diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt new file mode 100644 index 00000000..943960b9 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt @@ -0,0 +1,73 @@ +/* + * 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.coroutines + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.api.MongoDatabase +import opensavvy.ktmongo.dsl.LowLevelApi +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/) + */ +interface CoroutineMongoDatabase : MongoDatabase { + + /** + * Obtains the underlying MongoDB database from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.coroutine.MongoDatabase + + @LowLevelApi + override fun collection(name: String, type: KType): CoroutineMongoCollection + + /** + * 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): CoroutineMongoCollection = + collection(name, type = typeOf()) + +} diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt new file mode 100644 index 00000000..7a48b5d2 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt @@ -0,0 +1,39 @@ +/* + * 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.coroutines + +import com.mongodb.kotlin.client.coroutine.FindFlow +import opensavvy.ktmongo.api.MongoIterable + +/** + * 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/). + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface CoroutineMongoIterable : MongoIterable { + + /** + * Obtains the underlying MongoDB flow from the official Kotlin driver. + */ + fun asOfficial(): FindFlow + +} -- 2.51.2 From 5284d6838cd4ec2a7602c3f298bfb3165b999549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 19:54:16 +0200 Subject: [PATCH 05/18] feat(driver-coroutines): Implement CoroutineMongoClient --- .../jvmMain/kotlin/CoroutineMongoClient.kt | 7 +- .../kotlin/CoroutineMongoClientImpl.kt | 84 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt index c7b4ae67..521d0cb4 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClient.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +@file:JvmMultifileClass +@file:JvmName("KtMongo") + package opensavvy.ktmongo.coroutines import opensavvy.ktmongo.api.MongoClient @@ -46,7 +49,7 @@ import opensavvy.ktmongo.api.MongoClient * ) * * fun main() = runBlocking { - * val client: CoroutineMongoClient = // TODO + * val client = CoroutineMongoClient("mongodb://localhost:27017") * * val database = client.database("my-app") * val users = database.collection("users") @@ -54,6 +57,8 @@ import opensavvy.ktmongo.api.MongoClient * println("The database contains ${users.count()} users.") * } * ``` + * + * @see asKtMongo Convert an existing instance from the official Kotlin driver. */ interface CoroutineMongoClient : MongoClient { diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt new file mode 100644 index 00000000..3950a1bd --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt @@ -0,0 +1,84 @@ +/* + * 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.coroutines + +import com.mongodb.kotlin.client.coroutine.MongoClient + +private class CoroutineMongoClientImpl( + private val inner: MongoClient, +) : CoroutineMongoClient { + + override fun asOfficial(): MongoClient = + inner + + override fun database(name: String): CoroutineMongoDatabase { + TODO("Not yet implemented") + } + + override fun toString(): String = + "CoroutineMongoClient()" +} + +/** + * Instantiates a KtMongo [CoroutineMongoClient] 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 = CoroutineMongoClient() + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun CoroutineMongoClient( + connectionString: String = "mongodb://localhost:27017", +): CoroutineMongoClient = + CoroutineMongoClientImpl(MongoClient.create(connectionString)) + +/** + * Instantiates a KtMongo [CoroutineMongoClient] 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(): CoroutineMongoClient = + CoroutineMongoClientImpl(this) -- 2.51.2 From 9f4848d78311e09155d0011360f877485dbd4faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 20:08:48 +0200 Subject: [PATCH 06/18] feat(driver-coroutines): Implement CoroutineMongoDatabase --- .../kotlin/CoroutineMongoClientImpl.kt | 5 +- .../jvmMain/kotlin/CoroutineMongoDatabase.kt | 5 ++ .../kotlin/CoroutineMongoDatabaseImpl.kt | 67 +++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt index 3950a1bd..50fe50d7 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt @@ -28,9 +28,8 @@ private class CoroutineMongoClientImpl( override fun asOfficial(): MongoClient = inner - override fun database(name: String): CoroutineMongoDatabase { - TODO("Not yet implemented") - } + override fun database(name: String): CoroutineMongoDatabase = + inner.getDatabase(name).asKtMongo() override fun toString(): String = "CoroutineMongoClient()" diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt index 943960b9..ce74bb05 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabase.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +@file:JvmMultifileClass +@file:JvmName("KtMongo") + package opensavvy.ktmongo.coroutines import opensavvy.ktmongo.api.MongoClient @@ -46,6 +49,8 @@ import kotlin.reflect.typeOf * ### External resources * * - [Official documentation](https://www.mongodb.com/docs/manual/core/databases-and-collections/) + * + * @see asKtMongo Convert an existing instance from the official Kotlin driver. */ interface CoroutineMongoDatabase : MongoDatabase { diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt new file mode 100644 index 00000000..3bfe6f11 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.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.coroutines + +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KType + +private class CoroutineMongoDatabaseImpl( + private val inner: MongoDatabase, +) : CoroutineMongoDatabase { + + override fun asOfficial(): MongoDatabase = + inner + + @LowLevelApi + override fun collection(name: String, type: KType): CoroutineMongoCollection { + TODO("Not yet implemented") + } + + override val name: String + get() = inner.name + + override fun toString(): String = + "CoroutineMongoDatabase($name)" +} + +/** + * Instantiates a KtMongo [CoroutineMongoDatabase] 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(): CoroutineMongoDatabase = + CoroutineMongoDatabaseImpl( + inner = this, + ) -- 2.51.2 From a42436af569eb885c122fe6a01c35770bfcf5621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 20:26:31 +0200 Subject: [PATCH 07/18] feat(driver-coroutines): Implement CoroutineMongoCollection --- .../kotlin/CoroutineMongoCollection.kt | 5 + .../kotlin/CoroutineMongoCollectionImpl.kt | 121 ++++++++++++++++++ .../kotlin/CoroutineMongoDatabaseImpl.kt | 8 +- 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt index a2fda928..83ac0d7e 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollection.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +@file:JvmMultifileClass +@file:JvmName("KtMongo") + package opensavvy.ktmongo.coroutines import opensavvy.ktmongo.api.MongoCollection @@ -49,6 +52,8 @@ import opensavvy.ktmongo.bson.official.BsonFactory * * - [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 asKtMongo Convert an existing instance from the official Kotlin driver. */ interface CoroutineMongoCollection : MongoCollection { diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt new file mode 100644 index 00000000..accdcde1 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt @@ -0,0 +1,121 @@ +/* + * 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.coroutines + +import com.mongodb.kotlin.client.coroutine.MongoCollection +import opensavvy.ktmongo.bson.official.BsonFactory +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.path.PropertyNameStrategy +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +private class CoroutineMongoCollectionImpl( + private val inner: MongoCollection, + override val factory: BsonFactory, + override val propertyNameStrategy: PropertyNameStrategy, + override val objectIdGenerator: ObjectIdGenerator, + @property:LowLevelApi + override val type: KType, +) : CoroutineMongoCollection { + + 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 + + override val context: BsonContext = CoroutineBsonContext() + + override fun toString(): String = + "CoroutineMongoCollection($fullyQualifiedName)" +} + +/** + * Instantiates a KtMongo [CoroutineMongoCollection] 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, +): CoroutineMongoCollection = + CoroutineMongoCollectionImpl( + inner = this, + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = type, + ) + +/** + * Instantiates a KtMongo [CoroutineMongoCollection] 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(), +): CoroutineMongoCollection = + asKtMongo( + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = typeOf(), + ) diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt index 3bfe6f11..854ba10e 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoDatabaseImpl.kt @@ -21,6 +21,7 @@ package opensavvy.ktmongo.coroutines import com.mongodb.kotlin.client.coroutine.MongoDatabase import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KClass import kotlin.reflect.KType private class CoroutineMongoDatabaseImpl( @@ -31,9 +32,10 @@ private class CoroutineMongoDatabaseImpl( inner @LowLevelApi - override fun collection(name: String, type: KType): CoroutineMongoCollection { - TODO("Not yet implemented") - } + @Suppress("UNCHECKED_CAST") + override fun collection(name: String, type: KType): CoroutineMongoCollection = + inner.getCollection(name, (type.classifier as KClass).java) + .asKtMongo(type = type) override val name: String get() = inner.name -- 2.51.2 From 10be79e67e1e4fd3e0df3c55bac0f0ee5e2279e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 20:36:28 +0200 Subject: [PATCH 08/18] feat(driver-coroutines): Implement CoroutineMongoIterable --- .../src/jvmMain/kotlin/KMongoExt.kt | 4 +- .../jvmMain/kotlin/CoroutineMongoIterable.kt | 3 ++ .../kotlin/CoroutineMongoIterableImpl.kt | 51 +++++++++++++++++++ .../src/jvmMain/kotlin/JvmMongoCollection.kt | 8 +-- .../src/jvmMain/kotlin/JvmMongoIterable.kt | 6 +-- test/src/jvmMain/kotlin/TestDatabase.jvm.kt | 6 +-- 6 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterableImpl.kt diff --git a/driver-coroutines-kmongo/src/jvmMain/kotlin/KMongoExt.kt b/driver-coroutines-kmongo/src/jvmMain/kotlin/KMongoExt.kt index d9a13705..9c07e73b 100644 --- a/driver-coroutines-kmongo/src/jvmMain/kotlin/KMongoExt.kt +++ b/driver-coroutines-kmongo/src/jvmMain/kotlin/KMongoExt.kt @@ -18,7 +18,7 @@ package opensavvy.ktmongo.coroutines.kmongo import com.mongodb.reactivestreams.client.MongoCollection import opensavvy.ktmongo.coroutines.JvmMongoCollection -import opensavvy.ktmongo.coroutines.asKtMongo +import opensavvy.ktmongo.coroutines.asKtMongoLegacy import opensavvy.ktmongo.dsl.path.PropertyNameStrategy import opensavvy.ktmongo.utils.kmongo.KMongoNameStrategy @@ -28,4 +28,4 @@ import opensavvy.ktmongo.utils.kmongo.KMongoNameStrategy inline fun MongoCollection.asKtMongo( nameStrategy: PropertyNameStrategy = KMongoNameStrategy(), ): JvmMongoCollection = - com.mongodb.kotlin.client.coroutine.MongoCollection(this).asKtMongo(nameStrategy) + com.mongodb.kotlin.client.coroutine.MongoCollection(this).asKtMongoLegacy(nameStrategy) diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt index 7a48b5d2..c5836775 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterable.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +@file:JvmMultifileClass +@file:JvmName("KtMongo") + package opensavvy.ktmongo.coroutines import com.mongodb.kotlin.client.coroutine.FindFlow diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterableImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterableImpl.kt new file mode 100644 index 00000000..c003dfe1 --- /dev/null +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoIterableImpl.kt @@ -0,0 +1,51 @@ +/* + * 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.coroutines + +import com.mongodb.kotlin.client.coroutine.FindFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull + +private class CoroutineMongoIterableImpl( + private val inner: FindFlow, +) : CoroutineMongoIterable { + + override fun asOfficial(): FindFlow = + inner + + override suspend fun first(): Document = + inner.first() + + override suspend fun firstOrNull(): Document? = + inner.firstOrNull() + + override suspend fun forEach(action: suspend (Document) -> Unit): Unit = + inner.collect(action) + + override fun asFlow(): Flow = + inner +} + +/** + * Instantiates a KtMongo [CoroutineMongoIterable] using an existing flow from the official Kotlin driver. + */ +fun FindFlow.asKtMongo(): CoroutineMongoIterable = + CoroutineMongoIterableImpl(this) diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index 5eea0eb7..5b8b370a 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -58,7 +58,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.coroutine.MongoCollection, @@ -430,7 +430,7 @@ class JvmMongoCollection internal constructor( /** * Converts a [MongoDB collection][com.mongodb.kotlin.client.coroutine.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. */ -fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongo( +fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongoLegacy( nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, documentType: KType, ): JvmMongoCollection = @@ -439,10 +439,10 @@ fun com.mongodb.kotlin.client.coroutine.MongoCollection com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongo( +inline fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongoLegacy( nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, ): JvmMongoCollection = - asKtMongo(nameStrategy, typeOf()) + asKtMongoLegacy(nameStrategy, typeOf()) @LowLevelApi private fun com.mongodb.kotlin.client.coroutine.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.coroutine.MongoCollection { diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoIterable.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoIterable.kt index 7d1001ba..bc222436 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoIterable.kt +++ b/driver-coroutines/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. @@ -27,7 +27,7 @@ import opensavvy.ktmongo.dsl.LowLevelApi * * 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.coroutine.FindFlow, @@ -64,5 +64,5 @@ class JvmMongoIterable internal constructor( * Converts a [MongoDB FindFlow][com.mongodb.kotlin.client.coroutine.FindFlow] into a * [KtMongo MongoIterable][JvmMongoIterable]. */ -fun com.mongodb.kotlin.client.coroutine.FindFlow.asKtMongo(): JvmMongoIterable = +fun com.mongodb.kotlin.client.coroutine.FindFlow.asKtMongoLegacy(): JvmMongoIterable = JvmMongoIterable(this) diff --git a/test/src/jvmMain/kotlin/TestDatabase.jvm.kt b/test/src/jvmMain/kotlin/TestDatabase.jvm.kt index 02020458..690e9a6e 100644 --- a/test/src/jvmMain/kotlin/TestDatabase.jvm.kt +++ b/test/src/jvmMain/kotlin/TestDatabase.jvm.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. @@ -20,7 +20,7 @@ import com.mongodb.MongoTimeoutException import com.mongodb.kotlin.client.coroutine.MongoClient import kotlinx.coroutines.CoroutineName import opensavvy.ktmongo.coroutines.MongoCollection -import opensavvy.ktmongo.coroutines.asKtMongo +import opensavvy.ktmongo.coroutines.asKtMongoLegacy import opensavvy.prepared.suite.PreparedProvider import opensavvy.prepared.suite.prepared import opensavvy.prepared.suite.shared @@ -40,5 +40,5 @@ internal val database by shared(CoroutineName("mongodb-establish-connection")) { actual inline fun testCollectionExact(name: String): PreparedProvider> = prepared(CoroutineName("mongodb-create-collection-$name")) { val collection = database().getCollection(name) - collection.asKtMongo() + collection.asKtMongoLegacy() } -- 2.51.2 From afb9e8f554c7efa9d859083412b9dd98faf02cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 22 Jun 2026 20:40:11 +0200 Subject: [PATCH 09/18] test: Rename the integration tests module to :test-legacy The new :test module will only store the tests themselves, and a different module will execute the tests for the different dependencies we have. --- settings.gradle.kts | 2 +- {test => test-legacy}/README.md | 0 {test => test-legacy}/build.gradle.kts | 2 +- {test => test-legacy}/src/commonMain/kotlin/TestDatabase.kt | 2 +- {test => test-legacy}/src/commonTest/kotlin/AggregationTests.kt | 0 {test => test-legacy}/src/commonTest/kotlin/ArraysTest.kt | 0 .../src/commonTest/kotlin/BasicReadWriteTest.kt | 0 .../src/commonTest/kotlin/FilteredCollectionTest.kt | 2 +- {test => test-legacy}/src/commonTest/kotlin/MapsTest.kt | 2 +- {test => test-legacy}/src/commonTest/kotlin/SortTest.kt | 0 .../commonTest/kotlin/studies/AggregationProjectUnionWith.kt | 2 +- {test => test-legacy}/src/jsMain/kotlin/TestDatabase.js.kt | 2 +- {test => test-legacy}/src/jvmMain/kotlin/TestDatabase.jvm.kt | 0 13 files changed, 7 insertions(+), 7 deletions(-) rename {test => test-legacy}/README.md (100%) rename {test => test-legacy}/build.gradle.kts (95%) rename {test => test-legacy}/src/commonMain/kotlin/TestDatabase.kt (96%) rename {test => test-legacy}/src/commonTest/kotlin/AggregationTests.kt (100%) rename {test => test-legacy}/src/commonTest/kotlin/ArraysTest.kt (100%) rename {test => test-legacy}/src/commonTest/kotlin/BasicReadWriteTest.kt (100%) rename {test => test-legacy}/src/commonTest/kotlin/FilteredCollectionTest.kt (97%) rename {test => test-legacy}/src/commonTest/kotlin/MapsTest.kt (97%) rename {test => test-legacy}/src/commonTest/kotlin/SortTest.kt (100%) rename {test => test-legacy}/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt (99%) rename {test => test-legacy}/src/jsMain/kotlin/TestDatabase.js.kt (93%) rename {test => test-legacy}/src/jvmMain/kotlin/TestDatabase.jvm.kt (100%) diff --git a/settings.gradle.kts b/settings.gradle.kts index ef48450b..35dc1668 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -96,7 +96,7 @@ include( "driver-multiplatform-wire", "driver-multiplatform", - "test", + "test-legacy", "docs:website", "gradle:templates:template-app", diff --git a/test/README.md b/test-legacy/README.md similarity index 100% rename from test/README.md rename to test-legacy/README.md diff --git a/test/build.gradle.kts b/test-legacy/build.gradle.kts similarity index 95% rename from test/build.gradle.kts rename to test-legacy/build.gradle.kts index 94e559c7..7b075249 100644 --- a/test/build.gradle.kts +++ b/test-legacy/build.gradle.kts @@ -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. diff --git a/test/src/commonMain/kotlin/TestDatabase.kt b/test-legacy/src/commonMain/kotlin/TestDatabase.kt similarity index 96% rename from test/src/commonMain/kotlin/TestDatabase.kt rename to test-legacy/src/commonMain/kotlin/TestDatabase.kt index 390f450a..2028f6da 100644 --- a/test/src/commonMain/kotlin/TestDatabase.kt +++ b/test-legacy/src/commonMain/kotlin/TestDatabase.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. diff --git a/test/src/commonTest/kotlin/AggregationTests.kt b/test-legacy/src/commonTest/kotlin/AggregationTests.kt similarity index 100% rename from test/src/commonTest/kotlin/AggregationTests.kt rename to test-legacy/src/commonTest/kotlin/AggregationTests.kt diff --git a/test/src/commonTest/kotlin/ArraysTest.kt b/test-legacy/src/commonTest/kotlin/ArraysTest.kt similarity index 100% rename from test/src/commonTest/kotlin/ArraysTest.kt rename to test-legacy/src/commonTest/kotlin/ArraysTest.kt diff --git a/test/src/commonTest/kotlin/BasicReadWriteTest.kt b/test-legacy/src/commonTest/kotlin/BasicReadWriteTest.kt similarity index 100% rename from test/src/commonTest/kotlin/BasicReadWriteTest.kt rename to test-legacy/src/commonTest/kotlin/BasicReadWriteTest.kt diff --git a/test/src/commonTest/kotlin/FilteredCollectionTest.kt b/test-legacy/src/commonTest/kotlin/FilteredCollectionTest.kt similarity index 97% rename from test/src/commonTest/kotlin/FilteredCollectionTest.kt rename to test-legacy/src/commonTest/kotlin/FilteredCollectionTest.kt index 296131b7..77a15caf 100644 --- a/test/src/commonTest/kotlin/FilteredCollectionTest.kt +++ b/test-legacy/src/commonTest/kotlin/FilteredCollectionTest.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. diff --git a/test/src/commonTest/kotlin/MapsTest.kt b/test-legacy/src/commonTest/kotlin/MapsTest.kt similarity index 97% rename from test/src/commonTest/kotlin/MapsTest.kt rename to test-legacy/src/commonTest/kotlin/MapsTest.kt index f60e2378..eec0cc02 100644 --- a/test/src/commonTest/kotlin/MapsTest.kt +++ b/test-legacy/src/commonTest/kotlin/MapsTest.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. diff --git a/test/src/commonTest/kotlin/SortTest.kt b/test-legacy/src/commonTest/kotlin/SortTest.kt similarity index 100% rename from test/src/commonTest/kotlin/SortTest.kt rename to test-legacy/src/commonTest/kotlin/SortTest.kt diff --git a/test/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt b/test-legacy/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt similarity index 99% rename from test/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt rename to test-legacy/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt index f9d88b07..0706820c 100644 --- a/test/src/commonTest/kotlin/studies/AggregationProjectUnionWith.kt +++ b/test-legacy/src/commonTest/kotlin/studies/AggregationProjectUnionWith.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. diff --git a/test/src/jsMain/kotlin/TestDatabase.js.kt b/test-legacy/src/jsMain/kotlin/TestDatabase.js.kt similarity index 93% rename from test/src/jsMain/kotlin/TestDatabase.js.kt rename to test-legacy/src/jsMain/kotlin/TestDatabase.js.kt index 51d3e38b..ac221753 100644 --- a/test/src/jsMain/kotlin/TestDatabase.js.kt +++ b/test-legacy/src/jsMain/kotlin/TestDatabase.js.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 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. diff --git a/test/src/jvmMain/kotlin/TestDatabase.jvm.kt b/test-legacy/src/jvmMain/kotlin/TestDatabase.jvm.kt similarity index 100% rename from test/src/jvmMain/kotlin/TestDatabase.jvm.kt rename to test-legacy/src/jvmMain/kotlin/TestDatabase.jvm.kt -- 2.51.2 From 540bb79243bd2aba387c1313335c2fa112738c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 23 Jun 2026 22:14:41 +0200 Subject: [PATCH 10/18] test: Create the new test module --- .../kotlin/CoroutineMongoClientImpl.kt | 4 ++ settings.gradle.kts | 1 + {test-legacy => test}/README.md | 0 test/build.gradle.kts | 61 +++++++++++++++++ test/src/commonMain/kotlin/0_Client.kt | 37 ++++++++++ test/src/commonMain/kotlin/1_Database.kt | 34 ++++++++++ test/src/commonMain/kotlin/2_Collection.kt | 67 +++++++++++++++++++ test/src/commonMain/kotlin/Entrypoint.kt | 49 ++++++++++++++ test/src/commonTest/kotlin/Dummy.kt | 28 ++++++++ 9 files changed, 281 insertions(+) rename {test-legacy => test}/README.md (100%) create mode 100644 test/build.gradle.kts create mode 100644 test/src/commonMain/kotlin/0_Client.kt create mode 100644 test/src/commonMain/kotlin/1_Database.kt create mode 100644 test/src/commonMain/kotlin/2_Collection.kt create mode 100644 test/src/commonMain/kotlin/Entrypoint.kt create mode 100644 test/src/commonTest/kotlin/Dummy.kt diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt index 50fe50d7..0df7736b 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoClientImpl.kt @@ -31,6 +31,10 @@ private class CoroutineMongoClientImpl( override fun database(name: String): CoroutineMongoDatabase = inner.getDatabase(name).asKtMongo() + override fun close() { + inner.close() + } + override fun toString(): String = "CoroutineMongoClient()" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 35dc1668..56ff1fa9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -97,6 +97,7 @@ include( "driver-multiplatform", "test-legacy", + "test", "docs:website", "gradle:templates:template-app", diff --git a/test-legacy/README.md b/test/README.md similarity index 100% rename from test-legacy/README.md rename to test/README.md diff --git a/test/build.gradle.kts b/test/build.gradle.kts new file mode 100644 index 00000000..cacd8b78 --- /dev/null +++ b/test/build.gradle.kts @@ -0,0 +1,61 @@ +/* + * 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() + js { + nodejs() + } + // linuxX64() + // linuxArm64() + // macosX64() + // macosArm64() + // iosArm64() + // iosX64() + // iosSimulatorArm64() + // watchosX64() + // watchosArm32() + // watchosArm64() + // watchosSimulatorArm64() + // tvosX64() + // tvosArm64() + // tvosSimulatorArm64() + // mingwX64() + // wasmJs { + // nodejs() + // } + + sourceSets.commonMain.dependencies { + api(projects.driverApi) + + api(libsCommon.opensavvy.prepared.testBalloon) + implementation(libsCommon.bundles.testBalloon) + + implementation(libs.kotlinx.serialization) + } +} + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) +powerAssert { + includedSourceSets = listOf("commonMain", "commonTest", "jvmMain", "jsMain") +} diff --git a/test/src/commonMain/kotlin/0_Client.kt b/test/src/commonMain/kotlin/0_Client.kt new file mode 100644 index 00000000..2cb824be --- /dev/null +++ b/test/src/commonMain/kotlin/0_Client.kt @@ -0,0 +1,37 @@ +/* + * 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.api + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +fun SuiteDsl.verifyClient( + client: Prepared, +) = suite("Client") { + + test("Create") { + client() + + // Test is successful if no exception is thrown + } + + test("toString representation") { + check(client().toString().endsWith("MongoClient()")) + } + +} diff --git a/test/src/commonMain/kotlin/1_Database.kt b/test/src/commonMain/kotlin/1_Database.kt new file mode 100644 index 00000000..9c38eb3b --- /dev/null +++ b/test/src/commonMain/kotlin/1_Database.kt @@ -0,0 +1,34 @@ +/* + * 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.api + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +fun SuiteDsl.verifyDatabase( + client: Prepared, +) = suite("Database") { + + test("Create") { + val db = client().database("test") + + check(db.name == "test") + check(db.toString().endsWith("MongoDatabase(test)")) + } + +} diff --git a/test/src/commonMain/kotlin/2_Collection.kt b/test/src/commonMain/kotlin/2_Collection.kt new file mode 100644 index 00000000..31b13a56 --- /dev/null +++ b/test/src/commonMain/kotlin/2_Collection.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:OptIn(LowLevelApi::class) + +package opensavvy.ktmongo.tests.api + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl +import opensavvy.prepared.suite.prepared +import kotlin.reflect.typeOf + +fun SuiteDsl.verifyCollection( + client: Prepared, +) = suite("Collection") { + + val coll by prepared { + client() + .database("test") + .collection("does-not-exist") + } + + test("Create") { + check(coll().name == "does-not-exist") + check(coll().fullyQualifiedName == "test.does-not-exist") + + check(coll().type == typeOf()) + + check(coll().toString().endsWith("MongoCollection(test.does-not-exist)")) + } + + test("The factory is functional") { + check(coll().factory.buildDocument { writeString("a", "b") }["a"]?.decodeString() == "b") + } + + test("The name strategy is functional") { + check(coll().propertyNameStrategy.pathOf(String::length) == Path("length")) + } + + test("The ObjectId generator is functional") { + // We want to check that it doesn't throw + check(coll().newId() != ObjectId.MIN) + } + + test("The BsonContext is functional") { + // We want to check that it doesn't throw + val _ = coll().context + } + +} diff --git a/test/src/commonMain/kotlin/Entrypoint.kt b/test/src/commonMain/kotlin/Entrypoint.kt new file mode 100644 index 00000000..643784ee --- /dev/null +++ b/test/src/commonMain/kotlin/Entrypoint.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.tests.api + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.prepared.suite.* +import kotlin.coroutines.CoroutineContext + +fun SuiteDsl.verifyClient( + name: String, + createClient: suspend (connectionString: String, coroutineContext: CoroutineContext) -> MongoClient, +) = suite(name) { + + val connectionString by shared { + "mongodb://localhost:27017" + } + + test("Can connect to the database") { + println("This test suite will run with connection string: ${connectionString()}") + } + + val client by prepared { + val client = createClient(connectionString(), backgroundScope.coroutineContext) + + cleanUp("client") { + client.close() + } + + client + } + + verifyClient(client) + verifyDatabase(client) + verifyCollection(client) +} diff --git a/test/src/commonTest/kotlin/Dummy.kt b/test/src/commonTest/kotlin/Dummy.kt new file mode 100644 index 00000000..088fc565 --- /dev/null +++ b/test/src/commonTest/kotlin/Dummy.kt @@ -0,0 +1,28 @@ +/* + * 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.api + +import opensavvy.prepared.runner.testballoon.preparedSuite + +val DummyTest by preparedSuite { + + test("Dummy") { + // Each test scenario has its own Gradle module. + // This module only contains the test sources. + } + +} -- 2.51.2 From 5a81cc5db7b46c2c1ca4590f7e0e26aa423d423d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 23 Jun 2026 22:40:57 +0200 Subject: [PATCH 11/18] test(driver-coroutines): Run integration tests --- settings.gradle.kts | 2 ++ test-coroutines-kotlinx/build.gradle.kts | 36 +++++++++++++++++++ .../kotlin/CoroutinesMongoClient.kotlinx.kt | 29 +++++++++++++++ test-coroutines-reflection/build.gradle.kts | 36 +++++++++++++++++++ .../CoroutinesMongoClient.reflection.kt | 29 +++++++++++++++ 5 files changed, 132 insertions(+) create mode 100644 test-coroutines-kotlinx/build.gradle.kts create mode 100644 test-coroutines-kotlinx/src/jvmTest/kotlin/CoroutinesMongoClient.kotlinx.kt create mode 100644 test-coroutines-reflection/build.gradle.kts create mode 100644 test-coroutines-reflection/src/jvmTest/kotlin/CoroutinesMongoClient.reflection.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 56ff1fa9..b9408162 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -98,6 +98,8 @@ include( "test-legacy", "test", + "test-coroutines-kotlinx", + "test-coroutines-reflection", "docs:website", "gradle:templates:template-app", diff --git a/test-coroutines-kotlinx/build.gradle.kts b/test-coroutines-kotlinx/build.gradle.kts new file mode 100644 index 00000000..74f2b669 --- /dev/null +++ b/test-coroutines-kotlinx/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * 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.test) + } + + sourceSets.jvmTest.dependencies { + implementation(projects.driverCoroutines) + implementation(libs.mongodb.kotlinx.serialization) + implementation(libsCommon.bundles.testBalloon) + } +} diff --git a/test-coroutines-kotlinx/src/jvmTest/kotlin/CoroutinesMongoClient.kotlinx.kt b/test-coroutines-kotlinx/src/jvmTest/kotlin/CoroutinesMongoClient.kotlinx.kt new file mode 100644 index 00000000..a629ac39 --- /dev/null +++ b/test-coroutines-kotlinx/src/jvmTest/kotlin/CoroutinesMongoClient.kotlinx.kt @@ -0,0 +1,29 @@ +/* + * 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.coroutines.serialization + +import opensavvy.ktmongo.coroutines.CoroutineMongoClient +import opensavvy.ktmongo.tests.api.verifyClient +import opensavvy.prepared.runner.testballoon.preparedSuite + +val IntegrationTests by preparedSuite { + + verifyClient("CoroutinesMongoClient (serialization-based)") { connectionString, _ -> + CoroutineMongoClient(connectionString) + } + +} diff --git a/test-coroutines-reflection/build.gradle.kts b/test-coroutines-reflection/build.gradle.kts new file mode 100644 index 00000000..a51f637a --- /dev/null +++ b/test-coroutines-reflection/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * 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.test) + } + + sourceSets.jvmTest.dependencies { + implementation(projects.driverCoroutines) + implementation(libs.mongodb.kotlin.reflection) + implementation(libsCommon.bundles.testBalloon) + } +} diff --git a/test-coroutines-reflection/src/jvmTest/kotlin/CoroutinesMongoClient.reflection.kt b/test-coroutines-reflection/src/jvmTest/kotlin/CoroutinesMongoClient.reflection.kt new file mode 100644 index 00000000..6051fb2b --- /dev/null +++ b/test-coroutines-reflection/src/jvmTest/kotlin/CoroutinesMongoClient.reflection.kt @@ -0,0 +1,29 @@ +/* + * 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.coroutines.serialization + +import opensavvy.ktmongo.coroutines.CoroutineMongoClient +import opensavvy.ktmongo.tests.api.verifyClient +import opensavvy.prepared.runner.testballoon.preparedSuite + +val IntegrationTests by preparedSuite { + + verifyClient("CoroutinesMongoClient (reflection-based)") { connectionString, _ -> + CoroutineMongoClient(connectionString) + } + +} -- 2.51.2 From 412f8d6f4173462dae4d2429c190ceaeef596c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 23 Jun 2026 23:02:45 +0200 Subject: [PATCH 12/18] build(driver-coroutines): Aggregate code coverage from the integration tests --- driver-coroutines/build.gradle.kts | 5 +++++ test-coroutines-kotlinx/build.gradle.kts | 1 + test-coroutines-reflection/build.gradle.kts | 1 + 3 files changed, 7 insertions(+) diff --git a/driver-coroutines/build.gradle.kts b/driver-coroutines/build.gradle.kts index 453026bb..33990c75 100644 --- a/driver-coroutines/build.gradle.kts +++ b/driver-coroutines/build.gradle.kts @@ -44,6 +44,11 @@ kotlin { } } +dependencies { + kover(projects.testCoroutinesKotlinx) + kover(projects.testCoroutinesReflection) +} + library { name.set("KtMongo: MongoDB driver for Kotlin • Coroutines") description.set("Kotlin-first MongoDB driver, based on the official MongoDB Coroutines driver") diff --git a/test-coroutines-kotlinx/build.gradle.kts b/test-coroutines-kotlinx/build.gradle.kts index 74f2b669..a66ce0be 100644 --- a/test-coroutines-kotlinx/build.gradle.kts +++ b/test-coroutines-kotlinx/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(opensavvyConventions.plugins.kotlin.internal) alias(libsCommon.plugins.kotlinx.serialization) alias(libsCommon.plugins.testBalloon) + id("org.jetbrains.kotlinx.kover") } kotlin { diff --git a/test-coroutines-reflection/build.gradle.kts b/test-coroutines-reflection/build.gradle.kts index a51f637a..f2326146 100644 --- a/test-coroutines-reflection/build.gradle.kts +++ b/test-coroutines-reflection/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(opensavvyConventions.plugins.kotlin.internal) alias(libsCommon.plugins.kotlinx.serialization) alias(libsCommon.plugins.testBalloon) + id("org.jetbrains.kotlinx.kover") } kotlin { -- 2.51.2 From 993e3fc14ce8eb04b9a64ef52ad8cc8ad5b14f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 24 Jun 2026 10:37:29 +0200 Subject: [PATCH 13/18] feat(driver-api): Unify the CountOperations --- .../src/commonMain/kotlin/MongoCollection.kt | 12 +- .../kotlin/operations/BaseOperations.kt | 36 +++++ .../kotlin/operations/CountOperations.kt | 139 ++++++++++++++++++ .../kotlin/CoroutineMongoCollectionImpl.kt | 31 ++++ test/src/commonMain/kotlin/Entrypoint.kt | 3 + test/src/commonMain/kotlin/Utils.kt | 32 ++++ .../kotlin/operations/CountOperations.test.kt | 54 +++++++ 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 driver-api/src/commonMain/kotlin/operations/BaseOperations.kt create mode 100644 driver-api/src/commonMain/kotlin/operations/CountOperations.kt create mode 100644 test/src/commonMain/kotlin/Utils.kt create mode 100644 test/src/commonMain/kotlin/operations/CountOperations.test.kt diff --git a/driver-api/src/commonMain/kotlin/MongoCollection.kt b/driver-api/src/commonMain/kotlin/MongoCollection.kt index b143df74..bb8a379a 100644 --- a/driver-api/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-api/src/commonMain/kotlin/MongoCollection.kt @@ -16,10 +16,10 @@ package opensavvy.ktmongo.api +import opensavvy.ktmongo.api.operations.CountOperations import opensavvy.ktmongo.bson.BsonFactory import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.ObjectIdGenerator -import opensavvy.ktmongo.dsl.BsonContext import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.path.PropertyNameStrategy import kotlin.reflect.KType @@ -51,7 +51,8 @@ import kotlin.reflect.KType * - [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 { +interface MongoCollection : ObjectIdGenerator, + CountOperations { /** * THe name of this collection. @@ -102,13 +103,6 @@ interface MongoCollection : ObjectIdGenerator { */ val objectIdGenerator: ObjectIdGenerator - /** - * The full BSON configuration, used by the DSL to generate queries. - * - * For more information, see [BsonContext]. - */ - val context: BsonContext - override fun newId(): ObjectId = objectIdGenerator.newId() diff --git a/driver-api/src/commonMain/kotlin/operations/BaseOperations.kt b/driver-api/src/commonMain/kotlin/operations/BaseOperations.kt new file mode 100644 index 00000000..e5dbb43a --- /dev/null +++ b/driver-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.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-api/src/commonMain/kotlin/operations/CountOperations.kt b/driver-api/src/commonMain/kotlin/operations/CountOperations.kt new file mode 100644 index 00000000..ebae4ce4 --- /dev/null +++ b/driver-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.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. + */ + suspend 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/) + */ + suspend 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 + * } + * ``` + */ + suspend 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. + */ + suspend fun countEstimated(): Long + +} diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt index accdcde1..1cf05d1f 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt @@ -25,7 +25,11 @@ 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.command.Count +import opensavvy.ktmongo.dsl.command.CountOptions import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.official.options.toJava import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -52,8 +56,35 @@ private class CoroutineMongoCollectionImpl( ObjectIdGenerator by objectIdGenerator, PropertyNameStrategy by propertyNameStrategy + @LowLevelApi override val context: BsonContext = CoroutineBsonContext() + // region Count + + override suspend fun count(): Long = + inner.countDocuments() + + @OptIn(LowLevelApi::class) + override suspend 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 suspend fun countEstimated(): Long = + inner.estimatedDocumentCount() + + // endregion + override fun toString(): String = "CoroutineMongoCollection($fullyQualifiedName)" } diff --git a/test/src/commonMain/kotlin/Entrypoint.kt b/test/src/commonMain/kotlin/Entrypoint.kt index 643784ee..94b7c619 100644 --- a/test/src/commonMain/kotlin/Entrypoint.kt +++ b/test/src/commonMain/kotlin/Entrypoint.kt @@ -17,6 +17,7 @@ package opensavvy.ktmongo.tests.api import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.tests.api.operations.verifyCountOperations import opensavvy.prepared.suite.* import kotlin.coroutines.CoroutineContext @@ -46,4 +47,6 @@ fun SuiteDsl.verifyClient( verifyClient(client) verifyDatabase(client) verifyCollection(client) + + verifyCountOperations(client) } diff --git a/test/src/commonMain/kotlin/Utils.kt b/test/src/commonMain/kotlin/Utils.kt new file mode 100644 index 00000000..8fcdfcd3 --- /dev/null +++ b/test/src/commonMain/kotlin/Utils.kt @@ -0,0 +1,32 @@ +/* + * 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.api + +import opensavvy.ktmongo.api.MongoClient +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.prepared +import opensavvy.prepared.suite.random.randomInt + +val testId by randomInt(0, Int.MAX_VALUE) + +inline fun Prepared.collection( + prefix: String, +) = prepared { + this@collection() + .database("ktmongo-integration-tests") + .collection("$prefix-${testId()}") +} diff --git a/test/src/commonMain/kotlin/operations/CountOperations.test.kt b/test/src/commonMain/kotlin/operations/CountOperations.test.kt new file mode 100644 index 00000000..58eb0db2 --- /dev/null +++ b/test/src/commonMain/kotlin/operations/CountOperations.test.kt @@ -0,0 +1,54 @@ +/* + * 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.api.operations + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.tests.api.collection +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +@Serializable +data class CountOperationsUser( + val _id: ObjectId, + val name: String, +) + +fun SuiteDsl.verifyCountOperations( + client: Prepared, +) = suite("Count operations") { + val collection by client.collection("operation-count-users") + + suite("Empty collection") { + test("Count should be 0") { + check(collection().count() == 0L) + } + + test("Count with a predicate should be 0") { + check(collection().count { CountOperationsUser::name eq "Bob" } == 0L) + } + + test("Does an element exist, in an empty collection") { + check(!collection().exists {}) + } + + test("Count (estimated) of an empty collection") { + check(collection().countEstimated() == 0L) + } + } +} -- 2.51.2 From 4a75b28c607b476bd6f02c233d0da8dc469774ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 24 Jun 2026 20:12:52 +0200 Subject: [PATCH 14/18] fix(driver-coroutines): Fix the official driver's MongoCollection not being aware of Kotlin types --- .../src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt index 1cf05d1f..919bdedf 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt @@ -34,7 +34,7 @@ import kotlin.reflect.KType import kotlin.reflect.typeOf private class CoroutineMongoCollectionImpl( - private val inner: MongoCollection, + inner: MongoCollection, override val factory: BsonFactory, override val propertyNameStrategy: PropertyNameStrategy, override val objectIdGenerator: ObjectIdGenerator, @@ -42,6 +42,9 @@ private class CoroutineMongoCollectionImpl( override val type: KType, ) : CoroutineMongoCollection { + private val inner = inner + .withCodecRegistry(factory.codecRegistry) + override fun asOfficial(): MongoCollection = inner -- 2.51.2 From b02a8dba584c319ce1cea511492d5c6600068d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 25 Jun 2026 10:11:23 +0200 Subject: [PATCH 15/18] feat(driver-api): Unify the InsertOperations --- .../src/commonMain/kotlin/MongoCollection.kt | 4 +- .../kotlin/operations/InsertOperations.kt | 110 ++++++++++++++++++ .../kotlin/CoroutineMongoCollectionImpl.kt | 42 ++++++- test/src/commonMain/kotlin/Entrypoint.kt | 2 + .../operations/InsertOperations.test.kt | 99 ++++++++++++++++ 5 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 driver-api/src/commonMain/kotlin/operations/InsertOperations.kt create mode 100644 test/src/commonMain/kotlin/operations/InsertOperations.test.kt diff --git a/driver-api/src/commonMain/kotlin/MongoCollection.kt b/driver-api/src/commonMain/kotlin/MongoCollection.kt index bb8a379a..b6c9751f 100644 --- a/driver-api/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-api/src/commonMain/kotlin/MongoCollection.kt @@ -17,6 +17,7 @@ package opensavvy.ktmongo.api import opensavvy.ktmongo.api.operations.CountOperations +import opensavvy.ktmongo.api.operations.InsertOperations import opensavvy.ktmongo.bson.BsonFactory import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.ObjectIdGenerator @@ -52,7 +53,8 @@ import kotlin.reflect.KType * - [Size limits](https://www.mongodb.com/docs/manual/reference/limits/#bson-documents) */ interface MongoCollection : ObjectIdGenerator, - CountOperations { + CountOperations, + InsertOperations { /** * THe name of this collection. diff --git a/driver-api/src/commonMain/kotlin/operations/InsertOperations.kt b/driver-api/src/commonMain/kotlin/operations/InsertOperations.kt new file mode 100644 index 00000000..34f122c9 --- /dev/null +++ b/driver-api/src/commonMain/kotlin/operations/InsertOperations.kt @@ -0,0 +1,110 @@ +/* + * 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.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)) + * ``` + * + * ### 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. + */ + suspend 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) + * ``` + * + * ### 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. + */ + suspend 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) + * ) + * ``` + * + * ### 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. + */ + suspend fun insertMany( + vararg documents: Document, + options: InsertManyOptions.() -> Unit = {}, + ) { + insertMany(documents.asList(), options) + } + +} diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt index 919bdedf..67a978ee 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt @@ -25,11 +25,14 @@ 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.command.Count -import opensavvy.ktmongo.dsl.command.CountOptions +import opensavvy.ktmongo.dsl.command.* +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.official.options.toJava +import opensavvy.ktmongo.official.toJava import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -86,6 +89,33 @@ private class CoroutineMongoCollectionImpl( override suspend fun countEstimated(): Long = inner.estimatedDocumentCount() + // endregion + // region Insert + + @OptIn(LowLevelApi::class) + override suspend 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 suspend 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 override fun toString(): String = @@ -153,3 +183,11 @@ inline fun MongoCollection.asKtMongo( 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/test/src/commonMain/kotlin/Entrypoint.kt b/test/src/commonMain/kotlin/Entrypoint.kt index 94b7c619..296763c3 100644 --- a/test/src/commonMain/kotlin/Entrypoint.kt +++ b/test/src/commonMain/kotlin/Entrypoint.kt @@ -18,6 +18,7 @@ package opensavvy.ktmongo.tests.api import opensavvy.ktmongo.api.MongoClient import opensavvy.ktmongo.tests.api.operations.verifyCountOperations +import opensavvy.ktmongo.tests.api.operations.verifyInsertOperations import opensavvy.prepared.suite.* import kotlin.coroutines.CoroutineContext @@ -48,5 +49,6 @@ fun SuiteDsl.verifyClient( verifyDatabase(client) verifyCollection(client) + verifyInsertOperations(client) verifyCountOperations(client) } diff --git a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt new file mode 100644 index 00000000..a3f26e4f --- /dev/null +++ b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt @@ -0,0 +1,99 @@ +/* + * 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.api.operations + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.options.WriteConcern +import opensavvy.ktmongo.tests.api.collection +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +@Serializable +data class InsertOperationsUser( + val _id: ObjectId? = null, + val name: String, +) + +fun SuiteDsl.verifyInsertOperations( + client: Prepared, +) = suite("Insert operations") { + val collection by client.collection("operation-insert-users") + + suite("Simple") { + test("Insert a document") { + collection().insertOne( + InsertOperationsUser( + _id = collection().newId(), + name = "Bob", + ) + ) + } + + test("Insert a document without ID") { + collection().insertOne( + InsertOperationsUser( + name = "Bob", + ) + ) + } + + test("Insert multiple documents at once") { + collection().insertMany( + InsertOperationsUser( + _id = collection().newId(), + name = "Alice", + ), + InsertOperationsUser( + _id = collection().newId(), + name = "Bob", + ) + ) + } + } + + suite("Options") { + test("insertOne • Write concern") { + collection().insertOne( + InsertOperationsUser( + _id = collection().newId(), + name = "Bob", + ), + options = { + writeConcern(WriteConcern.FireAndForget) + } + ) + + // TODO: write a test that can check that the option is correctly applied + } + + test("insertMany • Write concern") { + collection().insertMany( + InsertOperationsUser( + _id = collection().newId(), + name = "Bob", + ), + options = { + writeConcern(WriteConcern.FireAndForget) + } + ) + + // TODO: write a test that can check that the option is correctly applied + } + } +} -- 2.51.2 From 78400c69f827aee1d04249f724e8251dd1af2b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 25 Jun 2026 21:42:58 +0200 Subject: [PATCH 16/18] test(driver-api): Extend the coverage of CountOperations --- .../kotlin/operations/CountOperations.test.kt | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/src/commonMain/kotlin/operations/CountOperations.test.kt b/test/src/commonMain/kotlin/operations/CountOperations.test.kt index 58eb0db2..10160332 100644 --- a/test/src/commonMain/kotlin/operations/CountOperations.test.kt +++ b/test/src/commonMain/kotlin/operations/CountOperations.test.kt @@ -22,6 +22,7 @@ import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.tests.api.collection import opensavvy.prepared.suite.Prepared import opensavvy.prepared.suite.SuiteDsl +import opensavvy.prepared.suite.prepared @Serializable data class CountOperationsUser( @@ -51,4 +52,118 @@ fun SuiteDsl.verifyCountOperations( check(collection().countEstimated() == 0L) } } + + suite("Collection with one element") { + val createBob by prepared { + collection().insertOne( + CountOperationsUser( + _id = collection().newId(), + name = "Bob", + ) + ) + } + + test("Count should be 1") { + createBob() + + check(collection().count() == 1L) + } + + test("Count with a predicate that includes the field should be 1") { + createBob() + + check(collection().count { CountOperationsUser::name eq "Bob" } == 1L) + } + + test("Count with a predicate that doesn't include the field should be 0") { + createBob() + + check(collection().count { CountOperationsUser::name eq "Alice" } == 0L) + } + + test("Bob exists") { + createBob() + + check(collection().exists { CountOperationsUser::name eq "Bob" }) + } + + test("Alice doesn't exist") { + createBob() + + check(!collection().exists { CountOperationsUser::name eq "Alice" }) + } + + test("Count (estimated) should still be 1") { + createBob() + + check(collection().countEstimated() == 1L) + } + } + + suite("Collection with multiple documents") { + val createDocuments by prepared { + collection().insertMany( + CountOperationsUser( + _id = collection().newId(), + name = "Bob", + ), + CountOperationsUser( + _id = collection().newId(), + name = "Alice", + ), + CountOperationsUser( + _id = collection().newId(), + name = "Fred", + ), + ) + } + + test("Count should be 3") { + createDocuments() + + check(collection().count() == 3L) + } + + test("Count should be 3, but with a skip") { + createDocuments() + + check(collection().count({ skip(1) }) {} == 2L) + } + + test("Count with a predicate that includes two documents") { + createDocuments() + + check(collection().count { CountOperationsUser::name gte "Bob" } == 2L) + } + + test("Count with a predicate that includes two documents, but with a limit") { + createDocuments() + + check(collection().count({ limit(1) }) { CountOperationsUser::name gte "Bob" } == 1L) + } + + test("Count with a predicate that doesn't include any document should be 0") { + createDocuments() + + check(collection().count { CountOperationsUser::name eq "Absent" } == 0L) + } + + test("Bob exists") { + createDocuments() + + check(collection().exists { CountOperationsUser::name eq "Bob" }) + } + + test("Absent doesn't exist") { + createDocuments() + + check(!collection().exists { CountOperationsUser::name eq "Absent" }) + } + + test("Count (estimated) should still be 3") { + createDocuments() + + check(collection().countEstimated() == 3L) + } + } } -- 2.51.2 From 98757429730a1bc1cf064700339cf44e1f238717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 26 Jun 2026 21:10:46 +0200 Subject: [PATCH 17/18] test: Try the CI MongoDB URL if the local one fails --- test/src/commonMain/kotlin/Entrypoint.kt | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/src/commonMain/kotlin/Entrypoint.kt b/test/src/commonMain/kotlin/Entrypoint.kt index 296763c3..481f1eb6 100644 --- a/test/src/commonMain/kotlin/Entrypoint.kt +++ b/test/src/commonMain/kotlin/Entrypoint.kt @@ -16,6 +16,9 @@ package opensavvy.ktmongo.tests.api +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import opensavvy.ktmongo.api.MongoClient import opensavvy.ktmongo.tests.api.operations.verifyCountOperations import opensavvy.ktmongo.tests.api.operations.verifyInsertOperations @@ -27,8 +30,29 @@ fun SuiteDsl.verifyClient( createClient: suspend (connectionString: String, coroutineContext: CoroutineContext) -> MongoClient, ) = suite(name) { + suspend fun verifyClientConnected(client: MongoClient): Boolean = try { + val count = client.use { + client.database("does-not-exist") + .collection("does-not-exist") + .count() + } + check(count == 0L) { "Client $client found values in the fake collection. Did you create it yourself?" } + true + } catch (_: Throwable) { + currentCoroutineContext().ensureActive() + false + } + val connectionString by shared { - "mongodb://localhost:27017" + val candidates = listOf( + "mongodb://localhost:27017", // Dev + "mongodb://mongo:27017", // CI + ) + + coroutineScope { + candidates + .first { verifyClientConnected(createClient(it, currentCoroutineContext())) } + } } test("Can connect to the database") { -- 2.51.2 From e4e2fcf51b651d0d20f82ef9d8c6891082dccbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 26 Jun 2026 21:38:05 +0200 Subject: [PATCH 18/18] ci(gitlab): Always rerun the JVM tests If we don't rerun them, Kover sees no test execution and reports a 0% code coverage. --- .gitlab-ci.main.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.main.kts b/.gitlab-ci.main.kts index 1bce8aa4..acfbbd09 100755 --- a/.gitlab-ci.main.kts +++ b/.gitlab-ci.main.kts @@ -148,7 +148,7 @@ gitlabCi { script { gradlew.tasks( - "check :koverLog :koverHtmlReport koverVerify --rerun", + "check :koverLog :koverHtmlReport koverVerify --rerun jvmTest --rerun", "-x jsBrowserTest", "-x jsNodeTest", "-x wasmJsBrowserTest", -- 2.51.2