From 8f161d5818e2aeee9565deef720c0c60ef71a060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 2 Aug 2026 10:16:48 +0200 Subject: [PATCH] feat(dsl): Add 'onEach' clause to simplify dealing with arrays --- .../kotlin/aggregation/stages/Lookup.kt | 220 ++++++++++++++++++ .../kotlin/aggregation/stages/Lookup.kt | 220 ++++++++++++++++++ .../kotlin/studies/AggregationLookup.kt | 8 +- 3 files changed, 441 insertions(+), 7 deletions(-) diff --git a/dsl-template/src/commonMain/kotlin/aggregation/stages/Lookup.kt b/dsl-template/src/commonMain/kotlin/aggregation/stages/Lookup.kt index 491e16c1..b926c948 100644 --- a/dsl-template/src/commonMain/kotlin/aggregation/stages/Lookup.kt +++ b/dsl-template/src/commonMain/kotlin/aggregation/stages/Lookup.kt @@ -447,6 +447,8 @@ interface LookupStageOperators : Com value: Value, ): Value = let(value, null) + // region 'on' clause + /** * Specifies that the field [foreignField] in the [foreign collection][from] * must have a value that is strictly equal to the value of the local field [localField]. @@ -660,6 +662,224 @@ interface LookupStageOperators : Com foreignField = foreignField.field, ) + // endregion + // region 'on' clause on array + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: Field>, + foreignField: Field, + ) { + // MongoDB decides on this behavior based on the actual type at read time, the query syntax is the exact same + on(localField.unsafeCast(), foreignField) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: KProperty1>, + foreignField: Field, + ) { + onEach(localField.field, foreignField) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: Field>, + foreignField: KProperty1, + ) { + onEach(localField, foreignField.field) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: KProperty1>, + foreignField: KProperty1, + ) { + onEach(localField.field, foreignField) + } + + // endregion + } @OptIn(LowLevelApi::class) diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Lookup.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Lookup.kt index 18341d70..b97d2293 100644 --- a/dsl/src/commonMain/kotlin/aggregation/stages/Lookup.kt +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Lookup.kt @@ -762,6 +762,8 @@ interface LookupStageOperators : Com ): Value = let(of(value)) + // region 'on' clause + /** * Specifies that the field [foreignField] in the [foreign collection][from] * must have a value that is strictly equal to the value of the local field [localField]. @@ -975,6 +977,224 @@ interface LookupStageOperators : Com foreignField = foreignField.field, ) + // endregion + // region 'on' clause on array + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: Field>, + foreignField: Field, + ) { + // MongoDB decides on this behavior based on the actual type at read time, the query syntax is the exact same + on(localField.unsafeCast(), foreignField) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: KProperty1>, + foreignField: Field, + ) { + onEach(localField.field, foreignField) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: Field>, + foreignField: KProperty1, + ) { + onEach(localField, foreignField.field) + } + + /** + * Specifies that the field [foreignField] in the [foreign collection][from] + * must have a value that is strictly equal to one of the elements of the local array [localField]. + * + * To learn more about the `$lookup` stage, see [HasLookup.lookup]. + * + * ### Example + * + * ```kotlin + * class Department( + * val _id: ObjectId, + * val name: String, + * ) + * + * class User( + * val _id: ObjectId, + * val name: String, + * val departmentIds: List, + * val departments: List, + * ) + * + * users.aggregate() + * .lookup { + * into(User::departments) + * from(departments.aggregate()) + * onEach(User::departmentIds, Department::_id) + * } + * ``` + * + * The `on` operator is semantically equivalent to a [match][HasMatch.matchExpr] stage using a [let] binding: + * ```kotlin + * users.aggregate() + * .lookup { + * into(User::departments) + * val departmentIds = let(User::departmentIds) + * from( + * departments.aggregate() + * .matchExpr { Department::_id isOneOf departmentIds } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array) + */ + fun onEach( + localField: KProperty1>, + foreignField: KProperty1, + ) { + onEach(localField.field, foreignField) + } + + // endregion + } @OptIn(LowLevelApi::class) diff --git a/test-legacy/src/commonTest/kotlin/studies/AggregationLookup.kt b/test-legacy/src/commonTest/kotlin/studies/AggregationLookup.kt index 674b39bc..730638ed 100644 --- a/test-legacy/src/commonTest/kotlin/studies/AggregationLookup.kt +++ b/test-legacy/src/commonTest/kotlin/studies/AggregationLookup.kt @@ -21,7 +21,6 @@ package opensavvy.ktmongo.sync.studies import kotlinx.serialization.Serializable import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.coroutines.toList -import opensavvy.ktmongo.dsl.path.Field import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite import kotlin.time.Instant @@ -167,18 +166,13 @@ val AggregationLookup by preparedSuite { SchoolClass(_id = writing, title = "But Writing ...", students = listOf(sherlock, madame)), ) - // When 'students' is an array, MongoDB automatically expands each element and matches it - // against the foreign '_id' field. Field.unsafe is required because the Kotlin type - // of the local field (List) differs from the join key type (ObjectId). - val studentsArrayField = Field.unsafe("students") - val allStudents = students().aggregate() val result = classes().aggregate() .lookup { into(SchoolClass::studentsData) from(allStudents) - on(studentsArrayField, SchoolStudent::_id) + onEach(SchoolClass::students, SchoolStudent::_id) } .toList() -- 2.51.2