From 8833e83467c3b1882fcab30cfd3c400ae72c6c1e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 3 Apr 2024 11:16:41 +0100 Subject: [PATCH] Expand bounds for distinct and MongoIterable#map (#1352) Allow for nullable types in those scenarios. JAVA-5358 --- driver-kotlin-coroutine/build.gradle.kts | 1 + .../kotlin/client/coroutine/SmokeTests.kt | 101 ++++++++++++++++++ .../com/mongodb/kotlin/client/SmokeTests.kt | 89 +++++++++++++++ .../mongodb/kotlin/client/DistinctIterable.kt | 2 +- .../mongodb/kotlin/client/MongoCollection.kt | 8 +- .../com/mongodb/kotlin/client/MongoCursor.kt | 4 +- .../mongodb/kotlin/client/MongoIterable.kt | 4 +- 7 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/SmokeTests.kt create mode 100644 driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/SmokeTests.kt diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 1467c832abe..abd81fcd1d3 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { testImplementation("io.github.classgraph:classgraph:4.8.154") integrationTestImplementation("org.jetbrains.kotlin:kotlin-test-junit") + integrationTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") integrationTestImplementation(project(path = ":driver-sync")) integrationTestImplementation(project(path = ":driver-core")) integrationTestImplementation(project(path = ":bson")) diff --git a/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/SmokeTests.kt b/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/SmokeTests.kt new file mode 100644 index 00000000000..db51912d17c --- /dev/null +++ b/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/SmokeTests.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.kotlin.client.coroutine + +import com.mongodb.client.Fixture.getDefaultDatabaseName +import com.mongodb.client.Fixture.getMongoClientSettings +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.toSet +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.bson.Document +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class SmokeTests { + + @AfterEach + fun afterEach() { + runBlocking { database?.drop() } + } + + @Test + @DisplayName("distinct and return nulls") + fun testDistinctNullable() = runTest { + collection!!.insertMany( + listOf( + Document.parse("{_id: 1, a: 0}"), + Document.parse("{_id: 2, a: 1}"), + Document.parse("{_id: 3, a: 0}"), + Document.parse("{_id: 4, a: null}"))) + + // nulls are auto excluded in reactive streams! + val actual = collection!!.distinct("a").toSet() + assertEquals(setOf(0, 1), actual) + } + + @Test + @DisplayName("mapping can return nulls") + fun testMongoIterableMap() = runTest { + collection!!.insertMany( + listOf( + Document.parse("{_id: 1, a: 0}"), + Document.parse("{_id: 2, a: 1}"), + Document.parse("{_id: 3, a: 0}"), + Document.parse("{_id: 4, a: null}"))) + + val actual = collection!!.find().map { it["a"] as Int? }.toList() + assertContentEquals(listOf(0, 1, 0, null), actual) + } + + companion object { + + private var mongoClient: MongoClient? = null + private var database: MongoDatabase? = null + private var collection: MongoCollection? = null + + @BeforeAll + @JvmStatic + internal fun beforeAll() { + runBlocking { + mongoClient = MongoClient.create(getMongoClientSettings()) + database = mongoClient?.getDatabase(getDefaultDatabaseName()) + database?.drop() + collection = database?.getCollection("SmokeTests") + } + } + + @AfterAll + @JvmStatic + internal fun afterAll() { + runBlocking { + collection = null + database?.drop() + database = null + mongoClient?.close() + mongoClient = null + } + } + } +} diff --git a/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/SmokeTests.kt b/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/SmokeTests.kt new file mode 100644 index 00000000000..3fc601d6425 --- /dev/null +++ b/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/SmokeTests.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.kotlin.client + +import com.mongodb.client.Fixture.getDefaultDatabaseName +import com.mongodb.client.Fixture.getMongoClientSettings +import kotlin.test.assertContentEquals +import org.bson.Document +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class SmokeTests { + + @AfterEach + fun afterEach() { + database?.drop() + } + + @Test + @DisplayName("distinct and return nulls") + fun testDistinctNullable() { + collection!!.insertMany( + listOf( + Document.parse("{_id: 1, a: 0}"), + Document.parse("{_id: 2, a: 1}"), + Document.parse("{_id: 3, a: 0}"), + Document.parse("{_id: 4, a: null}"))) + + val actual = collection!!.distinct("a").toList().toSet() + assertEquals(setOf(null, 0, 1), actual) + } + + @Test + @DisplayName("mapping can return nulls") + fun testMongoIterableMap() { + collection!!.insertMany( + listOf( + Document.parse("{_id: 1, a: 0}"), + Document.parse("{_id: 2, a: 1}"), + Document.parse("{_id: 3, a: 0}"), + Document.parse("{_id: 4, a: null}"))) + + val actual = collection!!.find().map { it["a"] }.toList() + assertContentEquals(listOf(0, 1, 0, null), actual) + } + + companion object { + + private var mongoClient: MongoClient? = null + private var database: MongoDatabase? = null + private var collection: MongoCollection? = null + + @BeforeAll + @JvmStatic + internal fun beforeAll() { + mongoClient = MongoClient.create(getMongoClientSettings()) + database = mongoClient?.getDatabase(getDefaultDatabaseName()) + database?.drop() + collection = database?.getCollection("SmokeTests") + } + + @AfterAll + @JvmStatic + internal fun afterAll() { + collection = null + database?.drop() + database = null + mongoClient?.close() + mongoClient = null + } + } +} diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/DistinctIterable.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/DistinctIterable.kt index b630af52517..de77215d033 100644 --- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/DistinctIterable.kt +++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/DistinctIterable.kt @@ -27,7 +27,7 @@ import org.bson.conversions.Bson * @param T The type of the result. * @see [Distinct command](https://www.mongodb.com/docs/manual/reference/command/distinct/) */ -public class DistinctIterable(private val wrapped: JDistinctIterable) : MongoIterable(wrapped) { +public class DistinctIterable(private val wrapped: JDistinctIterable) : MongoIterable(wrapped) { /** * Sets the number of documents to return per batch. * diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCollection.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCollection.kt index 1529af7eaba..786140caf12 100644 --- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCollection.kt +++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCollection.kt @@ -219,7 +219,7 @@ public class MongoCollection(private val wrapped: JMongoCollection) * @return an iterable of distinct values * @see [Distinct command](https://www.mongodb.com/docs/manual/reference/command/distinct/) */ - public fun distinct( + public fun distinct( fieldName: String, filter: Bson = BsonDocument(), resultClass: Class @@ -236,7 +236,7 @@ public class MongoCollection(private val wrapped: JMongoCollection) * @return an iterable of distinct values * @see [Distinct command](https://www.mongodb.com/docs/manual/reference/command/distinct/) */ - public fun distinct( + public fun distinct( clientSession: ClientSession, fieldName: String, filter: Bson = BsonDocument(), @@ -252,7 +252,7 @@ public class MongoCollection(private val wrapped: JMongoCollection) * @return an iterable of distinct values * @see [Distinct command](https://www.mongodb.com/docs/manual/reference/command/distinct/) */ - public inline fun distinct( + public inline fun distinct( fieldName: String, filter: Bson = BsonDocument() ): DistinctIterable = distinct(fieldName, filter, R::class.java) @@ -267,7 +267,7 @@ public class MongoCollection(private val wrapped: JMongoCollection) * @return an iterable of distinct values * @see [Distinct command](https://www.mongodb.com/docs/manual/reference/command/distinct/) */ - public inline fun distinct( + public inline fun distinct( clientSession: ClientSession, fieldName: String, filter: Bson = BsonDocument() diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCursor.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCursor.kt index 5c757bf5e65..b407195b079 100644 --- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCursor.kt +++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoCursor.kt @@ -36,7 +36,7 @@ import org.bson.BsonDocument * * @param T The type of documents the cursor contains */ -public sealed interface MongoCursor : Iterator, Closeable { +public sealed interface MongoCursor : Iterator, Closeable { /** * Gets the number of results available locally without blocking, which may be 0. @@ -90,7 +90,7 @@ public sealed interface MongoChangeStreamCursor : MongoCursor { public val resumeToken: BsonDocument? } -internal class MongoCursorImpl(private val wrapped: JMongoCursor) : MongoCursor { +internal class MongoCursorImpl(private val wrapped: JMongoCursor) : MongoCursor { override fun hasNext(): Boolean = wrapped.hasNext() diff --git a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoIterable.kt b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoIterable.kt index fffcea2ce76..b3c37d05d43 100644 --- a/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoIterable.kt +++ b/driver-kotlin-sync/src/main/kotlin/com/mongodb/kotlin/client/MongoIterable.kt @@ -23,7 +23,7 @@ import com.mongodb.client.MongoIterable as JMongoIterable * * @param T The type that this iterable will decode documents to. */ -public open class MongoIterable(private val delegate: JMongoIterable) { +public open class MongoIterable(private val delegate: JMongoIterable) { /** * Returns a cursor used for iterating over elements of type `T. The cursor is primarily used for change streams. @@ -71,7 +71,7 @@ public open class MongoIterable(private val delegate: JMongoIterable * @param transform a function that maps from the source to the target document type * @return an iterable which maps T to U */ - public fun map(transform: (T) -> R): MongoIterable = MongoIterable(delegate.map(transform)) + public fun map(transform: (T) -> R): MongoIterable = MongoIterable(delegate.map(transform)) /** Performs the given [action] on each element and safely closes the cursor. */ public fun forEach(action: (T) -> Unit): Unit = use { it.forEach(action) }