From e52559ebcac759243521ceb95d6f3245bad61f20 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 26 Sep 2024 12:02:39 +0100 Subject: [PATCH 1/8] Adding Kotlin extensions methods for projection. Fixes JAVA-5603 --- .../kotlin/client/model/Projections.kt | 257 +++++++++++++++ .../mongodb/kotlin/client/model/Properties.kt | 17 +- .../kotlin/client/model/KPropertiesTest.kt | 5 +- .../kotlin/client/model/ProjectionTest.kt | 311 ++++++++++++++++++ 4 files changed, 583 insertions(+), 7 deletions(-) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt new file mode 100644 index 00000000000..1d694da2812 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.annotations.Beta +import com.mongodb.annotations.Reason +import com.mongodb.client.model.Aggregates +import com.mongodb.client.model.Projections +import org.bson.conversions.Bson +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +/** + * The projection of the property. + * This is used in an aggregation pipeline to reference a property from a path. + */ +public val KProperty.projection: String get() = path().projection + +/** + * The projection of the property. + */ +public val String.projection: String get() = "\$$this" + +/** + * In order to write `$p.p2` + */ + +@JvmSynthetic +public infix fun KProperty1.projectionWith(p2: String): String = "$projection.$p2" + +/** + * Creates a projection of a property whose value is computed from the given expression. Projection with an expression can be used in the + * following contexts: + *
    + *
  • $project aggregation pipeline stage.
  • + *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the + * {@code MongoCollection}-based API where projection is supported, for example: + *
      + *
    • {@code find()}
    • + *
    • {@code findOneAndReplace()}
    • + *
    • {@code findOneAndUpdate()}
    • + *
    • {@code findOneAndDelete()}
    • + *
    + *
  • + *
+ * + * @param expression the expression + * @param the expression type + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ +@JvmSynthetic +public infix fun KProperty.computedFrom(expression: Any): Bson = + Projections.computed(path(), (expression as? KProperty<*>)?.projection ?: expression) + + +/** + * Creates a projection of a String whose value is computed from the given expression. Projection with an expression can be used in the + * following contexts: + *
    + *
  • $project aggregation pipeline stage.
  • + *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the + * {@code MongoCollection}-based API where projection is supported, for example: + *
      + *
    • {@code find()}
    • + *
    • {@code findOneAndReplace()}
    • + *
    • {@code findOneAndUpdate()}
    • + *
    • {@code findOneAndDelete()}
    • + *
    + *
  • + *
+ * + * @param expression the expression + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ + +@JvmSynthetic +public infix fun String.computedFrom(expression: Any): Bson = + @Suppress("UNCHECKED_CAST") + Projections.computed(this, (expression as? KProperty)?.projection ?: expression) + + +/** + * Creates a projection that includes all of the given properties. + * + * @param properties the field names + * @return the projection + */ +public fun include(vararg properties: KProperty<*>): Bson = include(properties.asList()) + +/** + * Creates a projection that includes all of the given properties. + * + * @param properties the field names + * @return the projection + */ +public fun include(properties: Iterable>): Bson = Projections.include(properties.map { it.path() }) + +/** + * Creates a projection that excludes all of the given properties. + * + * @param properties the field names + * @return the projection + */ +public fun exclude(vararg properties: KProperty<*>): Bson = exclude(properties.asList()) + +/** + * Creates a projection that excludes all of the given properties. + * + * @param properties the field names + * @return the projection + */ +public fun exclude(properties: Iterable>): Bson = Projections.exclude(properties.map { it.path() }) + +/** + * Creates a projection that excludes the _id field. This suppresses the automatic inclusion of _id that is the default, even when + * other fields are explicitly included. + * + * @return the projection + */ +public fun excludeId(): Bson = Projections.excludeId() + +/** + * Creates a projection that includes for the given property only the first element of an array that matches the query filter. This is + * referred to as the positional $ operator. + * + * @return the projection + * @mongodb.driver.manual reference/operator/projection/positional/#projection Project the first matching element ($ operator) + */ +public val KProperty.elemMatchProj: Bson get() = Projections.elemMatch(path()) + +/** + * Creates a projection that includes for the given property only the first element of the array value of that field that matches the given + * query filter. + * + * @param filter the filter to apply + * @return the projection + * @mongodb.driver.manual reference/operator/projection/elemMatch elemMatch + */ +public infix fun KProperty.elemMatchProj(filter: Bson): Bson = Projections.elemMatch(path(), filter) + +/** + * Creates a $meta projection for the given property + * + * @param metaFieldName the meta field name + * @return the projection + * @mongodb.driver.manual reference/operator/aggregation/meta/ + * @since 4.1 + * @see #metaTextScore(String) + * @see #metaSearchScore(String) + * @see #metaVectorSearchScore(String) + * @see #metaSearchHighlights(String) + */ +public infix fun KProperty.meta(metaFieldName: String): Bson = Projections.meta(path(), metaFieldName) + +/** + * Creates a textScore projection for the given property, for use with text queries. + * Calling this method is equivalent to calling {@link #meta(String)} with {@code "textScore"} as the argument. + * + * @return the projection + * @see Filters#text(String, TextSearchOptions) + * @mongodb.driver.manual reference/operator/aggregation/meta/#text-score-metadata--meta---textscore- textScore + */ +public fun KProperty.metaTextScore(): Bson = Projections.metaTextScore(path()) + +/** + * Creates a searchScore projection for the given property, + * for use with {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. + * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "searchScore"} as the argument. + * + * @return the projection + * @mongodb.atlas.manual atlas-search/scoring/ Scoring + * @since 4.7 + */ +public fun KProperty.metaSearchScore(): Bson = Projections.metaSearchScore(path()) + +/** + * Creates a vectorSearchScore projection for the given property, + * for use with {@link Aggregates#vectorSearch(FieldSearchPath, Iterable, String, long, VectorSearchOptions)} . + * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "vectorSearchScore"} as the argument. + * + * @return the projection + * @mongodb.atlas.manual atlas-search/scoring/ Scoring + * @mongodb.server.release 6.0.10 + * @since 4.11 + */ +@Beta(Reason.SERVER) +public fun KProperty.metaVectorSearchScore(): Bson = Projections.metaVectorSearchScore(path()) + +/** + * Creates a searchHighlights projection for the given property, + * for use with {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. + * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "searchHighlights"} as the argument. + * + * @return the projection + * @see com.mongodb.client.model.search.SearchHighlight + * @mongodb.atlas.manual atlas-search/highlighting/ Highlighting + * @since 4.7 + */ +public fun KProperty.metaSearchHighlights(): Bson = Projections.metaSearchHighlights(path()) + +/** + * Creates a projection to the given property of a slice of the array value of that field. + * + * @param limit the number of elements to project. + * @return the projection + * @mongodb.driver.manual reference/operator/projection/slice Slice + */ +public infix fun KProperty.slice(limit: Int): Bson = Projections.slice(path(), limit) + +/** + * Creates a projection to the given property of a slice of the array value of that field. + * + * @param skip the number of elements to skip before applying the limit + * @param limit the number of elements to project + * @return the projection + * @mongodb.driver.manual reference/operator/projection/slice Slice + */ +public fun KProperty.slice(skip: Int, limit: Int): Bson = Projections.slice(path(), skip, limit) + +/** + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last one takes + * precedence. + * + * @param projections the list of projections to combine + * @return the combined projection + */ +public fun fields(vararg projections: Bson): Bson = Projections.fields(*projections) + +/** + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last one takes + * precedence. + * + * @param projections the list of projections to combine + * @return the combined projection + * @mongodb.driver.manual + */ +public fun fields(projections: List): Bson = Projections.fields(projections) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt index 2c5cf956b94..436b00a9cd3 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt @@ -84,12 +84,17 @@ internal fun KProperty.path(): String { // If no path (serialName) then check for BsonId / BsonProperty if (path == null) { val originator = if (this is CustomProperty<*, *>) this.previous.property else this - val constructorProperty = - originator.javaField!!.declaringClass.kotlin.primaryConstructor?.findParameterByName(this.name) - - // Prefer BsonId annotation over BsonProperty - path = constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.let { "_id" } - path = path ?: constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.value + // If this property is calculated (doesn't have a backing field) ex "(Student::grades / Grades::score).posOp then + // originator.javaField will NPE. + // Only read various annotations on a declared property with a backing field + if (originator.javaField != null) { + val constructorProperty = + originator.javaField!!.declaringClass.kotlin.primaryConstructor?.findParameterByName(this.name) + + // Prefer BsonId annotation over BsonProperty + path = constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.let { "_id" } + path = path ?: constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.value + } path = path ?: this.name } path diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt index 80b835e0042..9d27b86c7ef 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt @@ -41,7 +41,8 @@ class KPropertiesTest { @BsonProperty("prop") val bsonProperty: String, @SerialName("rating") val score: String, val name: String, - @BsonProperty("old") val previous: Review? + @BsonProperty("old") val previous: Review?, + @BsonProperty("nested") val misc: List ) @Test @@ -84,6 +85,7 @@ class KPropertiesTest { fun testArrayPositionalOperator() { assertEquals("reviews.\$", Restaurant::reviews.posOp.path()) assertEquals("reviews.rating", (Restaurant::reviews / Review::score).path()) + assertEquals("reviews.nested.\$", (Restaurant::reviews / Review::misc).posOp.path()) assertEquals("reviews.\$.rating", (Restaurant::reviews.posOp / Review::score).path()) } @@ -91,6 +93,7 @@ class KPropertiesTest { fun testArrayAllPositionalOperator() { assertEquals("reviews.\$[]", Restaurant::reviews.allPosOp.path()) assertEquals("reviews.\$[].rating", (Restaurant::reviews.allPosOp / Review::score).path()) + assertEquals("reviews.nested.\$[]", (Restaurant::reviews / Review::misc).allPosOp.path()) } @Test diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt new file mode 100644 index 00000000000..8c1713d422f --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt @@ -0,0 +1,311 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Aggregates.project +import com.mongodb.client.model.Projections +import com.mongodb.kotlin.client.model.Filters.and +import com.mongodb.kotlin.client.model.Filters.eq +import com.mongodb.kotlin.client.model.Filters.gt +import org.bson.BsonDocument +import org.bson.conversions.Bson +import org.junit.Test +import kotlin.test.assertEquals + +class ProjectionTest { + + @Test + fun projection() { + assertEquals("\$name", Person::name.projection) + assertEquals("\$name.foo", Student::name.projectionWith("foo")) + } + + @Test + fun include() { + assertEquals("""{"name": 1, "age": 1, "results": 1, "address": 1}""", + include(Person::name, Person::age, Person::results, Person::address)) + + assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age))) + assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age, Person::name))) + + } + + @Test + fun exclude() { + assertEquals("""{"name": 0, "age": 0, "results": 0, "address": 0}""", + exclude(Person::name, Person::age, Person::results, Person::address)) + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age))) + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) + + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) + + assertEquals("""{"_id": 0}""", excludeId()) + assertEquals("Projections{projections=[{\"_id\": 0}, {\"name\": 1}]}", fields(excludeId(), include(Person::name)).toString()) + } + + @Test + fun firstElem() { + assertEquals(""" {"name.${'$'}" : 1} """, Person::name.elemMatchProj) + } + + @Test + fun elemMatch() { + // Given the following document: + /* + { + "_id": 1, + "name": "John Doe", + "grades": [ + { "subject": "Math", "score": 85 }, + { "subject": "English", "score": 90 }, + { "subject": "Science", "score": 75 } + ] + } + */ + + // This projection: + val expected = """ + {"grades": {"${'$'}elemMatch": {"${'$'}and": [{"subject": "Math"}, {"score": {"${'$'}gt": 80}}]}}} + """ + + // Should return: + /* + { + "_id": 1, + "grades": [ + { "subject": "Math", "score": 85 } + ] + } + */ + + assertEquals( + expected, + Student::grades.elemMatchProj(and((Grade::subject eq "Math"), (Grade::score gt 80))) + ) + + // Should create string representation for elemMatch with filter + assertEquals( + "ElemMatch Projection{fieldName='grades'," + + " filter=And Filter{filters=[Filter{fieldName='score', value=90}, " + + "Filter{fieldName='subject', value=Math}]}}", + Student::grades.elemMatchProj(and(Grade::score eq 90, Grade::subject eq "Math")).toString() + ) + } + + @Test + fun slice() { + // Given the following document: + /* + { + "_id": 1, + "name": "John Doe", + "grades": [ + { "subject": "Math", "score": 85 }, + { "subject": "English", "score": 90 }, + { "subject": "Science", "score": 75 } + ] + } + */ + + // This projection: + var expected = """ + {"grades": {"${'$'}slice": -1}} + """ + + // Should return: + /* + { + _id: 1, + name: 'John Doe', + grades: [ { subject: 'Science', score: 75 } ] + }, + */ + + assertEquals(expected, Student::grades.slice(-1)) + + // skip one, limit to two + expected = """ + {"grades": {"${'$'}slice": [1, 2]}} + """ + /* + { + _id: 1, + name: 'John Doe', + grades: [ + { subject: 'English', score: 90 }, + { subject: 'Science', score: 80 } + ] + } + */ + + assertEquals( + expected, + Student::grades.slice(1, 2) + ) + + // Combining projection + expected = """ + {"name": 0, "grades": {"${'$'}slice": [2, 1]}} + """ + // { _id: 1, grades: [ { subject: 'Science', score: 75 } ] } + assertEquals(expected, fields(exclude(Student::name), Student::grades.slice(2, 1))) + } + + @Test + fun meta() { + // Given the following document: + /* + { + "_id": 1, + "name": "John Doe", + "grades": [ + { "subject": "Math", "score": 85 }, + { "subject": "English", "score": 90 }, + { "subject": "Science", "score": 75 } + ] + } + */ + var expected = """ + {"score": {"${'$'}meta": "textScore"}} + """ + assertEquals(expected, Grade::score.metaTextScore()) + + // combining + expected = """ + {"_id": 0, "score": {"${'$'}meta": "textScore"}, "grades": {"${'$'}elemMatch": {"score": {"${'$'}gt": 87}}}} + """ + assertEquals( + expected, + fields( + excludeId(), Grade::score.metaTextScore(), + Student::grades.elemMatchProj(Grade::score gt 87) + ) + ) + // find({ $text: { $search: "Doe" } }, { _id: 0, score: { $meta: "textScore" }, grades: {$elemMatch: { score : {$gt: 87}}} } ) + /* + { score: 0.75, grades: [ { subject: 'English', score: 90 } ] } + */ + + expected = """ + {"score": {"${'$'}meta": "searchScore"}} + """ + assertEquals(expected, Grade::score.metaSearchScore()) + + expected = """ + {"score": {"${'$'}meta": "searchHighlights"}} + """ + assertEquals(expected, Grade::score.metaSearchHighlights()) + + expected = """ + {"score": {"${'$'}meta": "vectorSearchScore"}} + """ + assertEquals(expected, Grade::score.metaVectorSearchScore()) + assertEquals(expected, Grade::score.meta("vectorSearchScore")) + + expected = """ + {"_id": 0, "score": {"${'$'}meta": "vectorSearchScore"}} + """ + assertEquals(expected, fields(excludeId(), Grade::score meta "vectorSearchScore")) + } + + @Test + fun `computed projection`() { + assertEquals(""" {"c": "${'$'}y"} """, "c" computedFrom "\$y") + + assertEquals("""{"${'$'}project": {"c": "${'$'}name", "score": "${'$'}age"}}""", + project(fields("c" computedFrom Student::name, Grade::score computedFrom Student::age)) + ) + + // combine fields + assertEquals("{name : 1, age : 1, _id : 0}", + fields(include(Student::name, Student::age), excludeId()) + ) + + assertEquals("{name : 1, age : 1, _id : 0}", + fields(include(listOf(Student::name, Student::age)), excludeId()) + ) + + assertEquals("{name : 1, age : 0}", + fields(include(Student::name, Student::age), exclude(Student::age)) + ) + + // Should create string representation for include and exclude + assertEquals("""{"age": 1, "name": 1}""", include(Student::name, Student::age, Student::name).toString()) + assertEquals("""{"age": 0, "name": 0}""", exclude(Student::name, Student::age, Student::name).toString()) + assertEquals("""{"_id": 0}""", excludeId().toString()) + + // Should create string representation for computed + assertEquals("Expression{name='c', expression=\$y}", Projections.computed("c", "\$y").toString()) + assertEquals("Expression{name='c', expression=\$y}", ("c" computedFrom "\$y").toString()) + assertEquals("Expression{name='name', expression=\$y}", (Student::name computedFrom "\$y").toString()) + } + + @Test + fun `array projection`() { + assertEquals( + "{ \"grades.comments.${'$'}\": 1 }", + include((Student::grades / Grade::comments).posOp) + ) + + + // FIXME: this form is not supported "MongoServerError[Location31395]: positional projection cannot be used with exclusion" + // should we fail before hitting the wire protocol? the existing Java impl (com/mongodb/client/model/Projections.java) + // doesn't do similar checks AFAIK. + // Likewise using Student::grades.allPosOp should not be allowed ("FieldPath field names may not start with '$'. Consider + // using $getField or $setField") + // + assertEquals( + "{ \"grades.${'$'}\": 0 }", + exclude(Student::grades.posOp) + ) + } + + @Test + fun `projection in aggregation`() { + // Field Reference in Aggregation + assertEquals(""" {"${'$'}project": {"score": "${'$'}grades.score"}} """, + project((Grade::score computedFrom (Student::grades.projectionWith("score")))) + ) + + assertEquals("{\"${'$'}project\": {\"My Score\": \"${'$'}grades.score\"}}", + project("My Score" computedFrom (Student::grades / Grade::score)) + ) + + assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", + project("My Age" computedFrom Student::age) + ) + + assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", + project("My Age" computedFrom Student::age.projection) + ) + } + + // TODO maybe move these inside a Utils file + private data class Person(val name: String, val age: Int, val address: List, val results: List) + private data class Student(val name: String, val age: Int, val grades: List) + private data class Grade(val subject: String, val score: Int, val comments: List) + + private fun assertEquals(expected: String, result: Bson) = + assertEquals( + BsonDocument.parse(expected), + result.toBsonDocument() + ) +} + + From 1fddaaa509e4b5ccd5985ec83c2f7a4562105fb7 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 27 Sep 2024 09:42:00 +0100 Subject: [PATCH 2/8] Fixing formatting --- .../kotlin/client/model/Projections.kt | 161 ++++++++---------- .../mongodb/kotlin/client/model/Properties.kt | 6 +- .../kotlin/client/model/ProjectionTest.kt | 115 +++++-------- 3 files changed, 120 insertions(+), 162 deletions(-) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt index 1d694da2812..a37b3eb4f42 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt @@ -22,45 +22,37 @@ import com.mongodb.annotations.Beta import com.mongodb.annotations.Reason import com.mongodb.client.model.Aggregates import com.mongodb.client.model.Projections -import org.bson.conversions.Bson import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 +import org.bson.conversions.Bson -/** - * The projection of the property. - * This is used in an aggregation pipeline to reference a property from a path. - */ -public val KProperty.projection: String get() = path().projection +/** The projection of the property. This is used in an aggregation pipeline to reference a property from a path. */ +public val KProperty.projection: String + get() = path().projection -/** - * The projection of the property. - */ -public val String.projection: String get() = "\$$this" +/** The projection of the property. */ +public val String.projection: String + get() = "\$$this" -/** - * In order to write `$p.p2` - */ - -@JvmSynthetic -public infix fun KProperty1.projectionWith(p2: String): String = "$projection.$p2" +/** In order to write `$p.p2` */ +@JvmSynthetic public infix fun KProperty1.projectionWith(p2: String): String = "$projection.$p2" /** - * Creates a projection of a property whose value is computed from the given expression. Projection with an expression can be used in the - * following contexts: + * Creates a projection of a property whose value is computed from the given expression. Projection with an expression + * can be used in the following contexts: *
    - *
  • $project aggregation pipeline stage.
  • - *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the - * {@code MongoCollection}-based API where projection is supported, for example: - *
      - *
    • {@code find()}
    • - *
    • {@code findOneAndReplace()}
    • - *
    • {@code findOneAndUpdate()}
    • - *
    • {@code findOneAndDelete()}
    • - *
    - *
  • + *
  • $project aggregation pipeline stage.
  • + *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example:
      + *
    • {@code find()}
    • + *
    • {@code findOneAndReplace()}
    • + *
    • {@code findOneAndUpdate()}
    • + *
    • {@code findOneAndDelete()}
    • *
    * - * @param expression the expression + *
+ * + * @param expression the expression * @param the expression type * @return the projection * @see #computedSearchMeta(String) @@ -70,34 +62,29 @@ public infix fun KProperty1.projectionWith(p2: String): String public infix fun KProperty.computedFrom(expression: Any): Bson = Projections.computed(path(), (expression as? KProperty<*>)?.projection ?: expression) - /** - * Creates a projection of a String whose value is computed from the given expression. Projection with an expression can be used in the - * following contexts: + * Creates a projection of a String whose value is computed from the given expression. Projection with an expression can + * be used in the following contexts: *
    - *
  • $project aggregation pipeline stage.
  • - *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the - * {@code MongoCollection}-based API where projection is supported, for example: - *
      - *
    • {@code find()}
    • - *
    • {@code findOneAndReplace()}
    • - *
    • {@code findOneAndUpdate()}
    • - *
    • {@code findOneAndDelete()}
    • - *
    - *
  • + *
  • $project aggregation pipeline stage.
  • + *
  • Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example:
      + *
    • {@code find()}
    • + *
    • {@code findOneAndReplace()}
    • + *
    • {@code findOneAndUpdate()}
    • + *
    • {@code findOneAndDelete()}
    • *
    * - * @param expression the expression + *
+ * + * @param expression the expression * @return the projection * @see #computedSearchMeta(String) * @see Aggregates#project(Bson) */ - @JvmSynthetic public infix fun String.computedFrom(expression: Any): Bson = - @Suppress("UNCHECKED_CAST") - Projections.computed(this, (expression as? KProperty)?.projection ?: expression) - + @Suppress("UNCHECKED_CAST") Projections.computed(this, (expression as? KProperty)?.projection ?: expression) /** * Creates a projection that includes all of the given properties. @@ -132,29 +119,29 @@ public fun exclude(vararg properties: KProperty<*>): Bson = exclude(properties.a public fun exclude(properties: Iterable>): Bson = Projections.exclude(properties.map { it.path() }) /** - * Creates a projection that excludes the _id field. This suppresses the automatic inclusion of _id that is the default, even when - * other fields are explicitly included. + * Creates a projection that excludes the _id field. This suppresses the automatic inclusion of _id that is the default, + * even when other fields are explicitly included. * * @return the projection */ public fun excludeId(): Bson = Projections.excludeId() /** - * Creates a projection that includes for the given property only the first element of an array that matches the query filter. This is - * referred to as the positional $ operator. + * Creates a projection that includes for the given property only the first element of an array that matches the query + * filter. This is referred to as the positional $ operator. * - * @return the projection - * @mongodb.driver.manual reference/operator/projection/positional/#projection Project the first matching element ($ operator) + * @return the projection @mongodb.driver.manual reference/operator/projection/positional/#projection Project the first + * matching element ($ operator) */ -public val KProperty.elemMatchProj: Bson get() = Projections.elemMatch(path()) +public val KProperty.elemMatchProj: Bson + get() = Projections.elemMatch(path()) /** - * Creates a projection that includes for the given property only the first element of the array value of that field that matches the given - * query filter. + * Creates a projection that includes for the given property only the first element of the array value of that field + * that matches the given query filter. * * @param filter the filter to apply - * @return the projection - * @mongodb.driver.manual reference/operator/projection/elemMatch elemMatch + * @return the projection @mongodb.driver.manual reference/operator/projection/elemMatch elemMatch */ public infix fun KProperty.elemMatchProj(filter: Bson): Bson = Projections.elemMatch(path(), filter) @@ -162,58 +149,53 @@ public infix fun KProperty.elemMatchProj(filter: Bson): Bson = Projection * Creates a $meta projection for the given property * * @param metaFieldName the meta field name - * @return the projection - * @mongodb.driver.manual reference/operator/aggregation/meta/ - * @since 4.1 + * @return the projection @mongodb.driver.manual reference/operator/aggregation/meta/ * @see #metaTextScore(String) * @see #metaSearchScore(String) * @see #metaVectorSearchScore(String) * @see #metaSearchHighlights(String) + * @since 4.1 */ public infix fun KProperty.meta(metaFieldName: String): Bson = Projections.meta(path(), metaFieldName) /** - * Creates a textScore projection for the given property, for use with text queries. - * Calling this method is equivalent to calling {@link #meta(String)} with {@code "textScore"} as the argument. + * Creates a textScore projection for the given property, for use with text queries. Calling this method is equivalent + * to calling {@link #meta(String)} with {@code "textScore"} as the argument. * * @return the projection - * @see Filters#text(String, TextSearchOptions) - * @mongodb.driver.manual reference/operator/aggregation/meta/#text-score-metadata--meta---textscore- textScore + * @see Filters#text(String, TextSearchOptions) @mongodb.driver.manual + * reference/operator/aggregation/meta/#text-score-metadata--meta---textscore- textScore */ public fun KProperty.metaTextScore(): Bson = Projections.metaTextScore(path()) /** - * Creates a searchScore projection for the given property, - * for use with {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. - * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "searchScore"} as the argument. + * Creates a searchScore projection for the given property, for use with {@link Aggregates#search(SearchOperator, + * SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. Calling this method is equivalent to + * calling {@link #meta(String, String)} with {@code "searchScore"} as the argument. * - * @return the projection - * @mongodb.atlas.manual atlas-search/scoring/ Scoring + * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring * @since 4.7 */ public fun KProperty.metaSearchScore(): Bson = Projections.metaSearchScore(path()) /** - * Creates a vectorSearchScore projection for the given property, - * for use with {@link Aggregates#vectorSearch(FieldSearchPath, Iterable, String, long, VectorSearchOptions)} . - * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "vectorSearchScore"} as the argument. + * Creates a vectorSearchScore projection for the given property, for use with {@link + * Aggregates#vectorSearch(FieldSearchPath, Iterable, String, long, VectorSearchOptions)} . Calling this method is + * equivalent to calling {@link #meta(String, String)} with {@code "vectorSearchScore"} as the argument. * - * @return the projection - * @mongodb.atlas.manual atlas-search/scoring/ Scoring - * @mongodb.server.release 6.0.10 + * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring @mongodb.server.release 6.0.10 * @since 4.11 */ @Beta(Reason.SERVER) public fun KProperty.metaVectorSearchScore(): Bson = Projections.metaVectorSearchScore(path()) /** - * Creates a searchHighlights projection for the given property, - * for use with {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. - * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "searchHighlights"} as the argument. + * Creates a searchHighlights projection for the given property, for use with {@link Aggregates#search(SearchOperator, + * SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. Calling this method is equivalent to + * calling {@link #meta(String, String)} with {@code "searchHighlights"} as the argument. * * @return the projection - * @see com.mongodb.client.model.search.SearchHighlight - * @mongodb.atlas.manual atlas-search/highlighting/ Highlighting + * @see com.mongodb.client.model.search.SearchHighlight @mongodb.atlas.manual atlas-search/highlighting/ Highlighting * @since 4.7 */ public fun KProperty.metaSearchHighlights(): Bson = Projections.metaSearchHighlights(path()) @@ -222,8 +204,7 @@ public fun KProperty.metaSearchHighlights(): Bson = Projections.metaSearc * Creates a projection to the given property of a slice of the array value of that field. * * @param limit the number of elements to project. - * @return the projection - * @mongodb.driver.manual reference/operator/projection/slice Slice + * @return the projection @mongodb.driver.manual reference/operator/projection/slice Slice */ public infix fun KProperty.slice(limit: Int): Bson = Projections.slice(path(), limit) @@ -232,14 +213,13 @@ public infix fun KProperty.slice(limit: Int): Bson = Projections.slice(pa * * @param skip the number of elements to skip before applying the limit * @param limit the number of elements to project - * @return the projection - * @mongodb.driver.manual reference/operator/projection/slice Slice + * @return the projection @mongodb.driver.manual reference/operator/projection/slice Slice */ public fun KProperty.slice(skip: Int, limit: Int): Bson = Projections.slice(path(), skip, limit) /** - * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last one takes - * precedence. + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last + * one takes precedence. * * @param projections the list of projections to combine * @return the combined projection @@ -247,11 +227,10 @@ public fun KProperty.slice(skip: Int, limit: Int): Bson = Projections.sli public fun fields(vararg projections: Bson): Bson = Projections.fields(*projections) /** - * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last one takes - * precedence. + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the last + * one takes precedence. * * @param projections the list of projections to combine - * @return the combined projection - * @mongodb.driver.manual + * @return the combined projection @mongodb.driver.manual */ public fun fields(projections: List): Bson = Projections.fields(projections) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt index 436b00a9cd3..ec429d6e42e 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt @@ -84,7 +84,8 @@ internal fun KProperty.path(): String { // If no path (serialName) then check for BsonId / BsonProperty if (path == null) { val originator = if (this is CustomProperty<*, *>) this.previous.property else this - // If this property is calculated (doesn't have a backing field) ex "(Student::grades / Grades::score).posOp then + // If this property is calculated (doesn't have a backing field) ex + // "(Student::grades / Grades::score).posOp then // originator.javaField will NPE. // Only read various annotations on a declared property with a backing field if (originator.javaField != null) { @@ -93,7 +94,8 @@ internal fun KProperty.path(): String { // Prefer BsonId annotation over BsonProperty path = constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.let { "_id" } - path = path ?: constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.value + path = + path ?: constructorProperty?.annotations?.filterIsInstance()?.firstOrNull()?.value } path = path ?: this.name } diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt index 8c1713d422f..bd621a3c29d 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt @@ -23,10 +23,10 @@ import com.mongodb.client.model.Projections import com.mongodb.kotlin.client.model.Filters.and import com.mongodb.kotlin.client.model.Filters.eq import com.mongodb.kotlin.client.model.Filters.gt +import kotlin.test.assertEquals import org.bson.BsonDocument import org.bson.conversions.Bson import org.junit.Test -import kotlin.test.assertEquals class ProjectionTest { @@ -38,17 +38,18 @@ class ProjectionTest { @Test fun include() { - assertEquals("""{"name": 1, "age": 1, "results": 1, "address": 1}""", + assertEquals( + """{"name": 1, "age": 1, "results": 1, "address": 1}""", include(Person::name, Person::age, Person::results, Person::address)) assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age))) assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age, Person::name))) - } @Test fun exclude() { - assertEquals("""{"name": 0, "age": 0, "results": 0, "address": 0}""", + assertEquals( + """{"name": 0, "age": 0, "results": 0, "address": 0}""", exclude(Person::name, Person::age, Person::results, Person::address)) assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age))) assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) @@ -56,7 +57,9 @@ class ProjectionTest { assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) assertEquals("""{"_id": 0}""", excludeId()) - assertEquals("Projections{projections=[{\"_id\": 0}, {\"name\": 1}]}", fields(excludeId(), include(Person::name)).toString()) + assertEquals( + "Projections{projections=[{\"_id\": 0}, {\"name\": 1}]}", + fields(excludeId(), include(Person::name)).toString()) } @Test @@ -80,7 +83,8 @@ class ProjectionTest { */ // This projection: - val expected = """ + val expected = + """ {"grades": {"${'$'}elemMatch": {"${'$'}and": [{"subject": "Math"}, {"score": {"${'$'}gt": 80}}]}}} """ @@ -94,18 +98,14 @@ class ProjectionTest { } */ - assertEquals( - expected, - Student::grades.elemMatchProj(and((Grade::subject eq "Math"), (Grade::score gt 80))) - ) + assertEquals(expected, Student::grades.elemMatchProj(and((Grade::subject eq "Math"), (Grade::score gt 80)))) // Should create string representation for elemMatch with filter assertEquals( "ElemMatch Projection{fieldName='grades'," + - " filter=And Filter{filters=[Filter{fieldName='score', value=90}, " + - "Filter{fieldName='subject', value=Math}]}}", - Student::grades.elemMatchProj(and(Grade::score eq 90, Grade::subject eq "Math")).toString() - ) + " filter=And Filter{filters=[Filter{fieldName='score', value=90}, " + + "Filter{fieldName='subject', value=Math}]}}", + Student::grades.elemMatchProj(and(Grade::score eq 90, Grade::subject eq "Math")).toString()) } @Test @@ -154,10 +154,7 @@ class ProjectionTest { } */ - assertEquals( - expected, - Student::grades.slice(1, 2) - ) + assertEquals(expected, Student::grades.slice(1, 2)) // Combining projection expected = """ @@ -187,17 +184,15 @@ class ProjectionTest { assertEquals(expected, Grade::score.metaTextScore()) // combining - expected = """ + expected = + """ {"_id": 0, "score": {"${'$'}meta": "textScore"}, "grades": {"${'$'}elemMatch": {"score": {"${'$'}gt": 87}}}} """ assertEquals( expected, - fields( - excludeId(), Grade::score.metaTextScore(), - Student::grades.elemMatchProj(Grade::score gt 87) - ) - ) - // find({ $text: { $search: "Doe" } }, { _id: 0, score: { $meta: "textScore" }, grades: {$elemMatch: { score : {$gt: 87}}} } ) + fields(excludeId(), Grade::score.metaTextScore(), Student::grades.elemMatchProj(Grade::score gt 87))) + // find({ $text: { $search: "Doe" } }, { _id: 0, score: { $meta: "textScore" }, grades: + // {$elemMatch: { score : {$gt: 87}}} } ) /* { score: 0.75, grades: [ { subject: 'English', score: 90 } ] } */ @@ -228,22 +223,16 @@ class ProjectionTest { fun `computed projection`() { assertEquals(""" {"c": "${'$'}y"} """, "c" computedFrom "\$y") - assertEquals("""{"${'$'}project": {"c": "${'$'}name", "score": "${'$'}age"}}""", - project(fields("c" computedFrom Student::name, Grade::score computedFrom Student::age)) - ) + assertEquals( + """{"${'$'}project": {"c": "${'$'}name", "score": "${'$'}age"}}""", + project(fields("c" computedFrom Student::name, Grade::score computedFrom Student::age))) // combine fields - assertEquals("{name : 1, age : 1, _id : 0}", - fields(include(Student::name, Student::age), excludeId()) - ) + assertEquals("{name : 1, age : 1, _id : 0}", fields(include(Student::name, Student::age), excludeId())) - assertEquals("{name : 1, age : 1, _id : 0}", - fields(include(listOf(Student::name, Student::age)), excludeId()) - ) + assertEquals("{name : 1, age : 1, _id : 0}", fields(include(listOf(Student::name, Student::age)), excludeId())) - assertEquals("{name : 1, age : 0}", - fields(include(Student::name, Student::age), exclude(Student::age)) - ) + assertEquals("{name : 1, age : 0}", fields(include(Student::name, Student::age), exclude(Student::age))) // Should create string representation for include and exclude assertEquals("""{"age": 1, "name": 1}""", include(Student::name, Student::age, Student::name).toString()) @@ -258,42 +247,35 @@ class ProjectionTest { @Test fun `array projection`() { - assertEquals( - "{ \"grades.comments.${'$'}\": 1 }", - include((Student::grades / Grade::comments).posOp) - ) - + assertEquals("{ \"grades.comments.${'$'}\": 1 }", include((Student::grades / Grade::comments).posOp)) - // FIXME: this form is not supported "MongoServerError[Location31395]: positional projection cannot be used with exclusion" - // should we fail before hitting the wire protocol? the existing Java impl (com/mongodb/client/model/Projections.java) + // This form is not supported "MongoServerError[Location31395]: positional projection + // cannot be used with exclusion" + // should we fail before hitting the wire protocol? the existing Java impl + // (com/mongodb/client/model/Projections.java) // doesn't do similar checks AFAIK. - // Likewise using Student::grades.allPosOp should not be allowed ("FieldPath field names may not start with '$'. Consider + // Likewise using Student::grades.allPosOp should not be allowed ("FieldPath field + // names may not start with '$'. Consider // using $getField or $setField") // - assertEquals( - "{ \"grades.${'$'}\": 0 }", - exclude(Student::grades.posOp) - ) + assertEquals("{ \"grades.${'$'}\": 0 }", exclude(Student::grades.posOp)) } - @Test - fun `projection in aggregation`() { + @Test + fun `projection in aggregation`() { // Field Reference in Aggregation - assertEquals(""" {"${'$'}project": {"score": "${'$'}grades.score"}} """, - project((Grade::score computedFrom (Student::grades.projectionWith("score")))) - ) + assertEquals( + """ {"${'$'}project": {"score": "${'$'}grades.score"}} """, + project((Grade::score computedFrom (Student::grades.projectionWith("score"))))) - assertEquals("{\"${'$'}project\": {\"My Score\": \"${'$'}grades.score\"}}", - project("My Score" computedFrom (Student::grades / Grade::score)) - ) + assertEquals( + "{\"${'$'}project\": {\"My Score\": \"${'$'}grades.score\"}}", + project("My Score" computedFrom (Student::grades / Grade::score))) - assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", - project("My Age" computedFrom Student::age) - ) + assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", project("My Age" computedFrom Student::age)) - assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", - project("My Age" computedFrom Student::age.projection) - ) + assertEquals( + "{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", project("My Age" computedFrom Student::age.projection)) } // TODO maybe move these inside a Utils file @@ -302,10 +284,5 @@ class ProjectionTest { private data class Grade(val subject: String, val score: Int, val comments: List) private fun assertEquals(expected: String, result: Bson) = - assertEquals( - BsonDocument.parse(expected), - result.toBsonDocument() - ) + assertEquals(BsonDocument.parse(expected), result.toBsonDocument()) } - - From d070cb1781970db2b547249a540716d487991746 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 27 Sep 2024 12:14:23 +0100 Subject: [PATCH 3/8] Exclude false positive spotbug warring --- config/spotbugs/exclude.xml | 6 ++++++ driver-kotlin-extensions/build.gradle.kts | 7 +++++++ .../kotlin/com/mongodb/kotlin/client/model/Properties.kt | 1 - 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index f887757652f..d8586c497f1 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -229,6 +229,12 @@ + + + + + + - + From 09cf6ad9223f21f07c6824b127aa40a83c5478c5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 3 Oct 2024 09:53:50 +0100 Subject: [PATCH 8/8] Update since annotations --- .../com/mongodb/kotlin/client/model/Projections.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt index 430324c7a8f..d8f09d73be1 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt @@ -26,6 +26,11 @@ import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 import org.bson.conversions.Bson +/** + * Projection extension methods to improve Kotlin interop + * + * @since 5.3 + */ public object Projections { /** The projection of the property. This is used in an aggregation pipeline to reference a property from a path. */ @@ -246,7 +251,6 @@ public object Projections { * @see #metaSearchScore(String) * @see #metaVectorSearchScore(String) * @see #metaSearchHighlights(String) - * @since 4.1 */ @JvmSynthetic @JvmName("metaExt") @@ -262,7 +266,6 @@ public object Projections { * @see #metaSearchScore(String) * @see #metaVectorSearchScore(String) * @see #metaSearchHighlights(String) - * @since 4.1 */ public fun meta(property: KProperty, metaFieldName: String): Bson = property.meta(metaFieldName) @@ -282,7 +285,6 @@ public object Projections { * calling {@link #meta(String, String)} with {@code "searchScore"} as the argument. * * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring - * @since 4.7 */ public fun KProperty.metaSearchScore(): Bson = Projections.metaSearchScore(path()) @@ -292,7 +294,6 @@ public object Projections { * equivalent to calling {@link #meta(String, String)} with {@code "vectorSearchScore"} as the argument. * * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring @mongodb.server.release 6.0.10 - * @since 4.11 */ @Beta(Reason.SERVER) public fun KProperty.metaVectorSearchScore(): Bson = Projections.metaVectorSearchScore(path()) @@ -306,7 +307,6 @@ public object Projections { * @return the projection * @see com.mongodb.client.model.search.SearchHighlight @mongodb.atlas.manual atlas-search/highlighting/ * Highlighting - * @since 4.7 */ public fun KProperty.metaSearchHighlights(): Bson = Projections.metaSearchHighlights(path())