Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Union with #61

Merged
merged 2 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,20 @@ class AggregationDsl {
operations += UnsetOperation.unset(*PathSearchOptionDsl<Any>().apply(pathConfiguration).get().toTypedArray())
}

/**
* Configures a stage that performs a union of two collections.
* `$unionWith` combines pipeline results from two collections into a single result set.
* The stage outputs the combined result set (including duplicates) to the next stage.
*
* The order in which the combined result set documents are output is unspecified.
*
* @param unionWithConfiguration The configuration block for the [UnionWithStageDsl].
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/unionWith/">$unionWith (aggregation)</a>
*/
fun unionWith(unionWithConfiguration: UnionWithStageDsl.() -> Unit) {
UnionWithStageDsl().apply(unionWithConfiguration).build()?.let { operations += it }
}

/**
* Builds the [Aggregation] using the configured [AggregationOperation]s.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let
import org.springframework.data.mongodb.core.mapping.Document
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation

/**
* A Kotlin DSL to configure $lookup stage using idiomatic Kotlin code.
Expand Down Expand Up @@ -39,7 +40,7 @@ class LookupStageDsl {
* @see <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/#std-label-documents-lookup-example">Use a `$documents` Stage in a `$lookup` Stage</a>
*/
fun from(from: KClass<*>) {
val annotation = from.annotations.firstNotNullOfOrNull { it as? Document }
val annotation = from.findAnnotation<Document>()
operation.from(
annotation?.collection?.ifEmpty { annotation.value.takeIf { it.isNotEmpty() } }
?: from.simpleName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.github.inflab.spring.data.mongodb.core.aggregation

import com.github.inflab.spring.data.mongodb.core.annotation.AggregationMarker
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline
import org.springframework.data.mongodb.core.aggregation.UnionWithOperation
import org.springframework.data.mongodb.core.mapping.Document
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation

/**
* A Kotlin DSL to configure $unionWith stage using idiomatic Kotlin code.
*
* @author Jake Son
* @since 1.0
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/unionWith">$unionWith (aggregation)</a>
*/
@AggregationMarker
class UnionWithStageDsl {
private var collection: String? = null
private var pipeline: AggregationPipeline? = null

/**
* The collection or view whose `pipeline` results you wish to include in the result set.
*
* @param collection The collection name.
* @see <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#std-label-unionWith-coll">coll</a>
*/
fun coll(collection: String) {
this.collection = collection
}

/**
* The collection or view whose `pipeline` results you wish to include in the result set.
*
* @param collection The collection class.
* @see <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#std-label-unionWith-coll">coll</a>
*/
fun coll(collection: KClass<*>) {
val annotation = collection.findAnnotation<Document>()
this.collection = annotation?.collection?.ifEmpty { annotation.value.takeIf { it.isNotEmpty() } }
?: collection.simpleName
}

/**
* An aggregation pipeline to apply to the specified [coll].
*
* `[ <stage1>, <stage2>, ...]`
*
* The pipeline cannot include the `$out` and `$merge` stages.
* Starting in v6.0, the `pipeline` can contain the `Atlas Search $search` stage as the first stage inside the pipeline.
*
* @param configuration The configuration for [AggregationDsl].
* @see <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#std-label-unionWith-pipeline">pipeline</a>
*/
fun pipeline(configuration: AggregationDsl.() -> Unit) {
pipeline = AggregationDsl().apply(configuration).build().pipeline
}

internal fun build(): UnionWithOperation? {
val operation = collection?.let { UnionWithOperation.unionWith(it) } ?: return null

return pipeline?.let { operation.pipeline(it) } ?: operation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.kotest.matchers.shouldBe
import org.bson.Document
import org.springframework.data.mongodb.core.aggregation.Aggregation
import org.springframework.data.mongodb.core.aggregation.StringOperators
import org.springframework.data.mongodb.core.aggregation.UnionWithOperation
import org.springframework.data.mongodb.core.aggregation.UnsetOperation
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.TextCriteria
Expand Down Expand Up @@ -268,4 +269,20 @@ internal class AggregationDslTest : FreeSpec({
).toString()
}
}

"unionWith" - {
"should create unionWith stage" {
// when
val aggregation = aggregation {
unionWith {
coll("collectionName")
}
}

// then
aggregation.toString() shouldBe Aggregation.newAggregation(
UnionWithOperation.unionWith("collectionName"),
).toString()
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package com.github.inflab.spring.data.mongodb.core.aggregation

import com.github.inflab.spring.data.mongodb.core.util.shouldBeJson
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import org.springframework.data.mongodb.core.mapping.Document

internal class UnionWithStageDslTest : FreeSpec({
fun unionWith(block: UnionWithStageDsl.() -> Unit) =
UnionWithStageDsl().apply(block)

"coll" - {
"should add a collection by string" {
// given
val stage = unionWith {
coll("collection")
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "collection"
}
}
""".trimIndent(),
)
}

"should add a collection by class" {
// given
data class Collection(val property: String)
val stage = unionWith {
coll(Collection::class)
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "Collection"
}
}
""".trimIndent(),
)
}

"should add a collection by class with @Document annotation" {
// given
@Document("collection")
data class Collection(val property: String)
val stage = unionWith {
coll(Collection::class)
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "collection"
}
}
""".trimIndent(),
)
}

"should add a collection by class with @Document annotation with value" {
// given
@Document(value = "collection")
data class Collection(val property: String)
val stage = unionWith {
coll(Collection::class)
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "collection"
}
}
""".trimIndent(),
)
}

"should add a collection by class with @Document annotation with collection" {
// given
@Document(collection = "collection")
data class Collection(val property: String)
val stage = unionWith {
coll(Collection::class)
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "collection"
}
}
""".trimIndent(),
)
}

"should use class name if @Document annotation's properties are empty" {
// given
@Document(collection = "", value = "")
data class Collection(val property: String)

val stage = unionWith {
coll(Collection::class)
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "Collection"
}
}
""".trimIndent(),
)
}
}

"pipeline" - {
"should not build stage if collection is not given" {
// given
val stage = unionWith {
pipeline {
count("fieldName")
}
}

// when
val result = stage.build()

// then
result.shouldBeNull()
}

"should build stage if collection is given" {
// given
val stage = unionWith {
coll("collection")
pipeline {
count("fieldName")
}
}

// when
val result = stage.build()

// then
result.shouldNotBeNull()
result.shouldBeJson(
"""
{
"${'$'}unionWith": {
"coll": "collection",
"pipeline": [
{
"${'$'}count": "fieldName"
}
]
}
}
""".trimIndent(),
)
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ class LookupRepository(
}
}

print(aggregation.toString())

return mongoTemplate.aggregate(aggregation, ORDERS, OrderDto::class.java)
}

Expand Down
Loading