From 64fb0b642b76b5ab68143a9b5172ea8503132911 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Fri, 27 Mar 2026 16:59:22 +0100 Subject: [PATCH 01/34] MongoDB indexes support --- .../annotation/MongoCompoundIndex.java | 73 ++++ .../annotation/MongoCompoundIndexField.java | 66 ++++ .../annotation/MongoCompoundIndexes.java | 41 +++ .../mongodb/annotation/MongoGeoIndexType.java | 40 +++ .../mongodb/annotation/MongoGeoIndexed.java | 61 ++++ .../annotation/MongoHashedIndexed.java | 41 +++ .../annotation/MongoIndexDirection.java | 27 ++ .../data/mongodb/annotation/MongoIndexed.java | 71 ++++ .../mongodb/annotation/MongoTextIndexed.java | 46 +++ .../annotation/MongoWildcardIndex.java | 46 +++ .../annotation/MongoWildcardIndexed.java | 42 +++ .../mongodb/common/MongoEntityIndexes.java | 338 ++++++++++++++++++ .../mongodb/conf/MongoDataConfiguration.java | 16 +- .../data/mongodb/geo/MongoGeoPoint.java | 30 ++ .../mongodb/geo/MongoGeoPointConverter.java | 60 ++++ .../data/mongodb/geo/MongoGeoPointLike.java | 35 ++ .../init/AbstractMongoCollectionsCreator.java | 203 +++++++++-- .../mongodb/init/MongoCollectionsCreator.java | 94 ++++- .../init/MongoReactiveCollectionsCreator.java | 102 +++++- .../mongodb/MongoIndexInspector.groovy | 44 +++ .../MongoCollationIndexCreationSpec.groovy | 71 ++++ .../MongoCompoundIndexCreationSpec.groovy | 84 +++++ .../geo/MongoGeoIndexCreationSpec.groovy | 69 ++++ .../geo2d/MongoGeo2dIndexCreationSpec.groovy | 70 ++++ .../MongoGeo2dOptionsIndexCreationSpec.groovy | 69 ++++ .../MongoCompoundGeoIndexCreationSpec.groovy | 81 +++++ ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 83 +++++ ...MongoGeoPointValueIndexCreationSpec.groovy | 75 ++++ ...ustomGeoPointValueIndexCreationSpec.groovy | 91 +++++ .../MongoHashedIndexCreationSpec.groovy | 69 ++++ ...MongoPartialFilterIndexCreationSpec.groovy | 72 ++++ ...goReactiveCompoundIndexCreationSpec.groovy | 81 +++++ .../MongoReactiveIndexCreationSpec.groovy | 67 ++++ ...oReactiveCollationIndexCreationSpec.groovy | 71 ++++ .../MongoReactiveGeoIndexCreationSpec.groovy | 69 ++++ ...MongoReactiveGeo2dIndexCreationSpec.groovy | 70 ++++ ...activeGeo2dOptionsIndexCreationSpec.groovy | 71 ++++ ...eactiveCompoundGeoIndexCreationSpec.groovy | 81 +++++ ...ctiveGeoPointValueIndexCreationSpec.groovy | 75 ++++ ...ustomGeoPointValueIndexCreationSpec.groovy | 91 +++++ ...ongoReactiveHashedIndexCreationSpec.groovy | 69 ++++ ...ctivePartialFilterIndexCreationSpec.groovy | 73 ++++ .../MongoReactiveTextIndexCreationSpec.groovy | 68 ++++ ...goReactiveWildcardIndexCreationSpec.groovy | 68 ++++ .../simple/MongoIndexCreationSpec.groovy | 71 ++++ ...ongoAggregatedTextIndexCreationSpec.groovy | 71 ++++ .../text/MongoTextIndexCreationSpec.groovy | 68 ++++ .../MongoCollationValidationSpec.groovy | 45 +++ ...ndIndexDuplicateFieldValidationSpec.groovy | 54 +++ ...poundIndexEmptyFieldsValidationSpec.groovy | 45 +++ .../MongoCompoundGeoValidationSpec.groovy | 56 +++ ...oCompoundGeo2dOptionsValidationSpec.groovy | 55 +++ ...ngoCompoundGeoOptionsValidationSpec.groovy | 55 +++ ...oCompoundGeo2dOptionsValidationSpec.groovy | 55 +++ .../MongoGeoIndexValidationSpec.groovy | 46 +++ .../MongoCompoundIndexValidationSpec.groovy | 53 +++ ...undIndexPartialFilterValidationSpec.groovy | 54 +++ .../text/MongoTextIndexValidationSpec.groovy | 46 +++ ...MongoCompoundIndexTtlValidationSpec.groovy | 56 +++ .../MongoWildcardIndexCreationSpec.groovy | 68 ++++ ...goTopLevelWildcardIndexCreationSpec.groovy | 70 ++++ ...WildcardProjectionIndexCreationSpec.groovy | 73 ++++ 62 files changed, 4274 insertions(+), 31 deletions(-) create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java new file mode 100644 index 00000000000..d9e4f4103b0 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a compound MongoDB index for an entity. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +@Repeatable(MongoCompoundIndexes.class) +public @interface MongoCompoundIndex { + + /** + * @return The index name. + */ + String name() default ""; + + /** + * @return The fields. + */ + MongoCompoundIndexField[] fields(); + + /** + * @return Whether the index is unique. + */ + boolean unique() default false; + + /** + * @return Whether the index is sparse. + */ + boolean sparse() default false; + + /** + * @return The index expiration in seconds. + */ + int expireAfterSeconds() default -1; + + /** + * @return The partial filter expression as JSON. + */ + String partialFilterExpression() default ""; + + /** + * @return The collation definition as JSON. + */ + String collation() default ""; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java new file mode 100644 index 00000000000..b9beeb5e470 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares a field within a compound MongoDB index. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MongoCompoundIndexField { + + /** + * @return The property path. + */ + String value(); + + /** + * @return The field direction. + */ + MongoIndexDirection direction() default MongoIndexDirection.ASC; + + /** + * @return The geospatial key kind when this field should use a geospatial index key inside a compound index. + */ + MongoGeoIndexType geoType() default MongoGeoIndexType.GEO_2DSPHERE; + + /** + * @return Whether the field should use the geospatial key kind instead of the numeric direction. + */ + boolean geo() default false; + + /** + * @return The 2d index bits setting, or -1 if unset. + */ + int bits() default -1; + + /** + * @return The 2d index minimum value, or NaN if unset. + */ + double min() default Double.NaN; + + /** + * @return The 2d index maximum value, or NaN if unset. + */ + double max() default Double.NaN; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java new file mode 100644 index 00000000000..068bc9cf3d4 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Repeatable annotation for {@link MongoCompoundIndex}. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface MongoCompoundIndexes { + + /** + * @return The indexes. + */ + MongoCompoundIndex[] value() default {}; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java new file mode 100644 index 00000000000..f9133a84dc9 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +/** + * Supported MongoDB geospatial index kinds. + * + * @author radovanradic + * @since 5.0.0 + */ +public enum MongoGeoIndexType { + GEO_2D("2d"), + GEO_2DSPHERE("2dsphere"); + + private final String key; + + MongoGeoIndexType(String key) { + this.key = key; + } + + /** + * @return The MongoDB key value. + */ + public String getKey() { + return key; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java new file mode 100644 index 00000000000..3061535de29 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a simple MongoDB geospatial index for a property. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoGeoIndexed { + + /** + * @return The index name. + */ + String name() default ""; + + /** + * @return The geospatial index kind. + */ + MongoGeoIndexType type() default MongoGeoIndexType.GEO_2DSPHERE; + + /** + * @return The 2d index bits setting, or -1 if unset. + */ + int bits() default -1; + + /** + * @return The 2d index minimum value, or NaN if unset. + */ + double min() default Double.NaN; + + /** + * @return The 2d index maximum value, or NaN if unset. + */ + double max() default Double.NaN; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java new file mode 100644 index 00000000000..e706dc41185 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a simple MongoDB hashed index for a property. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoHashedIndexed { + + /** + * @return The index name. + */ + String name() default ""; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java new file mode 100644 index 00000000000..3143697cce0 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +/** + * MongoDB index direction. + * + * @author radovanradic + * @since 5.0.0 + */ +public enum MongoIndexDirection { + ASC, + DESC +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java new file mode 100644 index 00000000000..88347ff7320 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a simple MongoDB index for a property. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoIndexed { + + /** + * @return The index name. + */ + String name() default ""; + + /** + * @return The index direction. + */ + MongoIndexDirection direction() default MongoIndexDirection.ASC; + + /** + * @return Whether the index is unique. + */ + boolean unique() default false; + + /** + * @return Whether the index is sparse. + */ + boolean sparse() default false; + + /** + * @return The index expiration in seconds. + */ + int expireAfterSeconds() default -1; + + /** + * @return The partial filter expression as JSON. + */ + String partialFilterExpression() default ""; + + /** + * @return The collation definition as JSON. + */ + String collation() default ""; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java new file mode 100644 index 00000000000..6049fb71863 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a simple MongoDB text index for a property. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoTextIndexed { + + /** + * @return The index name. + */ + String name() default ""; + + /** + * @return The text index weight. + */ + int weight() default 1; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java new file mode 100644 index 00000000000..5d4866758b3 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a top-level MongoDB wildcard index for an entity. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoWildcardIndex { + + /** + * @return The index name. + */ + String name() default ""; + + /** + * @return The wildcard projection definition as JSON. + */ + String wildcardProjection() default ""; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java new file mode 100644 index 00000000000..eda844178b3 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a simple MongoDB wildcard index for a property. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoWildcardIndexed { + + /** + * @return The index name. + */ + String name() default ""; + +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java new file mode 100644 index 00000000000..e798fa942dc --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -0,0 +1,338 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.common; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanProperty; +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex; +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField; +import io.micronaut.data.mongodb.annotation.MongoIndexDirection; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType; +import io.micronaut.data.mongodb.annotation.MongoHashedIndexed; +import io.micronaut.data.mongodb.annotation.MongoIndexed; +import io.micronaut.data.mongodb.annotation.MongoTextIndexed; +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex; +import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed; +import io.micronaut.data.model.Association; +import io.micronaut.data.model.PersistentEntityUtils; +import io.micronaut.data.model.runtime.RuntimePersistentEntity; +import io.micronaut.data.model.runtime.RuntimePersistentProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Cached Mongo index metadata resolved at runtime. + * + * @author radovanradic + * @since 5.0.0 + */ +@Internal +public final class MongoEntityIndexes { + + + private static final Map, MongoEntityIndexes> INDEXES_BY_ENTITY = new ConcurrentHashMap<>(); + + private final List indexes; + + private MongoEntityIndexes(List indexes) { + this.indexes = indexes; + } + + /** + * Resolve Mongo indexes for the entity. + * + * @param entity The runtime entity + * @return The resolved indexes + */ + public static MongoEntityIndexes create(RuntimePersistentEntity entity) { + return INDEXES_BY_ENTITY.computeIfAbsent(entity, MongoEntityIndexes::resolve); + } + + /** + * @return The resolved indexes. + */ + public List getIndexes() { + return indexes; + } + + private static MongoEntityIndexes resolve(RuntimePersistentEntity entity) { + List indexes = new ArrayList<>(); + indexes.addAll(resolveFieldIndexes(entity)); + indexes.addAll(resolveTopLevelWildcardIndexes(entity)); + indexes.addAll(resolveTextIndexes(entity)); + indexes.addAll(resolveCompoundIndexes(entity)); + return new MongoEntityIndexes(List.copyOf(indexes)); + } + + private static List resolveTopLevelWildcardIndexes(RuntimePersistentEntity entity) { + List indexes = new ArrayList<>(); + var annotation = entity.getAnnotationMetadata().getAnnotation(MongoWildcardIndex.class); + if (annotation != null) { + indexes.add(new ResolvedIndex( + annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField("$**", 1, null, null, null, null)), + false, + false, + null, + null, + null, + null, + null, + null, + annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null) + )); + } + return indexes; + } + + private static List resolveFieldIndexes(RuntimePersistentEntity entity) { + List indexes = new ArrayList<>(); + BeanIntrospection introspection = entity.getIntrospection(); + for (BeanProperty beanProperty : introspection.getBeanProperties()) { + RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); + if (property == null || property instanceof Association) { + continue; + } + var annotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoIndexed.class); + if (annotation != null) { + indexes.add(new ResolvedIndex( + annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField(property.getPersistedName(), annotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC) == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)), + annotation.booleanValue("unique").orElse(false), + annotation.booleanValue("sparse").orElse(false), + annotation.intValue("expireAfterSeconds").isPresent() && annotation.intValue("expireAfterSeconds").getAsInt() >= 0 ? annotation.intValue("expireAfterSeconds").getAsInt() : null, + annotation.stringValue("partialFilterExpression").filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), + null, + null, + null, + null + )); + continue; + } + var textAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); + if (textAnnotation != null) { + continue; + } + var hashedAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoHashedIndexed.class); + if (hashedAnnotation != null) { + indexes.add(new ResolvedIndex( + hashedAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField(property.getPersistedName(), null, null, "hashed", null, null)), + false, + false, + null, + null, + null, + null, + null, + null, + null + )); + continue; + } + var geoAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoGeoIndexed.class); + if (geoAnnotation != null) { + MongoGeoIndexType type = geoAnnotation.enumValue("type", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); + Integer bits = geoAnnotation.intValue("bits").isPresent() && geoAnnotation.intValue("bits").getAsInt() >= 0 ? geoAnnotation.intValue("bits").getAsInt() : null; + Double min = geoAnnotation.doubleValue("min").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("min").getAsDouble()) ? geoAnnotation.doubleValue("min").getAsDouble() : null; + Double max = geoAnnotation.doubleValue("max").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("max").getAsDouble()) ? geoAnnotation.doubleValue("max").getAsDouble() : null; + if (type != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { + throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d indexes on entity [" + entity.getName() + "]"); + } + indexes.add(new ResolvedIndex( + geoAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField(property.getPersistedName(), null, null, type.getKey(), min, max)), + false, + false, + null, + null, + null, + bits, + min, + max, + null + )); + continue; + } + var wildcardAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoWildcardIndexed.class); + if (wildcardAnnotation != null) { + indexes.add(new ResolvedIndex( + wildcardAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField(property.getPersistedName() + ".$**", 1, null, null, null, null)), + false, + false, + null, + null, + null, + null, + null, + null, + null + )); + } + } + return indexes; + } + + private static List resolveTextIndexes(RuntimePersistentEntity entity) { + List fields = new ArrayList<>(); + String name = null; + BeanIntrospection introspection = entity.getIntrospection(); + for (BeanProperty beanProperty : introspection.getBeanProperties()) { + RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); + if (property == null || property instanceof Association) { + continue; + } + var textAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); + if (textAnnotation != null) { + int weight = textAnnotation.intValue("weight").orElse(1); + if (weight <= 0) { + throw new IllegalStateException("Mongo text index weight must be greater than zero for entity [" + entity.getName() + "]"); + } + String declaredName = textAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null); + if (name == null) { + name = declaredName; + } else if (declaredName != null && !declaredName.equals(name)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same index name"); + } + fields.add(new ResolvedIndexField(property.getPersistedName(), null, weight, "text", null, null)); + } + } + if (fields.isEmpty()) { + return List.of(); + } + return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, null, null, null, null, null, null, null)); + } + + private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { + List indexes = new ArrayList<>(); + for (var annotationValue : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoCompoundIndex.class)) { + List fields = new ArrayList<>(); + java.util.Set seenPaths = new java.util.LinkedHashSet<>(); + for (var fieldAnnotation : annotationValue.getAnnotations("fields", MongoCompoundIndexField.class)) { + String path = fieldAnnotation.stringValue().orElseThrow(); + String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, path) + .map(persistentPath -> { + var propertyPath = entity.getPropertyPath(persistentPath); + if (propertyPath == null) { + throw new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]"); + } + StringBuilder resolved = new StringBuilder(); + for (Association association : propertyPath.getAssociations()) { + if (!resolved.isEmpty()) { + resolved.append('.'); + } + resolved.append(association.getPersistedName()); + } + if (!resolved.isEmpty()) { + resolved.append('.'); + } + resolved.append(propertyPath.getProperty().getPersistedName()); + return resolved.toString(); + }) + .orElseThrow(() -> new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]")); + if (!seenPaths.add(persistedPath)) { + throw new IllegalStateException("Duplicate Mongo index path [" + persistedPath + "] for entity [" + entity.getName() + "]"); + } + boolean geo = fieldAnnotation.booleanValue("geo").orElse(false); + MongoIndexDirection direction = fieldAnnotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC); + if (geo) { + if (direction != MongoIndexDirection.ASC) { + throw new IllegalStateException("Mongo compound geospatial field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot define a numeric direction"); + } + MongoGeoIndexType geoType = fieldAnnotation.enumValue("geoType", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); + Integer bits = fieldAnnotation.intValue("bits").isPresent() && fieldAnnotation.intValue("bits").getAsInt() >= 0 ? fieldAnnotation.intValue("bits").getAsInt() : null; + Double min = fieldAnnotation.doubleValue("min").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("min").getAsDouble()) ? fieldAnnotation.doubleValue("min").getAsDouble() : null; + Double max = fieldAnnotation.doubleValue("max").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("max").getAsDouble()) ? fieldAnnotation.doubleValue("max").getAsDouble() : null; + if (geoType != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { + throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d compound geospatial fields on entity [" + entity.getName() + "]"); + } + fields.add(new ResolvedIndexField(persistedPath, null, bits, geoType.getKey(), min, max)); + } else { + if ((fieldAnnotation.intValue("bits").isPresent() && fieldAnnotation.intValue("bits").getAsInt() >= 0) + || (fieldAnnotation.doubleValue("min").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("min").getAsDouble())) + || (fieldAnnotation.doubleValue("max").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("max").getAsDouble()))) { + throw new IllegalStateException("2d-specific geospatial options require geo=true for Mongo compound index field [" + persistedPath + "] on entity [" + entity.getName() + "]"); + } + fields.add(new ResolvedIndexField(persistedPath, direction == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)); + } + } + if (fields.isEmpty()) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] must declare at least one field"); + } + if (annotationValue.intValue("expireAfterSeconds").isPresent() && annotationValue.intValue("expireAfterSeconds").getAsInt() >= 0) { + throw new IllegalStateException("TTL is not supported for Mongo compound index on entity [" + entity.getName() + "]"); + } + String partialFilterExpression = annotationValue.stringValue("partialFilterExpression").filter(s -> !s.isEmpty()).orElse(null); + boolean sparse = annotationValue.booleanValue("sparse").orElse(false); + if (sparse && partialFilterExpression != null) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] cannot define both sparse and partialFilterExpression"); + } + indexes.add(new ResolvedIndex( + annotationValue.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + List.copyOf(fields), + annotationValue.booleanValue("unique").orElse(false), + sparse, + null, + partialFilterExpression, + annotationValue.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), + null, + null, + null, + null + )); + } + return indexes; + } + + /** + * Resolved Mongo index definition. + * + * @param name The index name + * @param fields The index fields + * @param unique Whether unique + * @param sparse Whether sparse + * @param expireAfterSeconds TTL in seconds if any + */ + public record ResolvedIndex(@Nullable String name, + List fields, + boolean unique, + boolean sparse, + @Nullable Integer expireAfterSeconds, + @Nullable String partialFilterExpression, + @Nullable String collation, + @Nullable Integer bits, + @Nullable Double min, + @Nullable Double max, + @Nullable String wildcardProjection) { + } + + /** + * Resolved Mongo index field. + * + * @param path The persisted path + * @param order The field order + */ + public record ResolvedIndexField(String path, @Nullable Integer order, @Nullable Integer weight, @Nullable String kind, @Nullable Double min, @Nullable Double max) { + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java index fc150d36969..1c1b89753e2 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java @@ -31,16 +31,22 @@ public final class MongoDataConfiguration { public static final String PREFIX = "micronaut.data.mongodb"; public static final String CREATE_COLLECTIONS_PROPERTY = PREFIX + ".create-collections"; + public static final String CREATE_INDEXES_PROPERTY = PREFIX + ".create-indexes"; public static final String DRIVER_TYPE_PROPERTY = PREFIX + ".driver-type"; public static final String JSON_VIEWS_PROPERTY = PREFIX + ".ignore-json-views"; public static final String DRIVER_TYPE_SYNC = DriverType.SYNC.name(); public static final String DRIVER_TYPE_REACTIVE = DriverType.REACTIVE.name(); - public static final String DATABASE_CONFIGURATION_ERROR_MESSAGE = "MongoDB database name is not specified in the url! You can specify it as '@MongoRepository(database: \"mydb\")' or in the connect url: 'mongodb://username:password@localhost:27017/mydb'."; + public static final String DATABASE_CONFIGURATION_ERROR_MESSAGE = "MongoDB database name is not specified in the url! You can specify it as '@MongoRepository(databaseName: \"mydb\")' or in the connect url: 'mongodb://username:password@localhost:27017/mydb'."; /** * Create MongoDB collection at app initialization. */ private boolean createCollections; + /** + * Create MongoDB indexes at app initialization. + */ + private boolean createIndexes; + /** * Choose the appropriate driver type when both are on classpath. */ @@ -59,6 +65,14 @@ public void setCreateCollections(boolean createCollections) { this.createCollections = createCollections; } + public boolean isCreateIndexes() { + return createIndexes; + } + + public void setCreateIndexes(boolean createIndexes) { + this.createIndexes = createIndexes; + } + public boolean isIgnoreJsonViews() { return ignoreJsonViews; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java new file mode 100644 index 00000000000..1e20060ddd3 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.serde.annotation.Serdeable; + +/** + * Minimal GeoJSON point-like value for MongoDB geospatial fields. + * + * @param x longitude / x coordinate + * @param y latitude / y coordinate + * @author radovanradic + * @since 5.0.0 + */ +@Serdeable +public record MongoGeoPoint(double x, double y) implements MongoGeoPointLike { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java new file mode 100644 index 00000000000..3b2c2a40944 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import org.jspecify.annotations.Nullable; +import io.micronaut.data.model.runtime.convert.AttributeConverter; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoPoint} to and from a GeoJSON-like persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +public final class MongoGeoPointConverter implements AttributeConverter> { + + @Override + public @Nullable Map convertToPersistedValue(@Nullable MongoGeoPointLike entityValue, @NonNull io.micronaut.core.convert.ConversionContext context) { + if (entityValue == null) { + return null; + } + Map point = new LinkedHashMap<>(); + point.put("type", "Point"); + point.put("coordinates", List.of(entityValue.x(), entityValue.y())); + return point; + } + + @Override + public @Nullable MongoGeoPointLike convertToEntityValue(@Nullable Map persistedValue, @NonNull io.micronaut.core.convert.ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object coordinates = persistedValue.get("coordinates"); + if (coordinates instanceof List list && list.size() == 2) { + Object x = list.get(0); + Object y = list.get(1); + if (x instanceof Number xNumber && y instanceof Number yNumber) { + return new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue()); + } + } + throw new IllegalArgumentException("Invalid persisted MongoGeoPoint value: " + persistedValue); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java new file mode 100644 index 00000000000..758e512d5cf --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +/** + * Contract for point-like geospatial values that can be serialized to GeoJSON points. + * + * @author radovanradic + * @since 5.0.0 + */ +public interface MongoGeoPointLike { + + /** + * @return The x / longitude coordinate. + */ + double x(); + + /** + * @return The y / latitude coordinate. + */ + double y(); +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index 85fa27986c5..c8afcaff8fa 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -19,11 +19,11 @@ import io.micronaut.configuration.mongo.core.DefaultMongoConfiguration; import io.micronaut.configuration.mongo.core.NamedMongoConfiguration; import io.micronaut.context.BeanLocator; +import io.micronaut.context.env.Environment; import io.micronaut.context.Qualifier; import io.micronaut.context.annotation.Context; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; import io.micronaut.data.model.Association; @@ -31,14 +31,19 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.naming.NamingStrategy; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; +import io.micronaut.data.model.runtime.RuntimePersistentEntity; +import io.micronaut.data.mongodb.annotation.MongoIndexDirection; +import io.micronaut.data.mongodb.common.MongoEntityIndexes; +import io.micronaut.data.mongodb.conf.MongoDataConfiguration; import io.micronaut.data.mongodb.operations.MongoCollectionNameProvider; import io.micronaut.inject.qualifiers.Qualifiers; +import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.reflect.Modifier; -import java.util.Collection; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -53,6 +58,7 @@ @Context @Internal public class AbstractMongoCollectionsCreator { + private static final Logger LOG = LoggerFactory.getLogger(AbstractMongoCollectionsCreator.class); /** @@ -79,23 +85,38 @@ protected M getMongoFactory(Class mongoFactoryClass, BeanLocator beanLoca * Initialize the collections. * * @param runtimeEntityRegistry The entity registry + * @param environment The environment * @param mongoConfigurations The configuration + * @param mongoDataConfiguration The Mongo data configuration * @param databaseOperationsProvider The database provider * @param mongoCollectionNameProvider The Mongo collection name provider */ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, + Environment environment, List mongoConfigurations, + MongoDataConfiguration mongoDataConfiguration, DatabaseOperationsProvider databaseOperationsProvider, MongoCollectionNameProvider mongoCollectionNameProvider) { + boolean createCollections = mongoDataConfiguration.isCreateCollections(); + boolean createIndexes = mongoDataConfiguration.isCreateIndexes(); + if (!createCollections && !createIndexes) { + return; + } + for (AbstractMongoConfiguration mongoConfiguration : mongoConfigurations) { - // TODO: different initializer per conf - Collection> introspections = BeanIntrospector.SHARED.findIntrospections(MappedEntity.class); - PersistentEntity[] entities = introspections.stream() - // filter out inner / internal / abstract(MappedSuperClass) classes - .filter(i -> !i.getBeanType().getName().contains("$")) - .filter(i -> !Modifier.isAbstract(i.getBeanType().getModifiers())) - .map(e -> runtimeEntityRegistry.getEntity(e.getBeanType())).toArray(PersistentEntity[]::new); + List packageNames = environment.getProperty("mongodb.package-names", List.class).orElseGet(List::of); + List> entityTypes; + if (!packageNames.isEmpty()) { + entityTypes = environment.scan(MappedEntity.class, packageNames.toArray(new String[0])).toList(); + } else { + entityTypes = environment.scan(MappedEntity.class).toList(); + } + PersistentEntity[] entities = entityTypes.stream() + .filter(type -> !type.getName().contains("$")) + .filter(type -> !type.isSynthetic()) + .map(runtimeEntityRegistry::getEntity) + .toArray(PersistentEntity[]::new); DatabaseOperations databaseOperations = databaseOperationsProvider.get(mongoConfiguration); @@ -103,32 +124,95 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, Dtbs database = databaseOperations.find(entity); Set collections = databaseOperations.listCollectionNames(database); String persistedName = mongoCollectionNameProvider.provide(entity); - if (collections.add(persistedName)) { + boolean collectionExists = collections.contains(persistedName); + if (!collectionExists && createCollections) { if (LOG.isInfoEnabled()) { LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); } databaseOperations.createCollection(database, persistedName); + collections.add(persistedName); + } + if ((collectionExists || createCollections) && createIndexes) { + createIndexes(databaseOperations, database, entity, persistedName); + } + if (createCollections) { + createJoinCollections(databaseOperations, database, collections, entity, persistedName); } - for (PersistentProperty persistentProperty : entity.getPersistentProperties()) { - if (persistentProperty instanceof Association association) { - Optional inverseSide = association.getInverseSide().map(Function.identity()); - if (association.getKind() == Relation.Kind.MANY_TO_MANY || association.isForeignKey() && !inverseSide.isPresent()) { - Association owningAssociation = inverseSide.orElse(association); - NamingStrategy namingStrategy = association.getOwner().getNamingStrategy(); - String joinCollectionName = namingStrategy.mappedName(owningAssociation); - if (collections.add(joinCollectionName)) { - if (LOG.isInfoEnabled()) { - LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); - } - databaseOperations.createCollection(database, joinCollectionName); - } + } + } + } + + private void createJoinCollections(DatabaseOperations databaseOperations, + Dtbs database, + Set collections, + PersistentEntity entity, + String persistedName) { + for (PersistentProperty persistentProperty : entity.getPersistentProperties()) { + if (persistentProperty instanceof Association association) { + Optional inverseSide = association.getInverseSide().map(Function.identity()); + if (association.getKind() == Relation.Kind.MANY_TO_MANY || (association.isForeignKey() && inverseSide.isEmpty())) { + Association owningAssociation = inverseSide.orElse(association); + NamingStrategy namingStrategy = association.getOwner().getNamingStrategy(); + String joinCollectionName = namingStrategy.mappedName(owningAssociation); + if (collections.add(joinCollectionName)) { + if (LOG.isInfoEnabled()) { + LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); } + databaseOperations.createCollection(database, joinCollectionName); } } + } + } + } + + private void createIndexes(DatabaseOperations databaseOperations, + Dtbs database, + PersistentEntity entity, + String collectionName) { + List desiredIndexes = resolveIndexes(entity); + if (desiredIndexes.isEmpty()) { + return; + } + List existingIndexes = databaseOperations.listIndexes(database, collectionName); + for (MongoResolvedIndex desiredIndex : desiredIndexes) { + MongoResolvedIndex existingIndex = findMatchingIndex(existingIndexes, desiredIndex); + if (existingIndex == null) { + databaseOperations.createIndex(database, collectionName, desiredIndex); + continue; + } + if (!existingIndex.matchesManagedOptions(desiredIndex)) { + throw new IllegalStateException("Conflicting existing MongoDB index for entity [" + entity.getName() + "] and collection [" + collectionName + "]: desired " + desiredIndex.describe() + ", existing " + existingIndex.describe()); + } + if (desiredIndex.name() != null && existingIndex.name() != null && !desiredIndex.name().equals(existingIndex.name())) { + throw new IllegalStateException("Conflicting existing MongoDB index name for entity [" + entity.getName() + "] and collection [" + collectionName + "]: desired " + desiredIndex.describe() + ", existing " + existingIndex.describe()); + } + } + } + @Nullable + private MongoResolvedIndex findMatchingIndex(List existingIndexes, MongoResolvedIndex desiredIndex) { + for (MongoResolvedIndex existingIndex : existingIndexes) { + if (existingIndex.hasSameKey(desiredIndex)) { + return existingIndex; } } + return null; + } + + private List resolveIndexes(PersistentEntity entity) { + RuntimePersistentEntity runtimePersistentEntity = (RuntimePersistentEntity) entity; + List indexes = new ArrayList<>(); + for (MongoEntityIndexes.ResolvedIndex index : MongoEntityIndexes.create(runtimePersistentEntity).getIndexes()) { + List fields = index.fields().stream() + .map(field -> new MongoResolvedIndexField(field.path(), field.order(), field.weight(), field.kind(), field.min(), field.max())) + .toList(); + indexes.add(new MongoResolvedIndex(index.name(), fields, index.unique(), index.sparse(), index.expireAfterSeconds(), index.partialFilterExpression(), index.collation(), index.bits(), index.min(), index.max(), index.wildcardProjection())); + } + return indexes; + } + private int toOrder(MongoIndexDirection direction) { + return direction == MongoIndexDirection.DESC ? -1 : 1; } /** @@ -187,6 +271,75 @@ interface DatabaseOperations { */ void createCollection(Dtbs database, String collection); + /** + * List indexes for the given collection. + * + * @param database The database + * @param collection The collection name + * @return The indexes + */ + List listIndexes(Dtbs database, String collection); + + /** + * Create an index for the given collection. + * + * @param database The database + * @param collection The collection name + * @param index The index + */ + void createIndex(Dtbs database, String collection, MongoResolvedIndex index); + + } + + @Internal + record MongoResolvedIndexField(String path, @Nullable Integer order, @Nullable Integer weight, @Nullable String kind, @Nullable Double min, @Nullable Double max) { } + @Internal + record MongoResolvedIndex(@Nullable String name, + List fields, + boolean unique, + boolean sparse, + @Nullable Integer expireAfterSeconds, + @Nullable String partialFilterExpression, + @Nullable String collation, + @Nullable Integer bits, + @Nullable Double min, + @Nullable Double max, + @Nullable String wildcardProjection) { + + boolean hasSameKey(MongoResolvedIndex other) { + return fields.equals(other.fields); + } + + boolean matchesManagedOptions(MongoResolvedIndex other) { + return unique == other.unique + && sparse == other.sparse + && Objects.equals(expireAfterSeconds, other.expireAfterSeconds) + && Objects.equals(partialFilterExpression, other.partialFilterExpression) + && Objects.equals(collation, other.collation) + && Objects.equals(bits, other.bits) + && Objects.equals(min, other.min) + && Objects.equals(max, other.max) + && Objects.equals(wildcardProjection, other.wildcardProjection); + } + + Document keysDocument() { + Document document = new Document(); + for (MongoResolvedIndexField field : fields) { + if (field.order() != null) { + document.append(field.path(), field.order()); + } else if (field.kind() != null) { + document.append(field.path(), field.kind()); + } else { + document.append(field.path(), "text"); + } + } + return document; + } + + String describe() { + return "MongoResolvedIndex{name=" + name + ", fields=" + fields + ", unique=" + unique + ", sparse=" + sparse + ", expireAfterSeconds=" + expireAfterSeconds + ", partialFilterExpression=" + partialFilterExpression + '}'; + } + } } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 57398923b37..9ed48610c84 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -16,10 +16,15 @@ package io.micronaut.data.mongodb.init; import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Collation; +import com.mongodb.client.model.CollationStrength; +import com.mongodb.client.model.IndexOptions; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.context.BeanLocator; import io.micronaut.context.annotation.Context; +import io.micronaut.context.env.Environment; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.CollectionUtils; @@ -31,7 +36,9 @@ import io.micronaut.data.mongodb.operations.MongoCollectionNameProvider; import io.micronaut.data.mongodb.operations.MongoDatabaseNameProvider; import jakarta.annotation.PostConstruct; +import org.bson.Document; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -47,16 +54,18 @@ @Context @Internal @RequiresSyncMongo -@Requires(property = MongoDataConfiguration.CREATE_COLLECTIONS_PROPERTY, value = StringUtils.TRUE) +@Requires(property = MongoDataConfiguration.PREFIX) public final class MongoCollectionsCreator extends AbstractMongoCollectionsCreator { @PostConstruct void initialize(BeanLocator beanLocator, RuntimeEntityRegistry runtimeEntityRegistry, + Environment environment, List mongoConfigurations, + MongoDataConfiguration mongoDataConfiguration, MongoCollectionNameProvider mongoCollectionNameProvider) { - super.initialize(runtimeEntityRegistry, mongoConfigurations, mongoConfiguration -> { + super.initialize(runtimeEntityRegistry, environment, mongoConfigurations, mongoDataConfiguration, mongoConfiguration -> { MongoClient mongoClient = getMongoFactory(MongoClient.class, beanLocator, mongoConfiguration); MongoDatabaseNameProvider mongoDatabaseNameProvider = getMongoFactory(MongoDatabaseNameProvider.class, beanLocator, mongoConfiguration); Map> databaseCollections = new HashMap<>(); @@ -81,8 +90,89 @@ public Set listCollectionNames(MongoDatabase database) { public void createCollection(MongoDatabase database, String collection) { database.createCollection(collection); } + + @Override + public List listIndexes(MongoDatabase database, String collection) { + MongoCollection mongoCollection = database.getCollection(collection); + List indexes = new ArrayList<>(); + for (Document indexDocument : mongoCollection.listIndexes()) { + Document keyDocument = indexDocument.get("key", Document.class); + if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { + continue; + } + List fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } + } + indexes.add(new MongoResolvedIndex( + indexDocument.getString("name"), + List.copyOf(fields), + indexDocument.getBoolean("unique", false), + indexDocument.getBoolean("sparse", false), + indexDocument.getInteger("expireAfterSeconds"), + indexDocument.get("partialFilterExpression") == null ? null : indexDocument.get("partialFilterExpression").toString(), + indexDocument.get("collation") == null ? null : indexDocument.get("collation").toString(), + null, + null, + null, + indexDocument.get("wildcardProjection") == null ? null : indexDocument.get("wildcardProjection").toString() + )); + } + return indexes; + } + + @Override + public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { + MongoCollection mongoCollection = database.getCollection(collection); + IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); + if (index.name() != null) { + indexOptions.name(index.name()); + } + if (index.expireAfterSeconds() != null) { + indexOptions.expireAfter((long) index.expireAfterSeconds(), java.util.concurrent.TimeUnit.SECONDS); + } + if (index.partialFilterExpression() != null) { + indexOptions.partialFilterExpression(Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexOptions.collation(toCollation(Document.parse(index.collation()))); + } + if (index.bits() != null) { + indexOptions.bits(index.bits()); + } + if (index.min() != null) { + indexOptions.min(index.min()); + } + if (index.max() != null) { + indexOptions.max(index.max()); + } + mongoCollection.createIndex(index.keysDocument(), indexOptions); + } }; }, mongoCollectionNameProvider); } + + private Collation toCollation(Document document) { + Collation.Builder builder = Collation.builder(); + String locale = document.getString("locale"); + if (locale != null) { + builder.locale(locale); + } + Integer strength = document.getInteger("strength"); + if (strength != null) { + builder.collationStrength(CollationStrength.fromInt(strength)); + } + Boolean caseLevel = document.getBoolean("caseLevel"); + if (caseLevel != null) { + builder.caseLevel(caseLevel); + } + return builder.build(); + } + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index f9f6444a841..f3501da4ab4 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -15,14 +15,18 @@ */ package io.micronaut.data.mongodb.init; +import com.mongodb.client.model.Collation; +import com.mongodb.client.model.CollationStrength; +import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.context.BeanLocator; import io.micronaut.context.annotation.Context; +import io.micronaut.context.env.Environment; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.util.StringUtils; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; @@ -33,6 +37,9 @@ import reactor.core.publisher.Mono; import jakarta.annotation.PostConstruct; +import org.bson.Document; + +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,16 +55,18 @@ @Context @Internal @RequiresReactiveMongo -@Requires(property = MongoDataConfiguration.CREATE_COLLECTIONS_PROPERTY, value = StringUtils.TRUE) +@Requires(property = MongoDataConfiguration.PREFIX) public final class MongoReactiveCollectionsCreator extends AbstractMongoCollectionsCreator { @PostConstruct void initialize(BeanLocator beanLocator, RuntimeEntityRegistry runtimeEntityRegistry, + Environment environment, List mongoConfigurations, + MongoDataConfiguration mongoDataConfiguration, MongoCollectionNameProvider mongoCollectionNameProvider) { - super.initialize(runtimeEntityRegistry, mongoConfigurations, mongoConfiguration -> { + super.initialize(runtimeEntityRegistry, environment, mongoConfigurations, mongoDataConfiguration, mongoConfiguration -> { MongoClient mongoClient = getMongoFactory(MongoClient.class, beanLocator, mongoConfiguration); MongoDatabaseNameProvider mongoDatabaseNameProvider = getMongoFactory(MongoDatabaseNameProvider.class, beanLocator, mongoConfiguration); Map> databaseCollections = new HashMap<>(); @@ -82,8 +91,95 @@ public Set listCollectionNames(MongoDatabase database) { public void createCollection(MongoDatabase database, String collection) { Mono.from(database.createCollection(collection)).block(); } + + @Override + public List listIndexes(MongoDatabase database, String collection) { + MongoCollection mongoCollection = database.getCollection(collection); + List indexes = Flux.from(mongoCollection.listIndexes()) + .collectList() + .map(indexDocuments -> { + List resolvedIndexes = new ArrayList<>(); + for (Document indexDocument : indexDocuments) { + Document keyDocument = indexDocument.get("key", Document.class); + if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { + continue; + } + List fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } + } + resolvedIndexes.add(new MongoResolvedIndex( + indexDocument.getString("name"), + List.copyOf(fields), + indexDocument.getBoolean("unique", false), + indexDocument.getBoolean("sparse", false), + indexDocument.getInteger("expireAfterSeconds"), + indexDocument.get("partialFilterExpression") == null ? null : indexDocument.get("partialFilterExpression").toString(), + indexDocument.get("collation") == null ? null : indexDocument.get("collation").toString(), + null, + null, + null, + indexDocument.get("wildcardProjection") == null ? null : indexDocument.get("wildcardProjection").toString() + )); + } + return resolvedIndexes; + }) + .block(); + return indexes == null ? List.of() : indexes; + } + + @Override + public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { + MongoCollection mongoCollection = database.getCollection(collection); + IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); + if (index.name() != null) { + indexOptions.name(index.name()); + } + if (index.expireAfterSeconds() != null) { + indexOptions.expireAfter((long) index.expireAfterSeconds(), java.util.concurrent.TimeUnit.SECONDS); + } + if (index.partialFilterExpression() != null) { + indexOptions.partialFilterExpression(Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexOptions.collation(toCollation(Document.parse(index.collation()))); + } + if (index.bits() != null) { + indexOptions.bits(index.bits()); + } + if (index.min() != null) { + indexOptions.min(index.min()); + } + if (index.max() != null) { + indexOptions.max(index.max()); + } + Mono.from(mongoCollection.createIndex(index.keysDocument(), indexOptions)).block(); + } }; }, mongoCollectionNameProvider); } + + private Collation toCollation(Document document) { + Collation.Builder builder = Collation.builder(); + String locale = document.getString("locale"); + if (locale != null) { + builder.locale(locale); + } + Integer strength = document.getInteger("strength"); + if (strength != null) { + builder.collationStrength(CollationStrength.fromInt(strength)); + } + Boolean caseLevel = document.getBoolean("caseLevel"); + if (caseLevel != null) { + builder.caseLevel(caseLevel); + } + return builder.build(); + } + } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy new file mode 100644 index 00000000000..9f65bd2533b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy @@ -0,0 +1,44 @@ +package io.micronaut.data.document.mongodb + +import com.mongodb.client.MongoClient +import com.mongodb.client.MongoCollection +import io.micronaut.data.mongodb.init.AbstractMongoCollectionsCreator +import org.bson.Document + +final class MongoIndexInspector { + + private MongoIndexInspector() { + } + + static List> listNormalizedIndexes(MongoClient mongoClient, String databaseName, String collectionName) { + MongoCollection mongoCollection = mongoClient.getDatabase(databaseName).getCollection(collectionName) + List> indexes = [] + for (Document indexDocument : mongoCollection.listIndexes()) { + Document keyDocument = indexDocument.get('key', Document) + if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger('_id', 0) == 1)) { + continue + } + List fields = new ArrayList<>(keyDocument.size()) + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue() + if (value instanceof Number) { + fields.add(new AbstractMongoCollectionsCreator.MongoResolvedIndexField(entry.getKey(), ((Number) value).intValue(), null, null, null, null)) + } else { + fields.add(new AbstractMongoCollectionsCreator.MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)) + } + } + indexes << [ + name : indexDocument.getString('name'), + fields : List.copyOf(fields), + unique : indexDocument.getBoolean('unique', false), + sparse : indexDocument.getBoolean('sparse', false), + expireAfterSeconds: indexDocument.getInteger('expireAfterSeconds'), + partialFilterExpression: indexDocument.get('partialFilterExpression'), + collation : indexDocument.get('collation'), + min : indexDocument.get('min'), + max : indexDocument.get('max'), + ] + } + indexes + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy new file mode 100644 index 00000000000..0cf7949225c --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.collation + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCollationIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.collation'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field index with collation'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'collation_indexed_entities') + assert indexes*.name.contains('collation_name_idx') + def index = indexes.find { it.name == 'collation_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.collation != null + assert ((Document) index.collation).getString('locale') == 'en' + } + } +} + +@MongoRepository +interface CollationIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('collation_indexed_entities') +class CollationIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'collation_name_idx', collation = '{ "locale": "en", "strength": 2 }') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy new file mode 100644 index 00000000000..575b1de779e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy @@ -0,0 +1,84 @@ +package io.micronaut.data.document.mongodb.compound + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.compound'] + } + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + def setupSpec() { + // mongoClient.getDatabase('test').getCollection('compound_indexed_entities').drop() + // mongoClient.getDatabase('test').createCollection('compound_indexed_entities') + } + + void 'creates declared compound indexes for existing collections'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_indexed_entities') + assert indexes*.name.contains('name_age_idx') + def index = indexes.find { it.name == 'name_age_idx' } + assert index.unique + assert index.fields.size() == 2 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.fields[1].path() == 'age' + assert index.fields[1].order() == -1 + } + } +} + +@MongoRepository +interface CompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'name_age_idx', + unique = true, + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'age', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('compound_indexed_entities') +class CompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + Integer age +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy new file mode 100644 index 00000000000..2d12ddcc0ac --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.geo + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geo'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field 2dsphere index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_indexed_entities') + assert indexes*.name.contains('geo_location_idx') + def index = indexes.find { it.name == 'geo_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].order() == null + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface GeoIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_indexed_entities') +class GeoIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'geo_location_idx') + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy new file mode 100644 index 00000000000..f8e6863c8dd --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy @@ -0,0 +1,70 @@ +package io.micronaut.data.document.mongodb.geo2d + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geo2d'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field 2d index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_indexed_entities') + assert indexes*.name.contains('geo2d_location_idx') + def index = indexes.find { it.name == 'geo2d_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].order() == null + assert index.fields[0].kind() == '2d' + } + } +} + +@MongoRepository +interface Geo2dIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo2d_indexed_entities') +class Geo2dIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'geo2d_location_idx', type = MongoGeoIndexType.GEO_2D) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy new file mode 100644 index 00000000000..6247e3c4a04 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.geo2d.options + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeo2dOptionsIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geo2d.options'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates 2d index with bits min and max'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_options_indexed_entities') + assert indexes*.name.contains('geo2d_options_idx') + def index = indexes.find { it.name == 'geo2d_options_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2d' + } + } +} + +@MongoRepository +interface Geo2dOptionsIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo2d_options_indexed_entities') +class Geo2dOptionsIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'geo2d_options_idx', type = MongoGeoIndexType.GEO_2D, bits = 26, min = -180, max = 180) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy new file mode 100644 index 00000000000..0b609244dd9 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy @@ -0,0 +1,81 @@ +package io.micronaut.data.document.mongodb.geocompound + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geocompound'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates compound index with geospatial field'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_compound_indexed_entities') + assert indexes*.name.contains('geo_name_idx') + def index = indexes.find { it.name == 'geo_name_idx' } + assert index.fields.size() == 2 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + assert index.fields[1].path() == 'name' + assert index.fields[1].order() == 1 + } + } +} + +@MongoRepository +interface GeoCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'geo_name_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('geo_compound_indexed_entities') +class GeoCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy new file mode 100644 index 00000000000..820a6743e25 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -0,0 +1,83 @@ +package io.micronaut.data.document.mongodb.geocompound.options + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geocompound.options'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates compound 2d index with bits min and max on geospatial field'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_compound_options_indexed_entities') + assert indexes*.name.contains('geo2d_name_idx') + def index = indexes.find { it.name == 'geo2d_name_idx' } + assert index.fields.size() == 2 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2d' + assert index.fields[1].path() == 'name' + assert index.fields[1].order() == 1 + assert index.min == -180 + assert index.max == 180 + } + } +} + +@MongoRepository +interface Geo2dCompoundOptionsEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'geo2d_name_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D, bits = 26, min = -180, max = 180), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('geo2d_compound_options_indexed_entities') +class Geo2dCompoundOptionsEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..009372c5e9f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,75 @@ +package io.micronaut.data.document.mongodb.geovalue + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.MappedProperty +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.mongodb.geo.MongoGeoPointConverter +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates geospatial index on a MongoGeoPoint modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_point_value_indexed_entities') + assert indexes*.name.contains('geo_point_location_idx') + def index = indexes.find { it.name == 'geo_point_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface GeoPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_point_value_indexed_entities') +class GeoPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) + @MongoGeoIndexed(name = 'geo_point_location_idx') + MongoGeoPoint location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..f0107d9850b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,91 @@ +package io.micronaut.data.document.mongodb.geovalue.custom + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.MappedProperty +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPointConverter +import io.micronaut.data.mongodb.geo.MongoGeoPointLike +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCustomGeoPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.custom'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates geospatial index on a custom point-like modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'custom_geo_point_value_indexed_entities') + assert indexes*.name.contains('custom_geo_point_location_idx') + def index = indexes.find { it.name == 'custom_geo_point_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface CustomGeoPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('custom_geo_point_value_indexed_entities') +class CustomGeoPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) + @MongoGeoIndexed(name = 'custom_geo_point_location_idx') + CustomGeoPoint location +} + +@MappedEntity +class CustomGeoPoint implements MongoGeoPointLike { + double x + double y + + @Override + double x() { + return x + } + + @Override + double y() { + return y + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy new file mode 100644 index 00000000000..6b730b82b76 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.hashed + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoHashedIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoHashedIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.hashed'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field hashed index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'hashed_indexed_entities') + assert indexes*.name.contains('hashed_name_idx') + def index = indexes.find { it.name == 'hashed_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == null + assert index.fields[0].kind() == 'hashed' + } + } +} + +@MongoRepository +interface HashedIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('hashed_indexed_entities') +class HashedIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoHashedIndexed(name = 'hashed_name_idx') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy new file mode 100644 index 00000000000..05fb014ed8b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy @@ -0,0 +1,72 @@ +package io.micronaut.data.document.mongodb.partialfilter + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoPartialFilterIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.partialfilter'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field index with partial filter expression'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'partial_filter_indexed_entities') + assert indexes*.name.contains('partial_name_idx') + def index = indexes.find { it.name == 'partial_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.partialFilterExpression != null + assert index.partialFilterExpression.getBoolean('active') + } + } +} + +@MongoRepository +interface PartialFilterIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('partial_filter_indexed_entities') +class PartialFilterIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'partial_name_idx', partialFilterExpression = '{ "active": true }') + String name + + Boolean active +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy new file mode 100644 index 00000000000..b69f6fa1834 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy @@ -0,0 +1,81 @@ +package io.micronaut.data.document.mongodb.reactive + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCompoundIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates declared compound indexes for existing collections with reactive driver selected'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_indexed_entities') + assert indexes*.name.contains('reactive_name_age_idx') + def index = indexes.find { it.name == 'reactive_name_age_idx' } + assert index.unique + assert index.fields.size() == 2 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.fields[1].path() == 'age' + assert index.fields[1].order() == -1 + } + } +} + +@MongoRepository +interface ReactiveCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'reactive_name_age_idx', + unique = true, + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'age', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('reactive_compound_indexed_entities') +class ReactiveCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + Integer age +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy new file mode 100644 index 00000000000..34677b53a80 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy @@ -0,0 +1,67 @@ +package io.micronaut.data.document.mongodb.reactive + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive'] + } + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates declared indexes for existing collections with reactive driver selected'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_indexed_entities') + assert indexes*.name.contains('reactive_name_idx') + def index = indexes.find { it.name == 'reactive_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface ReactiveIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_indexed_entities') +class ReactiveIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'reactive_name_idx') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy new file mode 100644 index 00000000000..1220f5c8bda --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.reactive.collation + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCollationIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.collation'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field index with collation in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_collation_indexed_entities') + assert indexes*.name.contains('reactive_collation_name_idx') + def index = indexes.find { it.name == 'reactive_collation_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.collation != null + assert ((Document) index.collation).getString('locale') == 'en' + } + } +} + +@MongoRepository +interface ReactiveCollationIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_collation_indexed_entities') +class ReactiveCollationIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'reactive_collation_name_idx', collation = '{ "locale": "en", "strength": 2 }') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy new file mode 100644 index 00000000000..97b90246fd9 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.reactive.geo + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveGeoIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geo'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field 2dsphere index in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_indexed_entities') + assert indexes*.name.contains('reactive_geo_location_idx') + def index = indexes.find { it.name == 'reactive_geo_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].order() == null + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface ReactiveGeoIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_geo_indexed_entities') +class ReactiveGeoIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'reactive_geo_location_idx') + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy new file mode 100644 index 00000000000..9bbed21642f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy @@ -0,0 +1,70 @@ +package io.micronaut.data.document.mongodb.reactive.geo2d + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveGeo2dIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geo2d'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field 2d index in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo2d_indexed_entities') + assert indexes*.name.contains('reactive_geo2d_location_idx') + def index = indexes.find { it.name == 'reactive_geo2d_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].order() == null + assert index.fields[0].kind() == '2d' + } + } +} + +@MongoRepository +interface ReactiveGeo2dIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_geo2d_indexed_entities') +class ReactiveGeo2dIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'reactive_geo2d_location_idx', type = MongoGeoIndexType.GEO_2D) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy new file mode 100644 index 00000000000..62cdce1547d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.reactive.geo2d.options + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveGeo2dOptionsIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geo2d.options'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates 2d index with bits min and max in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo2d_options_indexed_entities') + assert indexes*.name.contains('reactive_geo2d_options_idx') + def index = indexes.find { it.name == 'reactive_geo2d_options_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2d' + assert index.min == -180 + assert index.max == 180 + } + } +} + +@MongoRepository +interface ReactiveGeo2dOptionsIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_geo2d_options_indexed_entities') +class ReactiveGeo2dOptionsIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'reactive_geo2d_options_idx', type = MongoGeoIndexType.GEO_2D, bits = 26, min = -180, max = 180) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy new file mode 100644 index 00000000000..4949f607373 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy @@ -0,0 +1,81 @@ +package io.micronaut.data.document.mongodb.reactive.geocompound + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCompoundGeoIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geocompound'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates compound index with geospatial field in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_compound_indexed_entities') + assert indexes*.name.contains('reactive_geo_name_idx') + def index = indexes.find { it.name == 'reactive_geo_name_idx' } + assert index.fields.size() == 2 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + assert index.fields[1].path() == 'name' + assert index.fields[1].order() == 1 + } + } +} + +@MongoRepository +interface ReactiveGeoCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'reactive_geo_name_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('reactive_geo_compound_indexed_entities') +class ReactiveGeoCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..130f1f135b6 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,75 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.MappedProperty +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.mongodb.geo.MongoGeoPointConverter +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveGeoPointValueIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geovalue'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates geospatial index on a MongoGeoPoint modeled value in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_point_value_indexed_entities') + assert indexes*.name.contains('reactive_geo_point_location_idx') + def index = indexes.find { it.name == 'reactive_geo_point_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface ReactiveGeoPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_geo_point_value_indexed_entities') +class ReactiveGeoPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) + @MongoGeoIndexed(name = 'reactive_geo_point_location_idx') + MongoGeoPoint location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..b949a506f6b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,91 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.custom + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.MappedProperty +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPointConverter +import io.micronaut.data.mongodb.geo.MongoGeoPointLike +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCustomGeoPointValueIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.geovalue.custom'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates geospatial index on a custom point-like modeled value in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_custom_geo_point_value_indexed_entities') + assert indexes*.name.contains('reactive_custom_geo_point_location_idx') + def index = indexes.find { it.name == 'reactive_custom_geo_point_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + } + } +} + +@MongoRepository +interface ReactiveCustomGeoPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_custom_geo_point_value_indexed_entities') +class ReactiveCustomGeoPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) + @MongoGeoIndexed(name = 'reactive_custom_geo_point_location_idx') + ReactiveCustomGeoPoint location +} + +@MappedEntity +class ReactiveCustomGeoPoint implements MongoGeoPointLike { + double x + double y + + @Override + double x() { + return x + } + + @Override + double y() { + return y + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy new file mode 100644 index 00000000000..8bdad5aba1c --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.reactive.hashed + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoHashedIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveHashedIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.hashed'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field hashed index in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_hashed_indexed_entities') + assert indexes*.name.contains('reactive_hashed_name_idx') + def index = indexes.find { it.name == 'reactive_hashed_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == null + assert index.fields[0].kind() == 'hashed' + } + } +} + +@MongoRepository +interface ReactiveHashedIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_hashed_indexed_entities') +class ReactiveHashedIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoHashedIndexed(name = 'reactive_hashed_name_idx') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy new file mode 100644 index 00000000000..ef6007dcf10 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.reactive.partialfilter + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactivePartialFilterIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.partialfilter'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field index with partial filter expression in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_partial_filter_indexed_entities') + assert indexes*.name.contains('reactive_partial_name_idx') + def index = indexes.find { it.name == 'reactive_partial_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.partialFilterExpression != null + assert ((Document) index.partialFilterExpression).getBoolean('active') + } + } +} + +@MongoRepository +interface ReactivePartialFilterIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_partial_filter_indexed_entities') +class ReactivePartialFilterIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'reactive_partial_name_idx', partialFilterExpression = '{ "active": true }') + String name + + Boolean active +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..9425bbe55cc --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy @@ -0,0 +1,68 @@ +package io.micronaut.data.document.mongodb.reactive.text + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveTextIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.text'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field text index in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_text_indexed_entities') + assert indexes*.name.contains('reactive_text_name_idx') + def index = indexes.find { it.name == 'reactive_text_name_idx' } + assert index.fields.size() == 2 + assert index.fields*.path().contains('_fts') + assert index.fields*.path().contains('_ftsx') + } + } +} + +@MongoRepository +interface ReactiveTextIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_text_indexed_entities') +class ReactiveTextIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'reactive_text_name_idx') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy new file mode 100644 index 00000000000..95e92a74927 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy @@ -0,0 +1,68 @@ +package io.micronaut.data.document.mongodb.reactive.wildcard + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveWildcardIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.wildcard'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field wildcard index in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_wildcard_indexed_entities') + assert indexes*.name.contains('reactive_wildcard_metadata_idx') + def index = indexes.find { it.name == 'reactive_wildcard_metadata_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'metadata.$**' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface ReactiveWildcardIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('reactive_wildcard_indexed_entities') +class ReactiveWildcardIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoWildcardIndexed(name = 'reactive_wildcard_metadata_idx') + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy new file mode 100644 index 00000000000..18d29502fd0 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.simple + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.simple'] + } + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + // mongoClient.getDatabase('test').getCollection('indexed_entities').drop() + // mongoClient.getDatabase('test').createCollection('indexed_entities') + } + + void 'creates declared indexes for existing collections'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'indexed_entities') + assert indexes*.name.contains('name_idx') + def index = indexes.find { it.name == 'name_idx' } + assert index.unique + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface IndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('indexed_entities') +class IndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'name_idx', unique = true) + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..39221a2ce72 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.text + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoAggregatedTextIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.text'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates a single aggregated text index from multiple text-indexed fields'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'aggregated_text_indexed_entities') + assert indexes*.name.contains('aggregated_text_idx') + def index = indexes.find { it.name == 'aggregated_text_idx' } + assert index.fields.size() == 2 + assert index.fields*.path().contains('_fts') + assert index.fields*.path().contains('_ftsx') + } + } +} + +@MongoRepository +interface AggregatedTextIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('aggregated_text_indexed_entities') +class AggregatedTextIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'aggregated_text_idx', weight = 2) + String title + + @MongoTextIndexed(name = 'aggregated_text_idx', weight = 5) + String description +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..64025a83546 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy @@ -0,0 +1,68 @@ +package io.micronaut.data.document.mongodb.text + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoTextIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.text'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field text index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'text_indexed_entities') + assert indexes*.name.contains('text_name_idx') + def index = indexes.find { it.name == 'text_name_idx' } + assert index.fields.size() == 2 + assert index.fields*.path().contains('_fts') + assert index.fields*.path().contains('_ftsx') + } + } +} + +@MongoRepository +interface TextIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('text_indexed_entities') +class TextIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'text_name_idx') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy new file mode 100644 index 00000000000..cbc7788beb4 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.data.document.mongodb.validation.collation + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCollationValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.collation'] + } + + void 'fails fast for invalid collation JSON'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('JSON reader') + } +} + +@MongoRepository +interface InvalidCollationIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_collation_indexed_entities') +class InvalidCollationIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'invalid_collation_idx', collation = '{ invalid json }') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy new file mode 100644 index 00000000000..bb72f1e5245 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.data.document.mongodb.validation.duplicatefield + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundIndexDuplicateFieldValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.duplicatefield'] + } + + + void 'fails fast for duplicate compound index field entries'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('Duplicate Mongo index path [name]') + } +} + +@MongoRepository +interface DuplicateCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'duplicate_idx', + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('duplicate_compound_indexed_entities') +class DuplicateCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy new file mode 100644 index 00000000000..9da32c4950a --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.data.document.mongodb.validation.emptyfields + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundIndexEmptyFieldsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.emptyfields'] + } + + void 'fails fast for empty compound index field list'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('must declare at least one field') + } +} + +@MongoRepository +interface EmptyFieldsCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex(name = 'empty_idx', fields = []) +@MappedEntity('empty_fields_compound_indexed_entities') +class EmptyFieldsCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy new file mode 100644 index 00000000000..ce096d108ff --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.data.document.mongodb.validation.geocompound + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundGeoValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geocompound'] + } + + void 'fails fast when compound geospatial field also declares numeric direction'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('cannot define a numeric direction') + } +} + +@MongoRepository +interface InvalidCompoundGeoEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_geo_name_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE, direction = MongoIndexDirection.DESC), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_geo_compound_indexed_entities') +class InvalidCompoundGeoEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy new file mode 100644 index 00000000000..3b314134e04 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.data.document.mongodb.validation.geocompoundopts + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundGeo2dOptionsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geocompoundopts'] + } + + void 'fails fast when 2d-specific options are used without geo=true on a compound field'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('require geo=true') + } +} + +@MongoRepository +interface InvalidGeo2dOptionsEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_geo_options_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', bits = 26), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_geo_options_indexed_entities') +class InvalidGeo2dOptionsEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy new file mode 100644 index 00000000000..f9a6853c902 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.data.document.mongodb.validation.geocompoundopts + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundGeoOptionsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geocompoundopts'] + } + + void 'fails fast when 2d options are used on non-geospatial compound field'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('require geo=true') + } +} + +@MongoRepository +interface InvalidGeoOptionsEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_geo_options_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', bits = 26), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_geo_options_indexed_entities') +class InvalidGeoOptionsEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy new file mode 100644 index 00000000000..5f490df5ece --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.data.document.mongodb.validation.geocompoundopts2d + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundGeo2dOptionsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geocompoundopts2d'] + } + + void 'fails fast when 2d-specific options are used without geo=true on a compound field'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('require geo=true') + } +} + +@MongoRepository +interface InvalidCompoundGeo2dOptionsEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_geo2d_opts_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', bits = 26), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_geo2d_opts_entities') +class InvalidCompoundGeo2dOptionsEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy new file mode 100644 index 00000000000..78283b2bd24 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.georules + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoGeoIndexValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.georules'] + } + + void 'fails fast when 2d-specific options are used on a 2dsphere index'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('2d-specific geospatial options are only supported for Mongo 2d indexes') + } +} + +@MongoRepository +interface InvalidGeoIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_geo_indexed_entities') +class InvalidGeoIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'invalid_geo_idx', type = MongoGeoIndexType.GEO_2DSPHERE, bits = 26) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy new file mode 100644 index 00000000000..864f93debcf --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy @@ -0,0 +1,53 @@ +package io.micronaut.data.document.mongodb.validation.invalidpath + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundIndexValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb'] + } + + + void 'fails fast for invalid compound index path'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('Invalid Mongo index path [missing]') + } +} + +@MongoRepository +interface InvalidCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_idx', + fields = [ + @MongoCompoundIndexField(value = 'missing', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_compound_indexed_entities') +class InvalidCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy new file mode 100644 index 00000000000..afffb063b6e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.data.document.mongodb.validation.partialfilter + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundIndexPartialFilterValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.partialfilter'] + } + + void 'fails fast when sparse and partialFilterExpression are both defined on a compound index'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('cannot define both sparse and partialFilterExpression') + } +} + +@MongoRepository +interface PartialFilterCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'partial_filter_idx', + sparse = true, + partialFilterExpression = '{ "name": { "$exists": true } }', + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('partial_filter_compound_indexed_entities') +class PartialFilterCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy new file mode 100644 index 00000000000..eff250c2eb6 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.text + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoTextIndexValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.text'] + } + + void 'fails fast for invalid text weight'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.cause != null + e.cause.message.contains('Mongo text index weight must be greater than zero') + } +} + +@MongoRepository +interface InvalidTextWeightEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_text_weight_entities') +class InvalidTextWeightEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'invalid_text_weight_idx', weight = 0) + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy new file mode 100644 index 00000000000..9a4039baa33 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.data.document.mongodb.validation.ttlcompound + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundIndexTtlValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.ttlcompound'] + } + + void 'fails fast for TTL on compound index'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('TTL') + } +} + +@MongoRepository +interface TtlCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'ttl_compound_idx', + expireAfterSeconds = 60, + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'age', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('ttl_compound_indexed_entities') +class TtlCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + Integer age +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy new file mode 100644 index 00000000000..5c511323115 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy @@ -0,0 +1,68 @@ +package io.micronaut.data.document.mongodb.wildcard + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoWildcardIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.wildcard'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field wildcard index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'wildcard_indexed_entities') + assert indexes*.name.contains('wildcard_metadata_idx') + def index = indexes.find { it.name == 'wildcard_metadata_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'metadata.$**' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface WildcardIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('wildcard_indexed_entities') +class WildcardIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoWildcardIndexed(name = 'wildcard_metadata_idx') + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy new file mode 100644 index 00000000000..84fde1241f4 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy @@ -0,0 +1,70 @@ +package io.micronaut.data.document.mongodb.wildcard.toplevel + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoTopLevelWildcardIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.wildcard.toplevel'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates top-level wildcard index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_indexed_entities') + assert indexes*.name.contains('top_level_wildcard_idx') + def index = indexes.find { it.name == 'top_level_wildcard_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == '$**' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface TopLevelWildcardIndexedEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'top_level_wildcard_idx') +@MappedEntity('top_level_wildcard_indexed_entities') +class TopLevelWildcardIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy new file mode 100644 index 00000000000..066d0ca1019 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.wildcard.toplevel + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoTopLevelWildcardProjectionIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.wildcard.toplevel'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates top-level wildcard index with wildcard projection'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_projection_indexed_entities') + assert indexes*.name.contains('top_level_wildcard_projection_idx') + def index = indexes.find { it.name == 'top_level_wildcard_projection_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == '$**' + assert index.fields[0].order() == 1 + assert index.wildcardProjection != null + assert ((Document) index.wildcardProjection).getInteger('metadata.secret') == 0 + } + } +} + +@MongoRepository +interface TopLevelWildcardProjectionIndexedEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'top_level_wildcard_projection_idx', wildcardProjection = '{ "metadata.secret": 0 }') +@MappedEntity('top_level_wildcard_projection_indexed_entities') +class TopLevelWildcardProjectionIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + Map metadata +} From dcbd91f01ba3b599f1d35253457760cb6cd560b9 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sat, 28 Mar 2026 20:11:15 +0100 Subject: [PATCH 02/34] MongoDB indexes support --- .../micronaut/data/annotation/JsonView.java | 4 +- .../annotation/MongoClusteredIndex.java | 51 +++++ .../annotation/MongoWildcardIndexed.java | 5 + .../mongodb/common/MongoEntityIndexes.java | 59 +++++- .../data/mongodb/geo/MongoGeoConverters.java | 78 ++++++++ .../data/mongodb/geo/MongoGeoGeometry.java | 25 +++ .../geo/MongoGeoGeometryCollection.java | 28 +++ .../MongoGeoGeometryCollectionConverter.java | 140 ++++++++++++++ .../data/mongodb/geo/MongoGeoLineString.java | 28 +++ .../geo/MongoGeoLineStringConverter.java | 107 ++++++++++ .../mongodb/geo/MongoGeoMultiLineString.java | 28 +++ .../geo/MongoGeoMultiLineStringConverter.java | 104 ++++++++++ .../data/mongodb/geo/MongoGeoMultiPoint.java | 28 +++ .../geo/MongoGeoMultiPointConverter.java | 93 +++++++++ .../mongodb/geo/MongoGeoMultiPolygon.java | 28 +++ .../geo/MongoGeoMultiPolygonConverter.java | 115 +++++++++++ .../data/mongodb/geo/MongoGeoPoint.java | 2 +- .../mongodb/geo/MongoGeoPointConverter.java | 173 ++++++++++++++++- .../data/mongodb/geo/MongoGeoPolygon.java | 28 +++ .../mongodb/geo/MongoGeoPolygonConverter.java | 104 ++++++++++ .../init/AbstractMongoCollectionsCreator.java | 182 +++++++++++++++++- .../mongodb/init/MongoCollectionsCreator.java | 61 +++++- .../init/MongoReactiveCollectionsCreator.java | 61 +++++- .../operations/DefaultMongoStoredQuery.java | 19 ++ .../mongodb/serde/DataDecoderContext.java | 35 +++- .../mongodb/serde/DataEncoderContext.java | 37 +++- .../mongodb/MongoIndexInspector.groovy | 1 + ...ongoClusteredCollectionCreationSpec.groovy | 69 +++++++ .../MongoCollationIndexCreationSpec.groovy | 6 +- .../MongoCompoundIndexCreationSpec.groovy | 6 +- .../geo/MongoGeoIndexCreationSpec.groovy | 6 +- .../geo2d/MongoGeo2dIndexCreationSpec.groovy | 6 +- .../MongoGeo2dOptionsIndexCreationSpec.groovy | 6 +- .../MongoCompoundGeoIndexCreationSpec.groovy | 6 +- ...MongoGeoPointValueIndexCreationSpec.groovy | 6 +- ...ustomGeoPointValueIndexCreationSpec.groovy | 37 ++-- ...tryCollectionValueIndexCreationSpec.groovy | 108 +++++++++++ ...ustomGeoPointValueIndexCreationSpec.groovy | 95 +++++++++ ...GeoLineStringValueIndexCreationSpec.groovy | 101 ++++++++++ ...ltiLineStringValueIndexCreationSpec.groovy | 109 +++++++++++ ...GeoMultiPointValueIndexCreationSpec.groovy | 101 ++++++++++ ...oMultiPolygonValueIndexCreationSpec.groovy | 118 ++++++++++++ ...ngoGeoPolygonValueIndexCreationSpec.groovy | 104 ++++++++++ .../MongoHashedIndexCreationSpec.groovy | 6 +- ...MongoPartialFilterIndexCreationSpec.groovy | 6 +- ...goReactiveCompoundIndexCreationSpec.groovy | 81 +------- .../MongoReactiveIndexCreationSpec.groovy | 67 +------ ...tiveClusteredCollectionCreationSpec.groovy | 69 +++++++ ...oReactiveCollationIndexCreationSpec.groovy | 71 +------ .../MongoReactiveGeoIndexCreationSpec.groovy | 69 +------ ...MongoReactiveGeo2dIndexCreationSpec.groovy | 70 +------ ...activeGeo2dOptionsIndexCreationSpec.groovy | 71 +------ ...eactiveCompoundGeoIndexCreationSpec.groovy | 81 +------- ...ctiveGeoPointValueIndexCreationSpec.groovy | 75 +------- ...ustomGeoPointValueIndexCreationSpec.groovy | 89 +-------- ...tryCollectionValueIndexCreationSpec.groovy | 18 ++ ...ustomGeoPointValueIndexCreationSpec.groovy | 18 ++ ...GeoLineStringValueIndexCreationSpec.groovy | 18 ++ ...ltiLineStringValueIndexCreationSpec.groovy | 18 ++ ...GeoMultiPointValueIndexCreationSpec.groovy | 18 ++ ...oMultiPolygonValueIndexCreationSpec.groovy | 18 ++ ...iveGeoPolygonValueIndexCreationSpec.groovy | 18 ++ ...ongoReactiveHashedIndexCreationSpec.groovy | 69 +------ ...ctivePartialFilterIndexCreationSpec.groovy | 73 +------ .../MongoReactiveTextIndexCreationSpec.groovy | 68 +------ .../MongoReactiveTtlIndexCreationSpec.groovy | 18 ++ ...goReactiveWildcardIndexCreationSpec.groovy | 68 +------ .../simple/MongoIndexCreationSpec.groovy | 6 +- .../text/MongoTextIndexCreationSpec.groovy | 6 +- .../ttl/MongoTtlIndexCreationSpec.groovy | 73 +++++++ .../MongoClusteredTtlValidationSpec.groovy | 43 +++++ .../MongoClusteredUniqueValidationSpec.groovy | 43 +++++ .../MongoGeoIndexValidationSpec.groovy | 3 +- .../geotype/MongoGeoTypeValidationSpec.groovy | 45 +++++ .../MongoCompoundIndexValidationSpec.groovy | 2 +- ...iveWildcardProjectionValidationSpec.groovy | 45 +++++ ...ngoWildcardProjectionValidationSpec.groovy | 45 +++++ .../MongoWildcardIndexCreationSpec.groovy | 6 +- .../mongo/mongoMapping/mongoIndexes.adoc | 153 +++++++++++++++ src/main/docs/guide/toc.yml | 2 +- 80 files changed, 3244 insertions(+), 913 deletions(-) create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy create mode 100644 src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc diff --git a/data-model/src/main/java/io/micronaut/data/annotation/JsonView.java b/data-model/src/main/java/io/micronaut/data/annotation/JsonView.java index 12cea50775c..519d04caf4a 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/JsonView.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/JsonView.java @@ -32,8 +32,8 @@ * {@code * @JsonView(value = "CONTACT_VIEW", alias = "cv", entity = Contact.class) * public class ContactView { - * @Id - * @GeneratedValue(GeneratedValue.Type.IDENTITY) + * @Id + * @GeneratedValue(GeneratedValue.Type.IDENTITY) * private Long id; * private String name; * private int age; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java new file mode 100644 index 00000000000..f596e924686 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares MongoDB clustered collection options for an entity collection. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Documented +@Inherited +public @interface MongoClusteredIndex { + + /** + * @return The clustered index name. + */ + String name() default ""; + + /** + * @return Whether clustered index is unique. MongoDB requires {@code true}. + */ + boolean unique() default true; + + /** + * @return The collection expiration in seconds for clustered TTL collections. + */ + int expireAfterSeconds() default -1; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java index eda844178b3..492de9d88ee 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java @@ -39,4 +39,9 @@ */ String name() default ""; + /** + * @return The wildcard projection definition as JSON. + */ + String wildcardProjection() default ""; + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index e798fa942dc..c9dc40a4e7d 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -29,6 +29,7 @@ import io.micronaut.data.mongodb.annotation.MongoTextIndexed; import io.micronaut.data.mongodb.annotation.MongoWildcardIndex; import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed; +import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.model.Association; import io.micronaut.data.model.PersistentEntityUtils; import io.micronaut.data.model.runtime.RuntimePersistentEntity; @@ -48,7 +49,6 @@ @Internal public final class MongoEntityIndexes { - private static final Map, MongoEntityIndexes> INDEXES_BY_ENTITY = new ConcurrentHashMap<>(); private final List indexes; @@ -152,6 +152,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? geoAnnotation.intValue("bits").getAsInt() : null; Double min = geoAnnotation.doubleValue("min").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("min").getAsDouble()) ? geoAnnotation.doubleValue("min").getAsDouble() : null; @@ -176,6 +177,10 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null); + if (wildcardProjection != null) { + throw new IllegalStateException("Mongo wildcardProjection on field-level @MongoWildcardIndexed is not supported by MongoDB. Use @MongoWildcardIndex on the entity instead for top-level wildcard projection."); + } indexes.add(new ResolvedIndex( wildcardAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(property.getPersistedName() + ".$**", 1, null, null, null, null)), @@ -194,6 +199,19 @@ private static List resolveFieldIndexes(RuntimePersistentEntity entity, + RuntimePersistentProperty property) { + Class propertyType = property.getType(); + if (MongoGeoConverters.supportsGeoIndexedPropertyType(propertyType)) { + return; + } + throw new IllegalStateException("Mongo geospatial index on entity [" + + entity.getName() + + "] property [" + + property.getName() + + "] requires a supported type (MongoGeoPoint, MongoGeoPointLike, point-like bean shape, MongoGeoMultiPoint, MongoGeoLineString, MongoGeoMultiLineString, MongoGeoPolygon, MongoGeoMultiPolygon, or MongoGeoGeometryCollection)"); + } + private static List resolveTextIndexes(RuntimePersistentEntity entity) { List fields = new ArrayList<>(); String name = null; @@ -229,6 +247,9 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit for (var annotationValue : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoCompoundIndex.class)) { List fields = new ArrayList<>(); java.util.Set seenPaths = new java.util.LinkedHashSet<>(); + Integer indexBits = null; + Double indexMin = null; + Double indexMax = null; for (var fieldAnnotation : annotationValue.getAnnotations("fields", MongoCompoundIndexField.class)) { String path = fieldAnnotation.stringValue().orElseThrow(); String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, path) @@ -267,7 +288,25 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (geoType != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d compound geospatial fields on entity [" + entity.getName() + "]"); } - fields.add(new ResolvedIndexField(persistedPath, null, bits, geoType.getKey(), min, max)); + if (bits != null) { + if (indexBits != null && !indexBits.equals(bits)) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] declares conflicting bits options for geospatial fields"); + } + indexBits = bits; + } + if (min != null) { + if (indexMin != null && !indexMin.equals(min)) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] declares conflicting min options for geospatial fields"); + } + indexMin = min; + } + if (max != null) { + if (indexMax != null && !indexMax.equals(max)) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] declares conflicting max options for geospatial fields"); + } + indexMax = max; + } + fields.add(new ResolvedIndexField(persistedPath, null, null, geoType.getKey(), min, max)); } else { if ((fieldAnnotation.intValue("bits").isPresent() && fieldAnnotation.intValue("bits").getAsInt() >= 0) || (fieldAnnotation.doubleValue("min").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("min").getAsDouble())) @@ -296,9 +335,9 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit null, partialFilterExpression, annotationValue.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), - null, - null, - null, + indexBits, + indexMin, + indexMax, null )); } @@ -313,6 +352,12 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit * @param unique Whether unique * @param sparse Whether sparse * @param expireAfterSeconds TTL in seconds if any + * @param partialFilterExpression The partial filter expression JSON + * @param collation The collation JSON + * @param bits The geospatial bits option for 2d indexes + * @param min The geospatial min option for 2d indexes + * @param max The geospatial max option for 2d indexes + * @param wildcardProjection The wildcard projection JSON */ public record ResolvedIndex(@Nullable String name, List fields, @@ -332,6 +377,10 @@ public record ResolvedIndex(@Nullable String name, * * @param path The persisted path * @param order The field order + * @param weight The text index weight + * @param kind The index kind + * @param min The geospatial min option for 2d indexes + * @param max The geospatial max option for 2d indexes */ public record ResolvedIndexField(String path, @Nullable Integer order, @Nullable Integer weight, @Nullable String kind, @Nullable Double min, @Nullable Double max) { } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java new file mode 100644 index 00000000000..1250daf7f34 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.Map; + +/** + * Utility methods for implicit Mongo geospatial converter resolution. + * + * @author radovanradic + * @since 5.0.0 + */ +public final class MongoGeoConverters { + + private MongoGeoConverters() { + } + + /** + * @param type The property type + * @return Whether this type has implicit geospatial converter support + */ + public static boolean supportsImplicitGeoType(Class type) { + return MongoGeoPointConverter.supportsImplicitPointType(type) + || MongoGeoLineStringConverter.supportsImplicitLineStringType(type) + || MongoGeoMultiLineStringConverter.supportsImplicitMultiLineStringType(type) + || MongoGeoMultiPointConverter.supportsImplicitMultiPointType(type) + || MongoGeoGeometryCollectionConverter.supportsImplicitGeometryCollectionType(type) + || MongoGeoMultiPolygonConverter.supportsImplicitMultiPolygonType(type) + || MongoGeoPolygonConverter.supportsImplicitPolygonType(type); + } + + /** + * @param type The property type + * @return The implicit converter class for this geospatial type + */ + public static Class resolveImplicitGeoConverterClass(Class type) { + if (MongoGeoMultiPointConverter.supportsImplicitMultiPointType(type)) { + return MongoGeoMultiPointConverter.class; + } + if (MongoGeoGeometryCollectionConverter.supportsImplicitGeometryCollectionType(type)) { + return MongoGeoGeometryCollectionConverter.class; + } + if (MongoGeoMultiLineStringConverter.supportsImplicitMultiLineStringType(type)) { + return MongoGeoMultiLineStringConverter.class; + } + if (MongoGeoMultiPolygonConverter.supportsImplicitMultiPolygonType(type)) { + return MongoGeoMultiPolygonConverter.class; + } + if (MongoGeoLineStringConverter.supportsImplicitLineStringType(type)) { + return MongoGeoLineStringConverter.class; + } + if (MongoGeoPolygonConverter.supportsImplicitPolygonType(type)) { + return MongoGeoPolygonConverter.class; + } + return MongoGeoPointConverter.class; + } + + /** + * @param type The property type + * @return Whether this type is supported for Mongo geospatial indexing + */ + public static boolean supportsGeoIndexedPropertyType(Class type) { + return Map.class.isAssignableFrom(type) || supportsImplicitGeoType(type); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java new file mode 100644 index 00000000000..129491b9d0f --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +/** + * Marker interface for modeled Mongo GeoJSON geometry values. + * + * @author radovanradic + * @since 5.0.0 + */ +public interface MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java new file mode 100644 index 00000000000..fa060b50ca8 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON geometry-collection value for MongoDB geospatial fields. + * + * @param geometries Ordered list of modeled GeoJSON geometries. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoGeometryCollection(List geometries) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java new file mode 100644 index 00000000000..150e2b6a784 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java @@ -0,0 +1,140 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Converts {@link MongoGeoGeometryCollection} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoGeometryCollectionConverter implements AttributeConverter> { + + private static final MongoGeoPointConverter POINT_CONVERTER = new MongoGeoPointConverter(); + private static final MongoGeoMultiPointConverter MULTI_POINT_CONVERTER = new MongoGeoMultiPointConverter(); + private static final MongoGeoLineStringConverter LINE_STRING_CONVERTER = new MongoGeoLineStringConverter(); + private static final MongoGeoMultiLineStringConverter MULTI_LINE_STRING_CONVERTER = new MongoGeoMultiLineStringConverter(); + private static final MongoGeoPolygonConverter POLYGON_CONVERTER = new MongoGeoPolygonConverter(); + private static final MongoGeoMultiPolygonConverter MULTI_POLYGON_CONVERTER = new MongoGeoMultiPolygonConverter(); + private static final MongoGeoGeometryCollectionConverter GEOMETRY_COLLECTION_CONVERTER = new MongoGeoGeometryCollectionConverter(); + + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoGeometryCollection geometryCollection)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial geometry-collection value type: " + entityValue.getClass().getName()); + } + List> geometries = new ArrayList<>(geometryCollection.geometries().size()); + for (MongoGeoGeometry geometry : geometryCollection.geometries()) { + geometries.add(convertGeometryToMap(geometry)); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "GeometryCollection"); + geoJson.put("geometries", geometries); + return geoJson; + } + + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"GeometryCollection".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON geometry-collection type: " + type); + } + Object geometries = persistedValue.get("geometries"); + if (!(geometries instanceof List geometryList)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoGeometryCollection value: " + persistedValue); + } + List geometryValues = new ArrayList<>(geometryList.size()); + for (Object geometry : geometryList) { + if (!(geometry instanceof Map geometryMapRaw)) { + throw new IllegalArgumentException("Invalid GeoJSON geometry entry: " + geometry); + } + Map geometryMap = (Map) geometryMapRaw; + geometryValues.add(convertMapToGeometry(geometryMap)); + } + return new MongoGeoGeometryCollection(List.copyOf(geometryValues)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit geometry-collection conversion + */ + public static boolean supportsImplicitGeometryCollectionType(Class type) { + return type == MongoGeoGeometryCollection.class || type.isAssignableFrom(MongoGeoGeometryCollection.class); + } + + private static Map convertGeometryToMap(MongoGeoGeometry geometry) { + if (geometry instanceof MongoGeoPoint point) { + return Objects.requireNonNull(POINT_CONVERTER.convertToPersistedValue(point, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoMultiPoint multiPoint) { + return Objects.requireNonNull(MULTI_POINT_CONVERTER.convertToPersistedValue(multiPoint, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoLineString lineString) { + return Objects.requireNonNull(LINE_STRING_CONVERTER.convertToPersistedValue(lineString, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoMultiLineString multiLineString) { + return Objects.requireNonNull(MULTI_LINE_STRING_CONVERTER.convertToPersistedValue(multiLineString, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoPolygon polygon) { + return Objects.requireNonNull(POLYGON_CONVERTER.convertToPersistedValue(polygon, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoMultiPolygon multiPolygon) { + return Objects.requireNonNull(MULTI_POLYGON_CONVERTER.convertToPersistedValue(multiPolygon, ConversionContext.DEFAULT)); + } + if (geometry instanceof MongoGeoGeometryCollection geometryCollection) { + return Objects.requireNonNull(GEOMETRY_COLLECTION_CONVERTER.convertToPersistedValue(geometryCollection, ConversionContext.DEFAULT)); + } + throw new IllegalArgumentException("Unsupported geometry collection entry type: " + geometry.getClass().getName()); + } + + private static MongoGeoGeometry convertMapToGeometry(Map geometryMap) { + Object type = geometryMap.get("type"); + if (!(type instanceof String geometryType)) { + throw new IllegalArgumentException("Invalid GeoJSON geometry entry type: " + geometryMap); + } + return switch (geometryType) { + case "Point" -> (MongoGeoGeometry) Objects.requireNonNull(POINT_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiPoint" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_POINT_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "LineString" -> (MongoGeoGeometry) Objects.requireNonNull(LINE_STRING_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiLineString" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_LINE_STRING_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "Polygon" -> (MongoGeoGeometry) Objects.requireNonNull(POLYGON_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiPolygon" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_POLYGON_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "GeometryCollection" -> (MongoGeoGeometry) Objects.requireNonNull(GEOMETRY_COLLECTION_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + default -> throw new IllegalArgumentException("Unsupported GeoJSON geometry collection entry type: " + geometryType); + }; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java new file mode 100644 index 00000000000..0ce742d8b66 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON line string value for MongoDB geospatial fields. + * + * @param coordinates Ordered list of points in this line string. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoLineString(List coordinates) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java new file mode 100644 index 00000000000..9b9ad7fb015 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoLineString} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoLineStringConverter implements AttributeConverter> { + + /** + * Converts a line string modeled value to a persisted GeoJSON map. + * + * @param entityValue The modeled geospatial value + * @param context The conversion context + * @return The persisted GeoJSON map or {@code null} + */ + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoLineString lineString)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial line string value type: " + entityValue.getClass().getName()); + } + List> coordinates = new ArrayList<>(lineString.coordinates().size()); + for (MongoGeoPoint point : lineString.coordinates()) { + coordinates.add(List.of(point.x(), point.y())); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "LineString"); + geoJson.put("coordinates", coordinates); + return geoJson; + } + + /** + * Converts a persisted GeoJSON map to a line string modeled value. + * + * @param persistedValue The persisted GeoJSON map + * @param context The conversion context + * @return The line string modeled value or {@code null} + */ + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"LineString".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON line string type: " + type); + } + Object coordinates = persistedValue.get("coordinates"); + if (!(coordinates instanceof List coordinatePairs)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoLineString value: " + persistedValue); + } + List lineCoordinates = new ArrayList<>(coordinatePairs.size()); + for (Object pointValue : coordinatePairs) { + if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { + throw new IllegalArgumentException("Invalid GeoJSON line string coordinate pair: " + pointValue); + } + Object x = coordinatePair.get(0); + Object y = coordinatePair.get(1); + if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { + throw new IllegalArgumentException("Invalid GeoJSON line string numeric coordinates: " + pointValue); + } + lineCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); + } + return new MongoGeoLineString(List.copyOf(lineCoordinates)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit line string conversion + */ + public static boolean supportsImplicitLineStringType(Class type) { + return type == MongoGeoLineString.class || type.isAssignableFrom(MongoGeoLineString.class); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java new file mode 100644 index 00000000000..48b1b824fcb --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON multi-line-string value for MongoDB geospatial fields. + * + * @param coordinates Line string list, each line string as ordered points. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoMultiLineString(List> coordinates) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java new file mode 100644 index 00000000000..a3a885d3735 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoMultiLineString} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoMultiLineStringConverter implements AttributeConverter> { + + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoMultiLineString multiLineString)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial multi-line-string value type: " + entityValue.getClass().getName()); + } + List>> coordinates = new ArrayList<>(multiLineString.coordinates().size()); + for (List lineString : multiLineString.coordinates()) { + List> persistedLineString = new ArrayList<>(lineString.size()); + for (MongoGeoPoint point : lineString) { + persistedLineString.add(List.of(point.x(), point.y())); + } + coordinates.add(persistedLineString); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "MultiLineString"); + geoJson.put("coordinates", coordinates); + return geoJson; + } + + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"MultiLineString".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-line-string type: " + type); + } + Object coordinates = persistedValue.get("coordinates"); + if (!(coordinates instanceof List lineStrings)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoMultiLineString value: " + persistedValue); + } + List> multiLineStringCoordinates = new ArrayList<>(lineStrings.size()); + for (Object lineStringValue : lineStrings) { + if (!(lineStringValue instanceof List lineString)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-line-string line-string: " + lineStringValue); + } + List lineCoordinates = new ArrayList<>(lineString.size()); + for (Object pointValue : lineString) { + if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { + throw new IllegalArgumentException("Invalid GeoJSON multi-line-string coordinate pair: " + pointValue); + } + Object x = coordinatePair.get(0); + Object y = coordinatePair.get(1); + if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-line-string numeric coordinates: " + pointValue); + } + lineCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); + } + multiLineStringCoordinates.add(lineCoordinates); + } + return new MongoGeoMultiLineString(List.copyOf(multiLineStringCoordinates)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit multi-line-string conversion + */ + public static boolean supportsImplicitMultiLineStringType(Class type) { + return type == MongoGeoMultiLineString.class || type.isAssignableFrom(MongoGeoMultiLineString.class); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java new file mode 100644 index 00000000000..80a9910569c --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON multi-point value for MongoDB geospatial fields. + * + * @param coordinates Point list in this multi-point geometry. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoMultiPoint(List coordinates) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java new file mode 100644 index 00000000000..f26c77404c6 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoMultiPoint} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoMultiPointConverter implements AttributeConverter> { + + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoMultiPoint multiPoint)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial multi-point value type: " + entityValue.getClass().getName()); + } + List> coordinates = new ArrayList<>(multiPoint.coordinates().size()); + for (MongoGeoPoint point : multiPoint.coordinates()) { + coordinates.add(List.of(point.x(), point.y())); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "MultiPoint"); + geoJson.put("coordinates", coordinates); + return geoJson; + } + + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"MultiPoint".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-point type: " + type); + } + Object coordinates = persistedValue.get("coordinates"); + if (!(coordinates instanceof List points)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoMultiPoint value: " + persistedValue); + } + List multiPointCoordinates = new ArrayList<>(points.size()); + for (Object pointValue : points) { + if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { + throw new IllegalArgumentException("Invalid GeoJSON multi-point coordinate pair: " + pointValue); + } + Object x = coordinatePair.get(0); + Object y = coordinatePair.get(1); + if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-point numeric coordinates: " + pointValue); + } + multiPointCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); + } + return new MongoGeoMultiPoint(List.copyOf(multiPointCoordinates)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit multi-point conversion + */ + public static boolean supportsImplicitMultiPointType(Class type) { + return type == MongoGeoMultiPoint.class || type.isAssignableFrom(MongoGeoMultiPoint.class); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java new file mode 100644 index 00000000000..59758293570 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON multi-polygon value for MongoDB geospatial fields. + * + * @param coordinates Polygon list, each polygon as list of rings, each ring as ordered points. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoMultiPolygon(List>> coordinates) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java new file mode 100644 index 00000000000..beb1d01854a --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoMultiPolygon} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoMultiPolygonConverter implements AttributeConverter> { + + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoMultiPolygon multiPolygon)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial multi-polygon value type: " + entityValue.getClass().getName()); + } + List>>> coordinates = new ArrayList<>(multiPolygon.coordinates().size()); + for (List> polygon : multiPolygon.coordinates()) { + List>> persistedPolygon = new ArrayList<>(polygon.size()); + for (List ring : polygon) { + List> persistedRing = new ArrayList<>(ring.size()); + for (MongoGeoPoint point : ring) { + persistedRing.add(List.of(point.x(), point.y())); + } + persistedPolygon.add(persistedRing); + } + coordinates.add(persistedPolygon); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "MultiPolygon"); + geoJson.put("coordinates", coordinates); + return geoJson; + } + + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"MultiPolygon".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-polygon type: " + type); + } + Object coordinates = persistedValue.get("coordinates"); + if (!(coordinates instanceof List polygons)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoMultiPolygon value: " + persistedValue); + } + List>> multiPolygonCoordinates = new ArrayList<>(polygons.size()); + for (Object polygonValue : polygons) { + if (!(polygonValue instanceof List rings)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-polygon polygon: " + polygonValue); + } + List> polygonRings = new ArrayList<>(rings.size()); + for (Object ringValue : rings) { + if (!(ringValue instanceof List ring)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-polygon ring: " + ringValue); + } + List polygonRing = new ArrayList<>(ring.size()); + for (Object pointValue : ring) { + if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { + throw new IllegalArgumentException("Invalid GeoJSON multi-polygon coordinate pair: " + pointValue); + } + Object x = coordinatePair.get(0); + Object y = coordinatePair.get(1); + if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { + throw new IllegalArgumentException("Invalid GeoJSON multi-polygon numeric coordinates: " + pointValue); + } + polygonRing.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); + } + polygonRings.add(polygonRing); + } + multiPolygonCoordinates.add(polygonRings); + } + return new MongoGeoMultiPolygon(List.copyOf(multiPolygonCoordinates)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit multi-polygon conversion + */ + public static boolean supportsImplicitMultiPolygonType(Class type) { + return type == MongoGeoMultiPolygon.class || type.isAssignableFrom(MongoGeoMultiPolygon.class); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java index 1e20060ddd3..c7f25155a51 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java @@ -26,5 +26,5 @@ * @since 5.0.0 */ @Serdeable -public record MongoGeoPoint(double x, double y) implements MongoGeoPointLike { +public record MongoGeoPoint(double x, double y) implements MongoGeoPointLike, MongoGeoGeometry { } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java index 3b2c2a40944..b6c62002184 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java @@ -15,9 +15,14 @@ */ package io.micronaut.data.mongodb.geo; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanProperty; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.annotation.NonNull; import org.jspecify.annotations.Nullable; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; import java.util.LinkedHashMap; import java.util.List; @@ -29,24 +34,68 @@ * @author radovanradic * @since 5.0.0 */ -public final class MongoGeoPointConverter implements AttributeConverter> { +@Singleton +public final class MongoGeoPointConverter implements AttributeConverter> { + + private static final String[] LONGITUDE_NAMES = {"x", "longitude", "lng", "lon"}; + private static final String[] LATITUDE_NAMES = {"y", "latitude", "lat"}; @Override - public @Nullable Map convertToPersistedValue(@Nullable MongoGeoPointLike entityValue, @NonNull io.micronaut.core.convert.ConversionContext context) { + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { if (entityValue == null) { return null; } - Map point = new LinkedHashMap<>(); - point.put("type", "Point"); - point.put("coordinates", List.of(entityValue.x(), entityValue.y())); - return point; + MongoGeoPoint point; + if (entityValue instanceof MongoGeoPointLike pointLike) { + point = new MongoGeoPoint(pointLike.x(), pointLike.y()); + } else { + point = toPoint(entityValue); + } + Map geoJsonPoint = new LinkedHashMap<>(); + geoJsonPoint.put("type", "Point"); + geoJsonPoint.put("coordinates", List.of(point.x(), point.y())); + return geoJsonPoint; } @Override - public @Nullable MongoGeoPointLike convertToEntityValue(@Nullable Map persistedValue, @NonNull io.micronaut.core.convert.ConversionContext context) { + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { if (persistedValue == null) { return null; } + MongoGeoPoint point = toPoint(persistedValue); + Class targetType = MongoGeoPoint.class; + if (context instanceof ArgumentConversionContext argumentConversionContext) { + targetType = argumentConversionContext.getArgument().getType(); + } + if (targetType == Object.class + || targetType == MongoGeoPoint.class + || targetType == MongoGeoPointLike.class + || targetType.isAssignableFrom(MongoGeoPoint.class)) { + return point; + } + return toTargetType(point, targetType); + } + + private MongoGeoPoint toPoint(Object value) { + if (value instanceof MongoGeoPointLike pointLike) { + return new MongoGeoPoint(pointLike.x(), pointLike.y()); + } + if (value instanceof Map map) { + return pointFromMap(map); + } + BeanIntrospection introspection = BeanIntrospection.getIntrospection(value.getClass()); + double longitude = readCoordinate(introspection, value, LONGITUDE_NAMES); + double latitude = readCoordinate(introspection, value, LATITUDE_NAMES); + return new MongoGeoPoint(longitude, latitude); + } + + private MongoGeoPoint pointFromMap(Map persistedValue) { + Object type = persistedValue.get("type"); + if (type != null && !"Point".equals(type)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoPoint type: " + type); + } Object coordinates = persistedValue.get("coordinates"); if (coordinates instanceof List list && list.size() == 2) { Object x = list.get(0); @@ -57,4 +106,114 @@ public final class MongoGeoPointConverter implements AttributeConverter introspection, Object value, String[] coordinateNames) { + for (String coordinateName : coordinateNames) { + @SuppressWarnings("unchecked") + BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); + if (beanProperty == null) { + continue; + } + Object coordinateValue = beanProperty.get(value); + if (coordinateValue instanceof Number number) { + return number.doubleValue(); + } + } + throw new IllegalArgumentException("Cannot extract Mongo geospatial coordinates from type [" + + introspection.getBeanType().getName() + + "]; expected numeric properties named one of " + + List.of(coordinateNames)); + } + + private Object toTargetType(MongoGeoPoint point, Class targetType) { + BeanIntrospection introspection = BeanIntrospection.getIntrospection(targetType); + Object instance; + try { + instance = introspection.instantiate(); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot instantiate Mongo geospatial type [" + + targetType.getName() + + "] from coordinates. Provide a no-args constructor or use MongoGeoPointLike.", e); + } + writeCoordinate(introspection, instance, point.x(), LONGITUDE_NAMES); + writeCoordinate(introspection, instance, point.y(), LATITUDE_NAMES); + return instance; + } + + private void writeCoordinate(BeanIntrospection introspection, + Object instance, + double value, + String[] coordinateNames) { + for (String coordinateName : coordinateNames) { + @SuppressWarnings("unchecked") + BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); + if (beanProperty == null || beanProperty.isReadOnly()) { + continue; + } + Class propertyType = beanProperty.getType(); + if (propertyType == double.class || propertyType == Double.class) { + beanProperty.set(instance, value); + return; + } + if (propertyType == float.class || propertyType == Float.class) { + beanProperty.set(instance, (float) value); + return; + } + if (propertyType == int.class || propertyType == Integer.class) { + beanProperty.set(instance, (int) value); + return; + } + if (propertyType == long.class || propertyType == Long.class) { + beanProperty.set(instance, (long) value); + return; + } + } + throw new IllegalArgumentException("Cannot write Mongo geospatial coordinate into type [" + + introspection.getBeanType().getName() + + "]; expected writable numeric property named one of " + + List.of(coordinateNames)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit point conversion + */ + public static boolean supportsImplicitPointType(Class type) { + if (type == MongoGeoPoint.class + || type == MongoGeoPointLike.class + || MongoGeoPointLike.class.isAssignableFrom(type) + || type.isAssignableFrom(MongoGeoPoint.class)) { + return true; + } + return hasPointLikeBeanShape(type); + } + + private static boolean hasPointLikeBeanShape(Class type) { + BeanIntrospection introspection; + try { + introspection = BeanIntrospection.getIntrospection(type); + } catch (Exception e) { + return false; + } + return hasNumericCoordinateProperty(introspection, LONGITUDE_NAMES) + && hasNumericCoordinateProperty(introspection, LATITUDE_NAMES); + } + + private static boolean hasNumericCoordinateProperty(BeanIntrospection introspection, String[] coordinateNames) { + for (String coordinateName : coordinateNames) { + @SuppressWarnings("unchecked") + BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); + if (beanProperty == null || beanProperty.isReadOnly()) { + continue; + } + Class propertyType = beanProperty.getType(); + if (propertyType == double.class || propertyType == Double.class + || propertyType == float.class || propertyType == Float.class + || propertyType == int.class || propertyType == Integer.class + || propertyType == long.class || propertyType == Long.class) { + return true; + } + } + return false; + } } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java new file mode 100644 index 00000000000..9672b519ea6 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import java.util.List; + +/** + * Minimal GeoJSON polygon value for MongoDB geospatial fields. + * + * @param coordinates Polygon rings, each ring as ordered list of points. + * @author radovanradic + * @since 5.0.0 + */ +public record MongoGeoPolygon(List> coordinates) implements MongoGeoGeometry { +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java new file mode 100644 index 00000000000..cb2ca8074c4 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.geo; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Singleton; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Converts {@link MongoGeoPolygon} to and from a GeoJSON persisted map. + * + * @author radovanradic + * @since 5.0.0 + */ +@Singleton +public final class MongoGeoPolygonConverter implements AttributeConverter> { + + @Override + public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, + @NonNull ConversionContext context) { + if (entityValue == null) { + return null; + } + if (!(entityValue instanceof MongoGeoPolygon polygon)) { + throw new IllegalArgumentException("Unsupported Mongo geospatial polygon value type: " + entityValue.getClass().getName()); + } + List>> coordinates = new ArrayList<>(polygon.coordinates().size()); + for (List ring : polygon.coordinates()) { + List> persistedRing = new ArrayList<>(ring.size()); + for (MongoGeoPoint point : ring) { + persistedRing.add(List.of(point.x(), point.y())); + } + coordinates.add(persistedRing); + } + Map geoJson = new LinkedHashMap<>(); + geoJson.put("type", "Polygon"); + geoJson.put("coordinates", coordinates); + return geoJson; + } + + @Override + public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, + @NonNull ConversionContext context) { + if (persistedValue == null) { + return null; + } + Object type = persistedValue.get("type"); + if (type != null && !"Polygon".equals(type)) { + throw new IllegalArgumentException("Invalid GeoJSON polygon type: " + type); + } + Object coordinates = persistedValue.get("coordinates"); + if (!(coordinates instanceof List rings)) { + throw new IllegalArgumentException("Invalid persisted MongoGeoPolygon value: " + persistedValue); + } + List> polygonRings = new ArrayList<>(rings.size()); + for (Object ringValue : rings) { + if (!(ringValue instanceof List ring)) { + throw new IllegalArgumentException("Invalid GeoJSON polygon ring: " + ringValue); + } + List polygonRing = new ArrayList<>(ring.size()); + for (Object pointValue : ring) { + if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { + throw new IllegalArgumentException("Invalid GeoJSON polygon coordinate pair: " + pointValue); + } + Object x = coordinatePair.get(0); + Object y = coordinatePair.get(1); + if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { + throw new IllegalArgumentException("Invalid GeoJSON polygon numeric coordinates: " + pointValue); + } + polygonRing.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); + } + polygonRings.add(polygonRing); + } + return new MongoGeoPolygon(List.copyOf(polygonRings)); + } + + /** + * @param type The property type + * @return Whether this type should use implicit polygon conversion + */ + public static boolean supportsImplicitPolygonType(Class type) { + return type == MongoGeoPolygon.class || type.isAssignableFrom(MongoGeoPolygon.class); + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index c8afcaff8fa..a636006934b 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -32,7 +32,7 @@ import io.micronaut.data.model.naming.NamingStrategy; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.model.runtime.RuntimePersistentEntity; -import io.micronaut.data.mongodb.annotation.MongoIndexDirection; +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex; import io.micronaut.data.mongodb.common.MongoEntityIndexes; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; import io.micronaut.data.mongodb.operations.MongoCollectionNameProvider; @@ -47,6 +47,10 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Date; /** * MongoDB's collections creator. @@ -124,12 +128,19 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, Dtbs database = databaseOperations.find(entity); Set collections = databaseOperations.listCollectionNames(database); String persistedName = mongoCollectionNameProvider.provide(entity); + MongoResolvedCollectionOptions desiredCollectionOptions = resolveCollectionOptions(entity); boolean collectionExists = collections.contains(persistedName); + if (collectionExists && desiredCollectionOptions != null) { + MongoResolvedCollectionOptions existingCollectionOptions = databaseOperations.getCollectionOptions(database, persistedName); + if (!desiredCollectionOptions.matches(existingCollectionOptions)) { + throw new IllegalStateException("Conflicting existing MongoDB collection options for entity [" + entity.getName() + "] and collection [" + persistedName + "]: desired " + desiredCollectionOptions.describe() + ", existing " + (existingCollectionOptions == null ? "null" : existingCollectionOptions.describe())); + } + } if (!collectionExists && createCollections) { if (LOG.isInfoEnabled()) { LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); } - databaseOperations.createCollection(database, persistedName); + databaseOperations.createCollection(database, persistedName, desiredCollectionOptions); collections.add(persistedName); } if ((collectionExists || createCollections) && createIndexes) { @@ -158,13 +169,47 @@ private void createJoinCollections(DatabaseOperations databaseOperations, if (LOG.isInfoEnabled()) { LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); } - databaseOperations.createCollection(database, joinCollectionName); + databaseOperations.createCollection(database, joinCollectionName, null); } } } } } + @Nullable + private MongoResolvedCollectionOptions resolveCollectionOptions(PersistentEntity entity) { + RuntimePersistentEntity runtimePersistentEntity = (RuntimePersistentEntity) entity; + var annotation = runtimePersistentEntity.getAnnotationMetadata().getAnnotation(MongoClusteredIndex.class); + if (annotation == null) { + return null; + } + boolean unique = annotation.booleanValue("unique").orElse(true); + if (!unique) { + throw new IllegalStateException("Mongo clustered index for entity [" + entity.getName() + "] must be unique=true"); + } + Integer expireAfterSeconds = annotation.intValue("expireAfterSeconds").isPresent() && annotation.intValue("expireAfterSeconds").getAsInt() >= 0 + ? annotation.intValue("expireAfterSeconds").getAsInt() : null; + if (expireAfterSeconds != null) { + PersistentProperty identity = entity.getIdentity(); + if (identity == null) { + throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires an identity property"); + } + String idType = identity.getTypeName(); + boolean supportedTtlIdType = idType.equals(Date.class.getName()) + || idType.equals(Instant.class.getName()) + || idType.equals(LocalDateTime.class.getName()) + || idType.equals(OffsetDateTime.class.getName()); + if (!supportedTtlIdType) { + throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires a date/time identity type, but found [" + idType + "]"); + } + } + return new MongoResolvedCollectionOptions( + annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + unique, + expireAfterSeconds + ); + } + private void createIndexes(DatabaseOperations databaseOperations, Dtbs database, PersistentEntity entity, @@ -206,13 +251,66 @@ private List resolveIndexes(PersistentEntity entity) { List fields = index.fields().stream() .map(field -> new MongoResolvedIndexField(field.path(), field.order(), field.weight(), field.kind(), field.min(), field.max())) .toList(); - indexes.add(new MongoResolvedIndex(index.name(), fields, index.unique(), index.sparse(), index.expireAfterSeconds(), index.partialFilterExpression(), index.collation(), index.bits(), index.min(), index.max(), index.wildcardProjection())); + indexes.add(new MongoResolvedIndex( + index.name(), + fields, + index.unique(), + index.sparse(), + index.expireAfterSeconds(), + normalizeJsonString(index.partialFilterExpression()), + normalizeJsonString(index.collation()), + index.bits(), + index.min(), + index.max(), + normalizeJsonString(index.wildcardProjection()) + )); } return indexes; } - private int toOrder(MongoIndexDirection direction) { - return direction == MongoIndexDirection.DESC ? -1 : 1; + static @Nullable String normalizeJsonValue(@Nullable Object value) { + if (value == null) { + return null; + } + if (value instanceof Document document) { + return document.toJson(); + } + if (value instanceof org.bson.BsonDocument bsonDocument) { + return bsonDocument.toJson(); + } + if (value instanceof String stringValue) { + return normalizeJsonString(stringValue); + } + return value.toString(); + } + + static @Nullable String normalizeJsonString(@Nullable String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + if (trimmed.isEmpty()) { + return null; + } + try { + return Document.parse(trimmed).toJson(); + } catch (RuntimeException ignored) { + return trimmed; + } + } + + static @Nullable Integer toInteger(@Nullable Object value) { + if (value instanceof Number number) { + return number.intValue(); + } + return null; + } + + static @Nullable Double toDouble(@Nullable Object value) { + if (value instanceof Number number) { + return number.doubleValue(); + } + return null; } /** @@ -264,12 +362,23 @@ interface DatabaseOperations { Set listCollectionNames(Dtbs database); /** - * Create a collection in the given database. + * Create a collection in the given database with options. * * @param database The database * @param collection The collection + * @param options The resolved collection options */ - void createCollection(Dtbs database, String collection); + void createCollection(Dtbs database, String collection, @Nullable MongoResolvedCollectionOptions options); + + /** + * Read collection options for an existing collection. + * + * @param database The database + * @param collection The collection + * @return The collection options if clustered, or {@code null} + */ + @Nullable + MongoResolvedCollectionOptions getCollectionOptions(Dtbs database, String collection); /** * List indexes for the given collection. @@ -291,6 +400,26 @@ interface DatabaseOperations { } + @Internal + record MongoResolvedCollectionOptions(@Nullable String clusteredIndexName, + boolean clusteredIndexUnique, + @Nullable Integer expireAfterSeconds) { + + boolean matches(@Nullable MongoResolvedCollectionOptions other) { + return other != null + && clusteredIndexUnique == other.clusteredIndexUnique + && Objects.equals(expireAfterSeconds, other.expireAfterSeconds) + && (clusteredIndexName == null || other.clusteredIndexName == null || Objects.equals(clusteredIndexName, other.clusteredIndexName)); + } + + String describe() { + return "MongoResolvedCollectionOptions{clusteredIndexName=" + clusteredIndexName + + ", clusteredIndexUnique=" + clusteredIndexUnique + + ", expireAfterSeconds=" + expireAfterSeconds + + '}'; + } + } + @Internal record MongoResolvedIndexField(String path, @Nullable Integer order, @Nullable Integer weight, @Nullable String kind, @Nullable Double min, @Nullable Double max) { } @@ -317,13 +446,35 @@ boolean matchesManagedOptions(MongoResolvedIndex other) { && sparse == other.sparse && Objects.equals(expireAfterSeconds, other.expireAfterSeconds) && Objects.equals(partialFilterExpression, other.partialFilterExpression) - && Objects.equals(collation, other.collation) + && collationMatches(collation, other.collation) && Objects.equals(bits, other.bits) && Objects.equals(min, other.min) && Objects.equals(max, other.max) && Objects.equals(wildcardProjection, other.wildcardProjection); } + private static boolean collationMatches(@Nullable String existingCollation, + @Nullable String desiredCollation) { + if (desiredCollation == null) { + return existingCollation == null; + } + if (existingCollation == null) { + return false; + } + try { + Document desired = Document.parse(desiredCollation); + Document existing = Document.parse(existingCollation); + for (String key : desired.keySet()) { + if (!Objects.equals(existing.get(key), desired.get(key))) { + return false; + } + } + return true; + } catch (RuntimeException ignored) { + return Objects.equals(existingCollation, desiredCollation); + } + } + Document keysDocument() { Document document = new Document(); for (MongoResolvedIndexField field : fields) { @@ -339,7 +490,18 @@ Document keysDocument() { } String describe() { - return "MongoResolvedIndex{name=" + name + ", fields=" + fields + ", unique=" + unique + ", sparse=" + sparse + ", expireAfterSeconds=" + expireAfterSeconds + ", partialFilterExpression=" + partialFilterExpression + '}'; + return "MongoResolvedIndex{name=" + name + + ", fields=" + fields + + ", unique=" + unique + + ", sparse=" + sparse + + ", expireAfterSeconds=" + expireAfterSeconds + + ", partialFilterExpression=" + partialFilterExpression + + ", collation=" + collation + + ", bits=" + bits + + ", min=" + min + + ", max=" + max + + ", wildcardProjection=" + wildcardProjection + + '}'; } } } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 9ed48610c84..84e3833bf57 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -20,6 +20,8 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Collation; import com.mongodb.client.model.CollationStrength; +import com.mongodb.client.model.ClusteredIndexOptions; +import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.IndexOptions; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.context.BeanLocator; @@ -27,8 +29,8 @@ import io.micronaut.context.env.Environment; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.core.util.StringUtils; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; @@ -44,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * MongoDB's collections creator. @@ -87,8 +90,44 @@ public Set listCollectionNames(MongoDatabase database) { } @Override - public void createCollection(MongoDatabase database, String collection) { - database.createCollection(collection); + public void createCollection(MongoDatabase database, String collection, @Nullable MongoResolvedCollectionOptions options) { + if (options == null) { + database.createCollection(collection); + return; + } + CreateCollectionOptions collectionOptions = new CreateCollectionOptions(); + ClusteredIndexOptions clusteredIndexOptions = new ClusteredIndexOptions(new Document("_id", 1), options.clusteredIndexUnique()); + if (options.clusteredIndexName() != null) { + clusteredIndexOptions.name(options.clusteredIndexName()); + } + collectionOptions.clusteredIndexOptions(clusteredIndexOptions); + if (options.expireAfterSeconds() != null) { + collectionOptions.expireAfter(options.expireAfterSeconds().longValue(), TimeUnit.SECONDS); + } + database.createCollection(collection, collectionOptions); + } + + @Override + public @Nullable MongoResolvedCollectionOptions getCollectionOptions(MongoDatabase database, String collection) { + for (Document collectionDocument : database.listCollections()) { + if (!collection.equals(collectionDocument.getString("name"))) { + continue; + } + Document options = collectionDocument.get("options", Document.class); + if (options == null) { + return null; + } + Document clustered = options.get("clusteredIndex", Document.class); + if (clustered == null) { + return null; + } + return new MongoResolvedCollectionOptions( + clustered.getString("name"), + clustered.getBoolean("unique", true), + options.getInteger("expireAfterSeconds") + ); + } + return null; } @Override @@ -115,12 +154,12 @@ public List listIndexes(MongoDatabase database, String colle indexDocument.getBoolean("unique", false), indexDocument.getBoolean("sparse", false), indexDocument.getInteger("expireAfterSeconds"), - indexDocument.get("partialFilterExpression") == null ? null : indexDocument.get("partialFilterExpression").toString(), - indexDocument.get("collation") == null ? null : indexDocument.get("collation").toString(), - null, - null, - null, - indexDocument.get("wildcardProjection") == null ? null : indexDocument.get("wildcardProjection").toString() + normalizeJsonValue(indexDocument.get("partialFilterExpression")), + normalizeJsonValue(indexDocument.get("collation")), + toInteger(indexDocument.get("bits")), + toDouble(indexDocument.get("min")), + toDouble(indexDocument.get("max")), + normalizeJsonValue(indexDocument.get("wildcardProjection")) )); } return indexes; @@ -151,13 +190,15 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.max() != null) { indexOptions.max(index.max()); } + if (index.wildcardProjection() != null) { + indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); + } mongoCollection.createIndex(index.keysDocument(), indexOptions); } }; }, mongoCollectionNameProvider); } - private Collation toCollation(Document document) { Collation.Builder builder = Collation.builder(); String locale = document.getString("locale"); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index f3501da4ab4..40cd01a6cd0 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -17,6 +17,8 @@ import com.mongodb.client.model.Collation; import com.mongodb.client.model.CollationStrength; +import com.mongodb.client.model.ClusteredIndexOptions; +import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; @@ -27,6 +29,7 @@ import io.micronaut.context.env.Environment; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; @@ -45,6 +48,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.concurrent.TimeUnit; /** * MongoDB's reactive collections creator. @@ -88,8 +92,45 @@ public Set listCollectionNames(MongoDatabase database) { } @Override - public void createCollection(MongoDatabase database, String collection) { - Mono.from(database.createCollection(collection)).block(); + public void createCollection(MongoDatabase database, String collection, @Nullable MongoResolvedCollectionOptions options) { + if (options == null) { + Mono.from(database.createCollection(collection)).block(); + return; + } + CreateCollectionOptions collectionOptions = new CreateCollectionOptions(); + ClusteredIndexOptions clusteredIndexOptions = new ClusteredIndexOptions(new Document("_id", 1), options.clusteredIndexUnique()); + if (options.clusteredIndexName() != null) { + clusteredIndexOptions.name(options.clusteredIndexName()); + } + collectionOptions.clusteredIndexOptions(clusteredIndexOptions); + if (options.expireAfterSeconds() != null) { + collectionOptions.expireAfter(options.expireAfterSeconds().longValue(), TimeUnit.SECONDS); + } + Mono.from(database.createCollection(collection, collectionOptions)).block(); + } + + @Override + public @Nullable MongoResolvedCollectionOptions getCollectionOptions(MongoDatabase database, String collection) { + Document collectionDocument = Flux.from(database.listCollections()) + .filter(document -> collection.equals(document.getString("name"))) + .next() + .block(); + if (collectionDocument == null) { + return null; + } + Document options = collectionDocument.get("options", Document.class); + if (options == null) { + return null; + } + Document clustered = options.get("clusteredIndex", Document.class); + if (clustered == null) { + return null; + } + return new MongoResolvedCollectionOptions( + clustered.getString("name"), + clustered.getBoolean("unique", true), + options.getInteger("expireAfterSeconds") + ); } @Override @@ -119,12 +160,12 @@ public List listIndexes(MongoDatabase database, String colle indexDocument.getBoolean("unique", false), indexDocument.getBoolean("sparse", false), indexDocument.getInteger("expireAfterSeconds"), - indexDocument.get("partialFilterExpression") == null ? null : indexDocument.get("partialFilterExpression").toString(), - indexDocument.get("collation") == null ? null : indexDocument.get("collation").toString(), - null, - null, - null, - indexDocument.get("wildcardProjection") == null ? null : indexDocument.get("wildcardProjection").toString() + normalizeJsonValue(indexDocument.get("partialFilterExpression")), + normalizeJsonValue(indexDocument.get("collation")), + toInteger(indexDocument.get("bits")), + toDouble(indexDocument.get("min")), + toDouble(indexDocument.get("max")), + normalizeJsonValue(indexDocument.get("wildcardProjection")) )); } return resolvedIndexes; @@ -158,13 +199,15 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.max() != null) { indexOptions.max(index.max()); } + if (index.wildcardProjection() != null) { + indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); + } Mono.from(mongoCollection.createIndex(index.keysDocument(), indexOptions)).block(); } }; }, mongoCollectionNameProvider); } - private Collation toCollation(Document document) { Collation.Builder builder = Collation.builder(); String locale = document.getString("locale"); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java index 9b0b6871248..60914b5d5ba 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java @@ -28,6 +28,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.Query; +import io.micronaut.data.annotation.MappedProperty; import io.micronaut.data.document.model.query.builder.MongoQueryBuilder; import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.intercept.annotation.DataMethod; @@ -41,6 +42,8 @@ import io.micronaut.data.model.runtime.StoredQuery; import io.micronaut.data.model.runtime.convert.AttributeConverter; import io.micronaut.data.mongodb.annotation.MongoCollation; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.mongodb.annotation.MongoProjection; import io.micronaut.data.mongodb.annotation.MongoSort; import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions; @@ -394,6 +397,10 @@ public Object convert(@Nullable Object value, @Nullable RuntimePersistentPropert if (converter != null) { return converter.convertToPersistedValue(value, createTypeConversionContext(property, property.getArgument())); } + if (shouldUseImplicitGeoConverter(property)) { + AttributeConverter implicitConverter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(property)); + return implicitConverter.convertToPersistedValue(value, createTypeConversionContext(property, property.getArgument())); + } } return value; } @@ -433,6 +440,18 @@ public void bindMany(QueryParameterBinding binding, Collection values) { return (Map.Entry) holder[0]; } + private boolean shouldUseImplicitGeoConverter(RuntimePersistentProperty property) { + if (!property.getAnnotationMetadata().isAnnotationPresent(MongoGeoIndexed.class)) { + return false; + } + Class converterClass = property.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); + return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(property.getType()); + } + + private Class resolveImplicitGeoConverterClass(RuntimePersistentProperty property) { + return MongoGeoConverters.resolveImplicitGeoConverterClass(property.getType()); + } + private BsonValue replaceQueryParametersInBsonValue(BsonValue value, @Nullable InvocationContext invocationContext, @Nullable E entity) { if (value instanceof BsonDocument bsonDocument) { BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER, null); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java index 9625bacd50c..52d8abbf2a0 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java @@ -27,7 +27,9 @@ import io.micronaut.data.document.serde.OneRelationDeserializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; +import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Decoder; import io.micronaut.serde.Deserializer; import io.micronaut.serde.LimitingStream; @@ -45,6 +47,7 @@ import java.io.IOException; import java.util.Collection; +import java.util.Map; /** * The Micronaut Data's Serde's {@link Deserializer.DecoderContext}. @@ -151,7 +154,7 @@ public Deserializer createSpecific(DecoderContext decoderContext, Argume .orElseThrow(IllegalStateException::new); Argument convertedType = Argument.of(converterPersistedType); AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); - Deserializer deserializer = findDeserializer(convertedType); + Deserializer deserializer = findDeserializer(convertedType).createSpecific(decoderContext, convertedType); return new Deserializer<>() { @Override @Nullable @@ -160,7 +163,7 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume return null; } Object deserialized = deserializer.deserialize(decoder, decoderContext, convertedType); - return converter.convertToEntityValue(deserialized, ConversionContext.of(convertedType)); + return converter.convertToEntityValue(deserialized, ConversionContext.of(type)); } }; } @@ -177,6 +180,22 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume @Override public Deserializer findDeserializer(Argument type) throws SerdeException { + if (shouldUseImplicitGeoConverter(type)) { + Argument mapArgument = Argument.of(Map.class); + Deserializer deserializer = findDeserializer(mapArgument).createSpecific(this, mapArgument); + AttributeConverter converter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(type)); + return (Deserializer) new Deserializer<>() { + @Override + @Nullable + public Object deserialize(Decoder decoder, DecoderContext context, Argument argument) throws IOException { + if (decoder.decodeNull()) { + return null; + } + Object deserialized = ((Deserializer) (Deserializer) deserializer).deserialize(decoder, context, Argument.OBJECT_ARGUMENT); + return converter.convertToEntityValue(deserialized, ConversionContext.of(type)); + } + }; + } Codec codec = codecRegistry.get(type.getType(), codecRegistry); if (codec instanceof MappedCodec mappedCodec) { return mappedCodec.deserializer; @@ -187,6 +206,18 @@ public Deserializer findDeserializer(Argument type return parent.findDeserializer(type); } + private boolean shouldUseImplicitGeoConverter(Argument type) { + if (!type.isAnnotationPresent(MongoGeoIndexed.class)) { + return false; + } + Class converterClass = type.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); + return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(type.getType()); + } + + private Class resolveImplicitGeoConverterClass(Argument type) { + return MongoGeoConverters.resolveImplicitGeoConverterClass(type.getType()); + } + @Override public D findNamingStrategy(Class namingStrategyClass) throws SerdeException { if (namingStrategyClass == IdPropertyNamingStrategy.class) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java index 8309dafe41f..e7ade010a95 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java @@ -26,7 +26,9 @@ import io.micronaut.data.document.serde.IdSerializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; +import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Encoder; import io.micronaut.serde.Serializer; import io.micronaut.serde.bson.custom.CodecBsonDecoder; @@ -39,6 +41,7 @@ import org.bson.types.ObjectId; import java.io.IOException; +import java.util.Map; /** * The Micronaut Data's Serde's {@link Serializer.EncoderContext}. @@ -125,7 +128,7 @@ public Serializer createSpecific(EncoderContext encoderContext, Argument Class converterPersistedType = type.getAnnotationMetadata().classValue(MappedProperty.class, "converterPersistedType") .orElseThrow(IllegalStateException::new); Argument convertedType = Argument.of(converterPersistedType); - Serializer serializer = findSerializer(convertedType); + Serializer serializer = findSerializer(convertedType).createSpecific(encoderContext, convertedType); AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); return new Serializer<>() { @@ -158,6 +161,26 @@ public void serialize(Encoder encoder, EncoderContext context, Argument type, @Override public Serializer findSerializer(Argument type) throws SerdeException { + if (shouldUseImplicitGeoConverter(type)) { + Argument mapArgument = Argument.of(Map.class); + Serializer serializer = findSerializer(mapArgument).createSpecific(this, mapArgument); + AttributeConverter converter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(type)); + return (Serializer) new Serializer<>() { + @Override + public void serialize(Encoder encoder, EncoderContext context, Argument argument, Object value) throws IOException { + if (value == null) { + encoder.encodeNull(); + return; + } + Object converted = converter.convertToPersistedValue(value, ConversionContext.of(type)); + if (converted == null) { + encoder.encodeNull(); + return; + } + serializer.serialize(encoder, context, mapArgument, (Map) converted); + } + }; + } Codec codec = codecRegistry.get(type.getType(), codecRegistry); if (codec instanceof MappedCodec mappedCodec) { return mappedCodec.serializer; @@ -168,6 +191,18 @@ public Serializer findSerializer(Argument type) thro return parent.findSerializer(type); } + private boolean shouldUseImplicitGeoConverter(Argument type) { + if (!type.isAnnotationPresent(MongoGeoIndexed.class)) { + return false; + } + Class converterClass = type.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); + return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(type.getType()); + } + + private Class resolveImplicitGeoConverterClass(Argument type) { + return MongoGeoConverters.resolveImplicitGeoConverterClass(type.getType()); + } + @Override public D findNamingStrategy(Class namingStrategyClass) throws SerdeException { if (namingStrategyClass == IdPropertyNamingStrategy.class) { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy index 9f65bd2533b..4758d60ec77 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy @@ -35,6 +35,7 @@ final class MongoIndexInspector { expireAfterSeconds: indexDocument.getInteger('expireAfterSeconds'), partialFilterExpression: indexDocument.get('partialFilterExpression'), collation : indexDocument.get('collation'), + wildcardProjection : indexDocument.get('wildcardProjection'), min : indexDocument.get('min'), max : indexDocument.get('max'), ] diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy new file mode 100644 index 00000000000..52cc18431d0 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.clustered + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoClusteredCollectionCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.clustered'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates clustered collection with configured name'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def collection = mongoClient.getDatabase('test').listCollections().into([]).find { it.getString('name') == 'clustered_indexed_entities' } + assert collection != null + def options = collection.get('options', Document) + assert options != null + def clustered = options.get('clusteredIndex', Document) + assert clustered != null + assert clustered.get('key', Document).getInteger('_id') == 1 + assert clustered.getBoolean('unique') + assert clustered.getString('name') == 'clustered_idx' + } + } +} + +@MongoRepository +interface ClusteredIndexedEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'clustered_idx') +@MappedEntity('clustered_indexed_entities') +class ClusteredIndexedEntity { + @Id + java.time.Instant id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy index 0cf7949225c..50da0568a6b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy @@ -29,6 +29,10 @@ class MongoCollationIndexCreationSpec extends Specification implements MongoTest ['io.micronaut.data.document.mongodb.collation'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -42,7 +46,7 @@ class MongoCollationIndexCreationSpec extends Specification implements MongoTest def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'collation_indexed_entities') assert indexes*.name.contains('collation_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy index 575b1de779e..3aac17a9af7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy @@ -25,6 +25,10 @@ class MongoCompoundIndexCreationSpec extends Specification implements MongoTestP List getPackageNames() { ['io.micronaut.data.document.mongodb.compound'] } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } @AutoCleanup @Shared ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ @@ -45,7 +49,7 @@ class MongoCompoundIndexCreationSpec extends Specification implements MongoTestP def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_indexed_entities') assert indexes*.name.contains('name_age_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy index 2d12ddcc0ac..704da6c30f7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoGeoIndexCreationSpec extends Specification implements MongoTestProper ['io.micronaut.data.document.mongodb.geo'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoGeoIndexCreationSpec extends Specification implements MongoTestProper def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_indexed_entities') assert indexes*.name.contains('geo_location_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy index f8e6863c8dd..8e8c2428095 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy @@ -29,6 +29,10 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp ['io.micronaut.data.document.mongodb.geo2d'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -42,7 +46,7 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_indexed_entities') assert indexes*.name.contains('geo2d_location_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy index 6247e3c4a04..12b316bc09d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy @@ -29,6 +29,10 @@ class MongoGeo2dOptionsIndexCreationSpec extends Specification implements MongoT ['io.micronaut.data.document.mongodb.geo2d.options'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -42,7 +46,7 @@ class MongoGeo2dOptionsIndexCreationSpec extends Specification implements MongoT def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_options_indexed_entities') assert indexes*.name.contains('geo2d_options_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy index 0b609244dd9..0f49300c1bf 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy @@ -31,6 +31,10 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe ['io.micronaut.data.document.mongodb.geocompound'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -44,7 +48,7 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_compound_indexed_entities') assert indexes*.name.contains('geo_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy index 009372c5e9f..7137bc8ed78 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -33,6 +33,10 @@ class MongoGeoPointValueIndexCreationSpec extends Specification implements Mongo ['io.micronaut.data.document.mongodb.geovalue'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -46,7 +50,7 @@ class MongoGeoPointValueIndexCreationSpec extends Specification implements Mongo def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_point_value_indexed_entities') assert indexes*.name.contains('geo_point_location_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy index f0107d9850b..cc526890d2a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy @@ -13,7 +13,6 @@ import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoPointConverter -import io.micronaut.data.mongodb.geo.MongoGeoPointLike import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -28,17 +27,25 @@ class MongoCustomGeoPointValueIndexCreationSpec extends Specification implements @Shared MongoClient mongoClient + @Shared + CustomGeoPointValueIndexedEntityRepository repository + @Override List getPackageNames() { ['io.micronaut.data.document.mongodb.geovalue.custom'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', 'micronaut.data.mongodb.create-indexes' : 'true' ]) mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(CustomGeoPointValueIndexedEntityRepository) } void 'creates geospatial index on a custom point-like modeled value'() { @@ -46,7 +53,7 @@ class MongoCustomGeoPointValueIndexCreationSpec extends Specification implements def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'custom_geo_point_value_indexed_entities') assert indexes*.name.contains('custom_geo_point_location_idx') @@ -56,6 +63,16 @@ class MongoCustomGeoPointValueIndexCreationSpec extends Specification implements assert index.fields[0].kind() == '2dsphere' } } + + void 'persists and reads arbitrary custom geospatial modeled value'() { + when: + def saved = repository.save(new CustomGeoPointValueIndexedEntity(location: new CustomGeoPoint(longitude: 12.5d, latitude: 45.8d))) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.location.longitude == 12.5d + loaded.location.latitude == 45.8d + } } @MongoRepository @@ -75,17 +92,7 @@ class CustomGeoPointValueIndexedEntity { } @MappedEntity -class CustomGeoPoint implements MongoGeoPointLike { - double x - double y - - @Override - double x() { - return x - } - - @Override - double y() { - return y - } +class CustomGeoPoint { + double longitude + double latitude } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..b0872f3086f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -0,0 +1,108 @@ +package io.micronaut.data.document.mongodb.geovalue.geometrycollection + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoGeometryCollection +import io.micronaut.data.mongodb.geo.MongoGeoLineString +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoGeometryCollectionValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoGeometryCollectionValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.geometrycollection'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoGeometryCollectionValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoGeometryCollection modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_geometry_collection_value_indexed_entities') + assert indexes*.name.contains('geo_geometry_collection_location_idx') + def index = indexes.find { it.name == 'geo_geometry_collection_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'geometry' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoGeometryCollection modeled value'() { + given: + def geometryCollection = new MongoGeoGeometryCollection([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]), + new MongoGeoLineString([ + new MongoGeoPoint(-74.01d, 40.72d), + new MongoGeoPoint(-74.00d, 40.71d) + ]) + ]) + + when: + def saved = repository.save(new GeoGeometryCollectionValueIndexedEntity(geometry: geometryCollection)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.geometry.geometries().size() == 3 + loaded.geometry.geometries()[0] instanceof MongoGeoPoint + loaded.geometry.geometries()[1] instanceof MongoGeoMultiPoint + loaded.geometry.geometries()[2] instanceof MongoGeoLineString + } +} + +@MongoRepository +interface GeoGeometryCollectionValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_geometry_collection_value_indexed_entities') +class GeoGeometryCollectionValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_geometry_collection_location_idx') + MongoGeoGeometryCollection geometry +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..1a01b25f3a3 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,95 @@ +package io.micronaut.data.document.mongodb.geovalue.implicit + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoImplicitCustomGeoPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + ImplicitCustomGeoPointValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.implicit'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(ImplicitCustomGeoPointValueIndexedEntityRepository) + } + + void 'creates geospatial index on custom modeled value without explicit converter'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'implicit_custom_geo_point_value_indexed_entities') + assert indexes*.name.contains('implicit_custom_geo_point_location_idx') + def index = indexes.find { it.name == 'implicit_custom_geo_point_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads custom geospatial modeled value without explicit converter'() { + when: + def saved = repository.save(new ImplicitCustomGeoPointValueIndexedEntity(location: new ImplicitCustomGeoPoint(longitude: 12.5d, latitude: 45.8d))) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.location.longitude == 12.5d + loaded.location.latitude == 45.8d + } +} + +@MongoRepository +interface ImplicitCustomGeoPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('implicit_custom_geo_point_value_indexed_entities') +class ImplicitCustomGeoPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'implicit_custom_geo_point_location_idx') + ImplicitCustomGeoPoint location +} + +@MappedEntity +class ImplicitCustomGeoPoint { + double longitude + double latitude +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..5548c20dd0e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy @@ -0,0 +1,101 @@ +package io.micronaut.data.document.mongodb.geovalue.linestring + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoLineString +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoLineStringValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoLineStringValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.linestring'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoLineStringValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoLineString modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_linestring_value_indexed_entities') + assert indexes*.name.contains('geo_linestring_location_idx') + def index = indexes.find { it.name == 'geo_linestring_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'route' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoLineString modeled value'() { + given: + def lineString = new MongoGeoLineString([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + + when: + def saved = repository.save(new GeoLineStringValueIndexedEntity(route: lineString)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.route.coordinates().size() == 3 + loaded.route.coordinates()[0].x() == -73.99d + loaded.route.coordinates()[0].y() == 40.75d + loaded.route.coordinates()[2].x() == -73.97d + loaded.route.coordinates()[2].y() == 40.73d + } +} + +@MongoRepository +interface GeoLineStringValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_linestring_value_indexed_entities') +class GeoLineStringValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_linestring_location_idx') + MongoGeoLineString route +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..ca4430cbdb3 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy @@ -0,0 +1,109 @@ +package io.micronaut.data.document.mongodb.geovalue.multilinestring + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoMultiLineString +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoMultiLineStringValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoMultiLineStringValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.multilinestring'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoMultiLineStringValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoMultiLineString modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_multilinestring_value_indexed_entities') + assert indexes*.name.contains('geo_multilinestring_location_idx') + def index = indexes.find { it.name == 'geo_multilinestring_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'paths' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoMultiLineString modeled value'() { + given: + def multiLineString = new MongoGeoMultiLineString([ + [ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ], + [ + new MongoGeoPoint(-74.01d, 40.73d), + new MongoGeoPoint(-74.00d, 40.72d), + new MongoGeoPoint(-73.99d, 40.71d) + ] + ]) + + when: + def saved = repository.save(new GeoMultiLineStringValueIndexedEntity(paths: multiLineString)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.paths.coordinates().size() == 2 + loaded.paths.coordinates()[0].size() == 3 + loaded.paths.coordinates()[0][0].x() == -73.99d + loaded.paths.coordinates()[0][0].y() == 40.75d + loaded.paths.coordinates()[1][0].x() == -74.01d + loaded.paths.coordinates()[1][0].y() == 40.73d + } +} + +@MongoRepository +interface GeoMultiLineStringValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_multilinestring_value_indexed_entities') +class GeoMultiLineStringValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_multilinestring_location_idx') + MongoGeoMultiLineString paths +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..b5987888d54 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy @@ -0,0 +1,101 @@ +package io.micronaut.data.document.mongodb.geovalue.multipoint + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoMultiPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoMultiPointValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.multipoint'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoMultiPointValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoMultiPoint modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_multipoint_value_indexed_entities') + assert indexes*.name.contains('geo_multipoint_location_idx') + def index = indexes.find { it.name == 'geo_multipoint_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'locations' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoMultiPoint modeled value'() { + given: + def multiPoint = new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + + when: + def saved = repository.save(new GeoMultiPointValueIndexedEntity(locations: multiPoint)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.locations.coordinates().size() == 3 + loaded.locations.coordinates()[0].x() == -73.99d + loaded.locations.coordinates()[0].y() == 40.75d + loaded.locations.coordinates()[2].x() == -73.97d + loaded.locations.coordinates()[2].y() == 40.73d + } +} + +@MongoRepository +interface GeoMultiPointValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_multipoint_value_indexed_entities') +class GeoMultiPointValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_multipoint_location_idx') + MongoGeoMultiPoint locations +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..8481b7ded6d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy @@ -0,0 +1,118 @@ +package io.micronaut.data.document.mongodb.geovalue.multipolygon + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoMultiPolygon +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoMultiPolygonValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoMultiPolygonValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.multipolygon'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoMultiPolygonValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoMultiPolygon modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_multipolygon_value_indexed_entities') + assert indexes*.name.contains('geo_multipolygon_location_idx') + def index = indexes.find { it.name == 'geo_multipolygon_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'areas' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoMultiPolygon modeled value'() { + given: + def multiPolygon = new MongoGeoMultiPolygon([ + [ + [ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.99d, 40.74d), + new MongoGeoPoint(-73.99d, 40.75d) + ] + ], + [ + [ + new MongoGeoPoint(-74.01d, 40.73d), + new MongoGeoPoint(-74.00d, 40.73d), + new MongoGeoPoint(-74.00d, 40.72d), + new MongoGeoPoint(-74.01d, 40.72d), + new MongoGeoPoint(-74.01d, 40.73d) + ] + ] + ]) + + when: + def saved = repository.save(new GeoMultiPolygonValueIndexedEntity(areas: multiPolygon)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.areas.coordinates().size() == 2 + loaded.areas.coordinates()[0].size() == 1 + loaded.areas.coordinates()[0][0].size() == 5 + loaded.areas.coordinates()[0][0][0].x() == -73.99d + loaded.areas.coordinates()[0][0][0].y() == 40.75d + loaded.areas.coordinates()[1][0][0].x() == -74.01d + loaded.areas.coordinates()[1][0][0].y() == 40.73d + } +} + +@MongoRepository +interface GeoMultiPolygonValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_multipolygon_value_indexed_entities') +class GeoMultiPolygonValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_multipolygon_location_idx') + MongoGeoMultiPolygon areas +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..4a1d2a25425 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy @@ -0,0 +1,104 @@ +package io.micronaut.data.document.mongodb.geovalue.polygon + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.TypeDef +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.DataType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.mongodb.geo.MongoGeoPolygon +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoGeoPolygonValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Shared + GeoPolygonValueIndexedEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.polygon'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(GeoPolygonValueIndexedEntityRepository) + } + + void 'creates geospatial index on a MongoGeoPolygon modeled value'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo_polygon_value_indexed_entities') + assert indexes*.name.contains('geo_polygon_location_idx') + def index = indexes.find { it.name == 'geo_polygon_location_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'area' + assert index.fields[0].kind() == '2dsphere' + } + } + + void 'persists and reads MongoGeoPolygon modeled value'() { + given: + def polygon = new MongoGeoPolygon([ + [ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.99d, 40.74d), + new MongoGeoPoint(-73.99d, 40.75d) + ] + ]) + + when: + def saved = repository.save(new GeoPolygonValueIndexedEntity(area: polygon)) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.area.coordinates().size() == 1 + loaded.area.coordinates()[0].size() == 5 + loaded.area.coordinates()[0][0].x() == -73.99d + loaded.area.coordinates()[0][0].y() == 40.75d + } +} + +@MongoRepository +interface GeoPolygonValueIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('geo_polygon_value_indexed_entities') +class GeoPolygonValueIndexedEntity { + @Id + @GeneratedValue + String id + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = 'geo_polygon_location_idx') + MongoGeoPolygon area +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy index 6b730b82b76..b07cd08e401 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoHashedIndexCreationSpec extends Specification implements MongoTestPro ['io.micronaut.data.document.mongodb.hashed'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoHashedIndexCreationSpec extends Specification implements MongoTestPro def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'hashed_indexed_entities') assert indexes*.name.contains('hashed_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy index 05fb014ed8b..1c1198dba51 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoPartialFilterIndexCreationSpec extends Specification implements Mongo ['io.micronaut.data.document.mongodb.partialfilter'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoPartialFilterIndexCreationSpec extends Specification implements Mongo def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'partial_filter_indexed_entities') assert indexes*.name.contains('partial_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy index b69f6fa1834..04307cbf3a9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy @@ -1,81 +1,18 @@ package io.micronaut.data.document.mongodb.reactive -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.compound.MongoCompoundIndexCreationSpec -class MongoReactiveCompoundIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveCompoundIndexCreationSpec extends MongoCompoundIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates declared compound indexes for existing collections with reactive driver selected'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_indexed_entities') - assert indexes*.name.contains('reactive_name_age_idx') - def index = indexes.find { it.name == 'reactive_name_age_idx' } - assert index.unique - assert index.fields.size() == 2 - assert index.fields[0].path() == 'name' - assert index.fields[0].order() == 1 - assert index.fields[1].path() == 'age' - assert index.fields[1].order() == -1 - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveCompoundIndexedEntityRepository extends CrudRepository { -} - -@MongoCompoundIndex( - name = 'reactive_name_age_idx', - unique = true, - fields = [ - @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), - @MongoCompoundIndexField(value = 'age', direction = MongoIndexDirection.DESC) - ] -) -@MappedEntity('reactive_compound_indexed_entities') -class ReactiveCompoundIndexedEntity { - @Id - @GeneratedValue - String id - - String name - - Integer age -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy index 34677b53a80..7af93dac281 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy @@ -1,67 +1,18 @@ package io.micronaut.data.document.mongodb.reactive -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.mongodb.annotation.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.simple.MongoIndexCreationSpec -class MongoReactiveIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { +class MongoReactiveIndexCreationSpec extends MongoIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive'] + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - @AutoCleanup - @Shared - ApplicationContext applicationContext - @Shared - MongoClient mongoClient - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) - } - - void 'creates declared indexes for existing collections with reactive driver selected'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_indexed_entities') - assert indexes*.name.contains('reactive_name_idx') - def index = indexes.find { it.name == 'reactive_name_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'name' - assert index.fields[0].order() == 1 - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_indexed_entities') -class ReactiveIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoIndexed(name = 'reactive_name_idx') - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy new file mode 100644 index 00000000000..f4bf2a77ca5 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy @@ -0,0 +1,69 @@ +package io.micronaut.data.document.mongodb.reactive.clustered + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveClusteredCollectionCreationSpec extends Specification implements MongoSelectReactiveDriver { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.clustered'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates clustered collection with configured name in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def collection = mongoClient.getDatabase('test').listCollections().into([]).find { it.getString('name') == 'reactive_clustered_indexed_entities' } + assert collection != null + def options = collection.get('options', Document) + assert options != null + def clustered = options.get('clusteredIndex', Document) + assert clustered != null + assert clustered.get('key', Document).getInteger('_id') == 1 + assert clustered.getBoolean('unique') + assert clustered.getString('name') == 'reactive_clustered_idx' + } + } +} + +@MongoRepository +interface ReactiveClusteredIndexedEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'reactive_clustered_idx') +@MappedEntity('reactive_clustered_indexed_entities') +class ReactiveClusteredIndexedEntity { + @Id + java.time.Instant id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy index 1220f5c8bda..4a495ce8ff9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy @@ -1,71 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.collation -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import org.bson.Document -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.collation.MongoCollationIndexCreationSpec -class MongoReactiveCollationIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveCollationIndexCreationSpec extends MongoCollationIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.collation'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field index with collation in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_collation_indexed_entities') - assert indexes*.name.contains('reactive_collation_name_idx') - def index = indexes.find { it.name == 'reactive_collation_name_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'name' - assert index.fields[0].order() == 1 - assert index.collation != null - assert ((Document) index.collation).getString('locale') == 'en' - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveCollationIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_collation_indexed_entities') -class ReactiveCollationIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoIndexed(name = 'reactive_collation_name_idx', collation = '{ "locale": "en", "strength": 2 }') - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy index 97b90246fd9..1fe1045924a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy @@ -1,69 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geo -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geo.MongoGeoIndexCreationSpec -class MongoReactiveGeoIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveGeoIndexCreationSpec extends MongoGeoIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geo'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field 2dsphere index in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_indexed_entities') - assert indexes*.name.contains('reactive_geo_location_idx') - def index = indexes.find { it.name == 'reactive_geo_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].order() == null - assert index.fields[0].kind() == '2dsphere' - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveGeoIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_geo_indexed_entities') -class ReactiveGeoIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoGeoIndexed(name = 'reactive_geo_location_idx') - Map location -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy index 9bbed21642f..dbfda5cd5e3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy @@ -1,70 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geo2d -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geo2d.MongoGeo2dIndexCreationSpec -class MongoReactiveGeo2dIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveGeo2dIndexCreationSpec extends MongoGeo2dIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geo2d'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field 2d index in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo2d_indexed_entities') - assert indexes*.name.contains('reactive_geo2d_location_idx') - def index = indexes.find { it.name == 'reactive_geo2d_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].order() == null - assert index.fields[0].kind() == '2d' - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveGeo2dIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_geo2d_indexed_entities') -class ReactiveGeo2dIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoGeoIndexed(name = 'reactive_geo2d_location_idx', type = MongoGeoIndexType.GEO_2D) - Map location -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy index 62cdce1547d..9660aae53eb 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy @@ -1,71 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geo2d.options -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geo2d.options.MongoGeo2dOptionsIndexCreationSpec -class MongoReactiveGeo2dOptionsIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveGeo2dOptionsIndexCreationSpec extends MongoGeo2dOptionsIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geo2d.options'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates 2d index with bits min and max in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo2d_options_indexed_entities') - assert indexes*.name.contains('reactive_geo2d_options_idx') - def index = indexes.find { it.name == 'reactive_geo2d_options_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2d' - assert index.min == -180 - assert index.max == 180 - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveGeo2dOptionsIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_geo2d_options_indexed_entities') -class ReactiveGeo2dOptionsIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoGeoIndexed(name = 'reactive_geo2d_options_idx', type = MongoGeoIndexType.GEO_2D, bits = 26, min = -180, max = 180) - Map location -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy index 4949f607373..04644ed8193 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy @@ -1,81 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geocompound -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geocompound.MongoCompoundGeoIndexCreationSpec -class MongoReactiveCompoundGeoIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveCompoundGeoIndexCreationSpec extends MongoCompoundGeoIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geocompound'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates compound index with geospatial field in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_compound_indexed_entities') - assert indexes*.name.contains('reactive_geo_name_idx') - def index = indexes.find { it.name == 'reactive_geo_name_idx' } - assert index.fields.size() == 2 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2dsphere' - assert index.fields[1].path() == 'name' - assert index.fields[1].order() == 1 - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveGeoCompoundIndexedEntityRepository extends CrudRepository { -} - -@MongoCompoundIndex( - name = 'reactive_geo_name_idx', - fields = [ - @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE), - @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) - ] -) -@MappedEntity('reactive_geo_compound_indexed_entities') -class ReactiveGeoCompoundIndexedEntity { - @Id - @GeneratedValue - String id - - Map location - - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy index 130f1f135b6..5886f04dbac 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy @@ -1,75 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geovalue -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.MappedProperty -import io.micronaut.data.annotation.TypeDef -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.mongodb.geo.MongoGeoPointConverter -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geovalue.MongoGeoPointValueIndexCreationSpec -class MongoReactiveGeoPointValueIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveGeoPointValueIndexCreationSpec extends MongoGeoPointValueIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geovalue'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates geospatial index on a MongoGeoPoint modeled value in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_geo_point_value_indexed_entities') - assert indexes*.name.contains('reactive_geo_point_location_idx') - def index = indexes.find { it.name == 'reactive_geo_point_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2dsphere' - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveGeoPointValueIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_geo_point_value_indexed_entities') -class ReactiveGeoPointValueIndexedEntity { - @Id - @GeneratedValue - String id - - @TypeDef(type = DataType.OBJECT) - @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) - @MongoGeoIndexed(name = 'reactive_geo_point_location_idx') - MongoGeoPoint location -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy index b949a506f6b..e94d9cb5baf 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy @@ -1,91 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.geovalue.custom -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.MappedProperty -import io.micronaut.data.annotation.TypeDef -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPointConverter -import io.micronaut.data.mongodb.geo.MongoGeoPointLike -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.geovalue.custom.MongoCustomGeoPointValueIndexCreationSpec -class MongoReactiveCustomGeoPointValueIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.geovalue.custom'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) - } - - void 'creates geospatial index on a custom point-like modeled value in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_custom_geo_point_value_indexed_entities') - assert indexes*.name.contains('reactive_custom_geo_point_location_idx') - def index = indexes.find { it.name == 'reactive_custom_geo_point_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2dsphere' - } - } -} - -@MongoRepository -interface ReactiveCustomGeoPointValueIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_custom_geo_point_value_indexed_entities') -class ReactiveCustomGeoPointValueIndexedEntity { - @Id - @GeneratedValue - String id - - @TypeDef(type = DataType.OBJECT) - @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) - @MongoGeoIndexed(name = 'reactive_custom_geo_point_location_idx') - ReactiveCustomGeoPoint location -} - -@MappedEntity -class ReactiveCustomGeoPoint implements MongoGeoPointLike { - double x - double y +class MongoReactiveCustomGeoPointValueIndexCreationSpec extends MongoCustomGeoPointValueIndexCreationSpec { @Override - double x() { - return x + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } @Override - double y() { - return y + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..141a1666c50 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.geometrycollection + +import io.micronaut.data.document.mongodb.geovalue.geometrycollection.MongoGeoGeometryCollectionValueIndexCreationSpec + +class MongoReactiveGeoGeometryCollectionValueIndexCreationSpec extends MongoGeoGeometryCollectionValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..8df57694c1f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.implicit + +import io.micronaut.data.document.mongodb.geovalue.implicit.MongoImplicitCustomGeoPointValueIndexCreationSpec + +class MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec extends MongoImplicitCustomGeoPointValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..f5944ec11a5 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.linestring + +import io.micronaut.data.document.mongodb.geovalue.linestring.MongoGeoLineStringValueIndexCreationSpec + +class MongoReactiveGeoLineStringValueIndexCreationSpec extends MongoGeoLineStringValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..58e0256af62 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.multilinestring + +import io.micronaut.data.document.mongodb.geovalue.multilinestring.MongoGeoMultiLineStringValueIndexCreationSpec + +class MongoReactiveGeoMultiLineStringValueIndexCreationSpec extends MongoGeoMultiLineStringValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..cabd28705b7 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.multipoint + +import io.micronaut.data.document.mongodb.geovalue.multipoint.MongoGeoMultiPointValueIndexCreationSpec + +class MongoReactiveGeoMultiPointValueIndexCreationSpec extends MongoGeoMultiPointValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..5c38ba5f9bd --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.multipolygon + +import io.micronaut.data.document.mongodb.geovalue.multipolygon.MongoGeoMultiPolygonValueIndexCreationSpec + +class MongoReactiveGeoMultiPolygonValueIndexCreationSpec extends MongoGeoMultiPolygonValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy new file mode 100644 index 00000000000..b6005c6c0ed --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geovalue.polygon + +import io.micronaut.data.document.mongodb.geovalue.polygon.MongoGeoPolygonValueIndexCreationSpec + +class MongoReactiveGeoPolygonValueIndexCreationSpec extends MongoGeoPolygonValueIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy index 8bdad5aba1c..8327dde2c78 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy @@ -1,69 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.hashed -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoHashedIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.hashed.MongoHashedIndexCreationSpec -class MongoReactiveHashedIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveHashedIndexCreationSpec extends MongoHashedIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.hashed'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field hashed index in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_hashed_indexed_entities') - assert indexes*.name.contains('reactive_hashed_name_idx') - def index = indexes.find { it.name == 'reactive_hashed_name_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'name' - assert index.fields[0].order() == null - assert index.fields[0].kind() == 'hashed' - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveHashedIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_hashed_indexed_entities') -class ReactiveHashedIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoHashedIndexed(name = 'reactive_hashed_name_idx') - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy index ef6007dcf10..d94d050e110 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy @@ -1,73 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.partialfilter -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import org.bson.Document -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.partialfilter.MongoPartialFilterIndexCreationSpec -class MongoReactivePartialFilterIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactivePartialFilterIndexCreationSpec extends MongoPartialFilterIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.partialfilter'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field index with partial filter expression in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_partial_filter_indexed_entities') - assert indexes*.name.contains('reactive_partial_name_idx') - def index = indexes.find { it.name == 'reactive_partial_name_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'name' - assert index.fields[0].order() == 1 - assert index.partialFilterExpression != null - assert ((Document) index.partialFilterExpression).getBoolean('active') - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactivePartialFilterIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_partial_filter_indexed_entities') -class ReactivePartialFilterIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoIndexed(name = 'reactive_partial_name_idx', partialFilterExpression = '{ "active": true }') - String name - - Boolean active -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy index 9425bbe55cc..f90f02d1e60 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy @@ -1,68 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.text -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.text.MongoTextIndexCreationSpec -class MongoReactiveTextIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveTextIndexCreationSpec extends MongoTextIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.text'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field text index in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_text_indexed_entities') - assert indexes*.name.contains('reactive_text_name_idx') - def index = indexes.find { it.name == 'reactive_text_name_idx' } - assert index.fields.size() == 2 - assert index.fields*.path().contains('_fts') - assert index.fields*.path().contains('_ftsx') - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveTextIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_text_indexed_entities') -class ReactiveTextIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoTextIndexed(name = 'reactive_text_name_idx') - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy new file mode 100644 index 00000000000..befb1fd6806 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.ttl + +import io.micronaut.data.document.mongodb.ttl.MongoTtlIndexCreationSpec + +class MongoReactiveTtlIndexCreationSpec extends MongoTtlIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy index 95e92a74927..55e9065e6f4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy @@ -1,68 +1,18 @@ package io.micronaut.data.document.mongodb.reactive.wildcard -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions +import io.micronaut.data.document.mongodb.wildcard.MongoWildcardIndexCreationSpec -class MongoReactiveWildcardIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient +class MongoReactiveWildcardIndexCreationSpec extends MongoWildcardIndexCreationSpec { @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.wildcard'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] } - void 'creates field wildcard index in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_wildcard_indexed_entities') - assert indexes*.name.contains('reactive_wildcard_metadata_idx') - def index = indexes.find { it.name == 'reactive_wildcard_metadata_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'metadata.$**' - assert index.fields[0].order() == 1 - } + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator } } - -@MongoRepository -interface ReactiveWildcardIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('reactive_wildcard_indexed_entities') -class ReactiveWildcardIndexedEntity { - @Id - @GeneratedValue - String id - - @MongoWildcardIndexed(name = 'reactive_wildcard_metadata_idx') - Map metadata -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy index 18d29502fd0..3181d53378d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy @@ -21,6 +21,10 @@ class MongoIndexCreationSpec extends Specification implements MongoTestPropertyP List getPackageNames() { ['io.micronaut.data.document.mongodb.simple'] } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } @AutoCleanup @Shared ApplicationContext applicationContext @@ -43,7 +47,7 @@ class MongoIndexCreationSpec extends Specification implements MongoTestPropertyP def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'indexed_entities') assert indexes*.name.contains('name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy index 64025a83546..425a41f17cc 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoTextIndexCreationSpec extends Specification implements MongoTestPrope ['io.micronaut.data.document.mongodb.text'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoTextIndexCreationSpec extends Specification implements MongoTestPrope def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'text_indexed_entities') assert indexes*.name.contains('text_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy new file mode 100644 index 00000000000..b5ea24dd7af --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.ttl + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoTtlIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.ttl'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates field TTL index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'ttl_indexed_entities') + assert indexes*.name.contains('expires_at_ttl_idx') + def index = indexes.find { it.name == 'expires_at_ttl_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'expires_at' + assert index.fields[0].order() == 1 + assert index.expireAfterSeconds == 60 + } + } +} + +@MongoRepository +interface TtlIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('ttl_indexed_entities') +class TtlIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'expires_at_ttl_idx', expireAfterSeconds = 60) + java.time.Instant expiresAt +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy new file mode 100644 index 00000000000..5aec931df91 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy @@ -0,0 +1,43 @@ +package isolated.mongodb.validation.clusteredttl + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoClusteredTtlValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['isolated.mongodb.validation.clusteredttl'] + } + + void 'fails fast when clustered TTL uses non date id type'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('requires a date/time identity type') + } +} + +@MongoRepository +interface InvalidClusteredTtlEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'invalid_clustered_ttl_idx', expireAfterSeconds = 300) +@MappedEntity('invalid_clustered_ttl_entities') +class InvalidClusteredTtlEntity { + @Id + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy new file mode 100644 index 00000000000..e85da25855b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy @@ -0,0 +1,43 @@ +package isolated.mongodb.validation.clusteredunique + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoClusteredUniqueValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['isolated.mongodb.validation.clusteredunique'] + } + + void 'fails fast when clustered unique is false'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('must be unique=true') + } +} + +@MongoRepository +interface InvalidClusteredUniqueEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'invalid_clustered_unique_idx', unique = false) +@MappedEntity('invalid_clustered_unique_entities') +class InvalidClusteredUniqueEntity { + @Id + java.time.Instant id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy index 78283b2bd24..a3a9779aca5 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -8,6 +8,7 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.Specification @@ -42,5 +43,5 @@ class InvalidGeoIndexedEntity { String id @MongoGeoIndexed(name = 'invalid_geo_idx', type = MongoGeoIndexType.GEO_2DSPHERE, bits = 26) - Map location + MongoGeoPoint location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy new file mode 100644 index 00000000000..8ee6c967ef3 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.data.document.mongodb.validation.geotype + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoGeoTypeValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geotype'] + } + + void 'fails fast when MongoGeoIndexed uses unsupported property type'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('requires a supported type') + } +} + +@MongoRepository +interface InvalidGeoTypeEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_geo_type_entities') +class InvalidGeoTypeEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'invalid_geo_type_idx') + String location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy index 864f93debcf..59f2af0d83c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy @@ -16,7 +16,7 @@ class MongoCompoundIndexValidationSpec extends Specification implements MongoTes @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb'] + ['io.micronaut.data.document.mongodb.validation.invalidpath'] } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy new file mode 100644 index 00000000000..396201eaf97 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.data.document.mongodb.validation.reactivewildcardprojection + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoReactiveWildcardProjectionValidationSpec extends Specification implements MongoSelectReactiveDriver { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.reactivewildcardprojection'] + } + + void 'fails fast for field-level wildcard projection in reactive mode'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('field-level @MongoWildcardIndexed is not supported by MongoDB') + } +} + +@MongoRepository +interface InvalidReactiveWildcardProjectionEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_reactive_wildcard_projection_entities') +class InvalidReactiveWildcardProjectionEntity { + @Id + @GeneratedValue + String id + + @MongoWildcardIndexed(name = 'invalid_reactive_wildcard_projection_idx', wildcardProjection = '{ "metadata.secret": 0 }') + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy new file mode 100644 index 00000000000..a372a9c5ad4 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.data.document.mongodb.validation.wildcardprojection + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoWildcardProjectionValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.wildcardprojection'] + } + + void 'fails fast for field-level wildcard projection'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('field-level @MongoWildcardIndexed is not supported by MongoDB') + } +} + +@MongoRepository +interface InvalidWildcardProjectionEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_wildcard_projection_entities') +class InvalidWildcardProjectionEntity { + @Id + @GeneratedValue + String id + + @MongoWildcardIndexed(name = 'invalid_wildcard_projection_idx', wildcardProjection = '{ "metadata.secret": 0 }') + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy index 5c511323115..412dbd7474d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoWildcardIndexCreationSpec extends Specification implements MongoTestP ['io.micronaut.data.document.mongodb.wildcard'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoWildcardIndexCreationSpec extends Specification implements MongoTestP def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'wildcard_indexed_entities') assert indexes*.name.contains('wildcard_metadata_idx') diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc new file mode 100644 index 00000000000..1543f34fdd8 --- /dev/null +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -0,0 +1,153 @@ +Micronaut Data MongoDB supports declaration-based index creation for mapped entities. + +To enable startup index creation, set `micronaut.data.mongodb.create-indexes=true`. + +NOTE: Index creation is opt-in and runs during startup collection initialization. + +== Supported index annotations + +Micronaut Data currently supports the following MongoDB index annotations: + +* ann:data.mongodb.annotation.MongoIndexed[] (single-field indexes) +* ann:data.mongodb.annotation.MongoCompoundIndex[] / ann:data.mongodb.annotation.MongoCompoundIndexes[] (compound indexes) +* ann:data.mongodb.annotation.MongoTextIndexed[] (text indexes, including multi-field aggregation) +* ann:data.mongodb.annotation.MongoHashedIndexed[] (hashed indexes) +* ann:data.mongodb.annotation.MongoGeoIndexed[] (geospatial indexes: `2d`, `2dsphere`) +* ann:data.mongodb.annotation.MongoWildcardIndexed[] (field-level wildcard indexes) +* ann:data.mongodb.annotation.MongoWildcardIndex[] (top-level wildcard indexes) +* ann:data.mongodb.annotation.MongoClusteredIndex[] (clustered collection options) + +== Single-field indexes + +Use ann:data.mongodb.annotation.MongoIndexed[] to declare standard single-field indexes. + +Supported options include direction, `unique`, `sparse`, `expireAfterSeconds` (TTL), `partialFilterExpression`, and `collation`. + +[source,java] +---- +@MappedEntity("products") +class ProductEntity { + @Id + @GeneratedValue + String id; + + @MongoIndexed(name = "product_sku_idx", unique = true) + String sku; +} +---- + +== Compound indexes + +Use ann:data.mongodb.annotation.MongoCompoundIndex[] for multi-field index declarations at entity level. + +[source,java] +---- +@MongoCompoundIndex( + name = "order_customer_created_idx", + fields = { + @MongoCompoundIndexField(value = "customerId", direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = "createdAt", direction = MongoIndexDirection.DESC) + } +) +@MappedEntity("orders") +class OrderEntity { + @Id + @GeneratedValue + String id; + + String customerId; + Instant createdAt; +} +---- + +NOTE: TTL on compound indexes is rejected during startup validation. + +== Text indexes + +Use ann:data.mongodb.annotation.MongoTextIndexed[] on text fields. + +Multiple text-indexed fields on the same entity are aggregated into the corresponding MongoDB text index definition. + +== Hashed indexes + +Use ann:data.mongodb.annotation.MongoHashedIndexed[] to create hashed indexes. + +== Geospatial indexes + +Use ann:data.mongodb.annotation.MongoGeoIndexed[] for geospatial indexing (`2d` and `2dsphere`). + +Modeled geospatial values are supported for `MongoGeoPoint`, `MongoGeoMultiPoint`, `MongoGeoLineString`, `MongoGeoMultiLineString`, `MongoGeoPolygon`, `MongoGeoMultiPolygon`, `MongoGeoGeometryCollection`, and custom point-like modeled types. + +For properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[], Micronaut Data now applies `MongoGeoPointConverter` implicitly when no explicit `@MappedProperty(converter=...)` is declared. +Point-like modeled types can map coordinates through numeric properties such as `x/y`, `longitude/latitude`, or `lng|lon/lat`. + +For polygon modeled values, Micronaut Data applies `MongoGeoPolygonConverter` implicitly for `MongoGeoPolygon` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +For line-string modeled values, Micronaut Data applies `MongoGeoLineStringConverter` implicitly for `MongoGeoLineString` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +For multi-line-string modeled values, Micronaut Data applies `MongoGeoMultiLineStringConverter` implicitly for `MongoGeoMultiLineString` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +For multi-point modeled values, Micronaut Data applies `MongoGeoMultiPointConverter` implicitly for `MongoGeoMultiPoint` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +For multi-polygon modeled values, Micronaut Data applies `MongoGeoMultiPolygonConverter` implicitly for `MongoGeoMultiPolygon` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +For geometry-collection modeled values, Micronaut Data applies `MongoGeoGeometryCollectionConverter` implicitly for `MongoGeoGeometryCollection` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. + +With `MongoGeoGeometryCollection` in place, modeled support now covers all standard GeoJSON geometry kinds (`Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`). + +NOTE: ann:data.mongodb.annotation.MongoGeoIndexed[] now validates property type compatibility during startup and fails fast for unsupported non-geospatial property types. + +== Wildcard indexes + +Use ann:data.mongodb.annotation.MongoWildcardIndexed[] for field-level wildcard indexes. + +Use ann:data.mongodb.annotation.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. + +NOTE: MongoDB allows `wildcardProjection` only for top-level wildcard indexes (`{"$**": 1}`). +Using `wildcardProjection` on field-level ann:data.mongodb.annotation.MongoWildcardIndexed[] is rejected during startup validation. + +== TTL indexes + +Use ann:data.mongodb.annotation.MongoIndexed[] with `expireAfterSeconds` to create TTL indexes: + +[source,java] +---- +@MappedEntity("sessions") +class SessionEntity { + @Id + @GeneratedValue + String id; + + @MongoIndexed(name = "expires_at_ttl_idx", expireAfterSeconds = 60) + Instant expiresAt; +} +---- + +== Clustered collections + +Use ann:data.mongodb.annotation.MongoClusteredIndex[] on the entity to configure clustered collection options during startup collection initialization. + +[source,java] +---- +@MongoClusteredIndex(name = "events_clustered_idx", unique = true, expireAfterSeconds = 3600) +@MappedEntity("events") +class EventEntity { + @Id + Instant id; + + String payload; +} +---- + +Clustered collection support follows MongoDB collection-level rules: + +* clustered options are applied through collection creation, not ordinary index creation +* `unique` must remain `true` +* when `expireAfterSeconds` is set, the identity type must be a date/time type (`Date`, `Instant`, `LocalDateTime`, `OffsetDateTime`) + +== Existing collections and conflicts + +When `create-indexes` is enabled, Micronaut Data checks existing collection indexes before creating new ones. +If an index with the same key exists but managed options differ, startup fails fast with an explicit conflict message. + +For clustered declarations, existing collection options are also compared, and conflicting clustered options fail fast. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 90e44b15792..fa308718f8a 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -114,6 +114,7 @@ mongo: mongoMapping: title: Mapping Entities mongoAnnotations: Mapping Annotations + mongoIndexes: MongoDB Indexes mongoIdGenerator: ID Generation mongoCompositeId: Composite Primary Keys mongoConstructorArguments: Constructor Arguments @@ -150,4 +151,3 @@ graal: spring: title: Spring Data Support dataGuides: Guides - From c98ab84e69191072bad6cd8bd72ca9895112f360 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sat, 28 Mar 2026 21:43:49 +0100 Subject: [PATCH 03/34] MongoDB indexes support --- .../annotation/MongoCompoundIndex.java | 15 +++ .../mongodb/annotation/MongoGeoIndexed.java | 10 ++ .../annotation/MongoHashedIndexed.java | 10 ++ .../data/mongodb/annotation/MongoIndexed.java | 10 ++ .../mongodb/annotation/MongoTextIndexed.java | 10 ++ .../annotation/MongoWildcardIndex.java | 15 +++ .../annotation/MongoWildcardIndexed.java | 10 ++ .../mongodb/common/MongoEntityIndexes.java | 47 ++++++- .../init/AbstractMongoCollectionsCreator.java | 14 ++- .../mongodb/init/MongoCollectionsCreator.java | 66 +++++++++- .../init/MongoReactiveCollectionsCreator.java | 82 ++++++++++-- .../operations/DefaultMongoStoredQuery.java | 13 +- .../mongodb/MongoIndexInspector.groovy | 1 + .../MongoIndexCommentCreationSpec.groovy | 73 +++++++++++ ...ongoGeoRawQueryParameterBindingSpec.groovy | 103 +++++++++++++++ .../MongoHiddenIndexCreationSpec.groovy | 119 ++++++++++++++++++ ...ngoReactiveIndexCommentCreationSpec.groovy | 18 +++ ...ongoReactiveHiddenIndexCreationSpec.groovy | 18 +++ ...veTopLevelWildcardIndexCreationSpec.groovy | 18 +++ ...WildcardProjectionIndexCreationSpec.groovy | 18 +++ ...oIndexAdvancedOptionsResolutionSpec.groovy | 110 ++++++++++++++++ ...goTopLevelWildcardIndexCreationSpec.groovy | 6 +- ...WildcardProjectionIndexCreationSpec.groovy | 6 +- .../rawquery/MongoGeoRawQueryEntity.java | 37 ++++++ .../MongoGeoRawQueryEntityRepository.java | 33 +++++ .../rawquery/ShiftedGeoPointConverter.java | 32 +++++ .../mongo/mongoMapping/mongoIndexes.adoc | 8 +- 27 files changed, 881 insertions(+), 21 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy create mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java create mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java create mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java index d9e4f4103b0..7cff187592d 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java @@ -56,6 +56,11 @@ */ boolean sparse() default false; + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + /** * @return The index expiration in seconds. */ @@ -70,4 +75,14 @@ * @return The collation definition as JSON. */ String collation() default ""; + + /** + * @return The index creation command comment. + */ + String comment() default ""; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java index 3061535de29..b74f3299b47 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java @@ -44,6 +44,16 @@ */ MongoGeoIndexType type() default MongoGeoIndexType.GEO_2DSPHERE; + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + + /** + * @return The index creation command comment. + */ + String comment() default ""; + /** * @return The 2d index bits setting, or -1 if unset. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java index e706dc41185..0194e97d341 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java @@ -38,4 +38,14 @@ * @return The index name. */ String name() default ""; + + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + + /** + * @return The index creation command comment. + */ + String comment() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java index 88347ff7320..7aa864ab679 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java @@ -54,6 +54,11 @@ */ boolean sparse() default false; + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + /** * @return The index expiration in seconds. */ @@ -68,4 +73,9 @@ * @return The collation definition as JSON. */ String collation() default ""; + + /** + * @return The index creation command comment. + */ + String comment() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java index 6049fb71863..6aa86b6e8fa 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java @@ -43,4 +43,14 @@ * @return The text index weight. */ int weight() default 1; + + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + + /** + * @return The index creation command comment. + */ + String comment() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java index 5d4866758b3..4d8c32f732f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java @@ -43,4 +43,19 @@ * @return The wildcard projection definition as JSON. */ String wildcardProjection() default ""; + + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + + /** + * @return The index creation command comment. + */ + String comment() default ""; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java index 492de9d88ee..8f08f94f4e3 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java @@ -44,4 +44,14 @@ */ String wildcardProjection() default ""; + /** + * @return Whether the index is hidden. + */ + boolean hidden() default false; + + /** + * @return The index creation command comment. + */ + String comment() default ""; + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index c9dc40a4e7d..fd5d6843396 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -92,13 +93,16 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist List.of(new ResolvedIndexField("$**", 1, null, null, null, null)), false, false, + annotation.booleanValue("hidden").orElse(false), null, null, null, null, null, null, - annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null) + annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) )); } return indexes; @@ -119,12 +123,15 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? annotation.intValue("expireAfterSeconds").getAsInt() : null, annotation.stringValue("partialFilterExpression").filter(s -> !s.isEmpty()).orElse(null), annotation.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), null, null, null, + null, + annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null )); continue; @@ -140,12 +147,15 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); continue; @@ -165,12 +175,15 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); continue; @@ -186,12 +199,15 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); } @@ -215,6 +231,8 @@ private static void validateGeoIndexedType(RuntimePersistentEntity entity, private static List resolveTextIndexes(RuntimePersistentEntity entity) { List fields = new ArrayList<>(); String name = null; + Boolean hidden = null; + String comment = null; BeanIntrospection introspection = entity.getIntrospection(); for (BeanProperty beanProperty : introspection.getBeanProperties()) { RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); @@ -233,13 +251,25 @@ private static List resolveTextIndexes(RuntimePersistentEntity } else if (declaredName != null && !declaredName.equals(name)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same index name"); } + boolean declaredHidden = textAnnotation.booleanValue("hidden").orElse(false); + if (hidden == null) { + hidden = declaredHidden; + } else if (!hidden.equals(declaredHidden)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same hidden option"); + } + String declaredComment = textAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null); + if (comment == null) { + comment = declaredComment; + } else if (!Objects.equals(comment, declaredComment)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same comment option"); + } fields.add(new ResolvedIndexField(property.getPersistedName(), null, weight, "text", null, null)); } } if (fields.isEmpty()) { return List.of(); } - return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, null, null, null, null, null, null, null)); + return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, null, comment, null)); } private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { @@ -332,13 +362,16 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit List.copyOf(fields), annotationValue.booleanValue("unique").orElse(false), sparse, + annotationValue.booleanValue("hidden").orElse(false), null, partialFilterExpression, annotationValue.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), indexBits, indexMin, indexMax, - null + null, + annotationValue.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + annotationValue.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) )); } return indexes; @@ -351,6 +384,7 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit * @param fields The index fields * @param unique Whether unique * @param sparse Whether sparse + * @param hidden Whether hidden * @param expireAfterSeconds TTL in seconds if any * @param partialFilterExpression The partial filter expression JSON * @param collation The collation JSON @@ -358,18 +392,23 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit * @param min The geospatial min option for 2d indexes * @param max The geospatial max option for 2d indexes * @param wildcardProjection The wildcard projection JSON + * @param comment The index creation comment + * @param commitQuorum The createIndexes commit quorum */ public record ResolvedIndex(@Nullable String name, List fields, boolean unique, boolean sparse, + boolean hidden, @Nullable Integer expireAfterSeconds, @Nullable String partialFilterExpression, @Nullable String collation, @Nullable Integer bits, @Nullable Double min, @Nullable Double max, - @Nullable String wildcardProjection) { + @Nullable String wildcardProjection, + @Nullable String comment, + @Nullable String commitQuorum) { } /** diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index a636006934b..c1e0fdc71b8 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -256,13 +256,16 @@ private List resolveIndexes(PersistentEntity entity) { fields, index.unique(), index.sparse(), + index.hidden(), index.expireAfterSeconds(), normalizeJsonString(index.partialFilterExpression()), normalizeJsonString(index.collation()), index.bits(), index.min(), index.max(), - normalizeJsonString(index.wildcardProjection()) + normalizeJsonString(index.wildcardProjection()), + index.comment(), + index.commitQuorum() )); } return indexes; @@ -429,13 +432,16 @@ record MongoResolvedIndex(@Nullable String name, List fields, boolean unique, boolean sparse, + boolean hidden, @Nullable Integer expireAfterSeconds, @Nullable String partialFilterExpression, @Nullable String collation, @Nullable Integer bits, @Nullable Double min, @Nullable Double max, - @Nullable String wildcardProjection) { + @Nullable String wildcardProjection, + @Nullable String comment, + @Nullable String commitQuorum) { boolean hasSameKey(MongoResolvedIndex other) { return fields.equals(other.fields); @@ -444,6 +450,7 @@ boolean hasSameKey(MongoResolvedIndex other) { boolean matchesManagedOptions(MongoResolvedIndex other) { return unique == other.unique && sparse == other.sparse + && hidden == other.hidden && Objects.equals(expireAfterSeconds, other.expireAfterSeconds) && Objects.equals(partialFilterExpression, other.partialFilterExpression) && collationMatches(collation, other.collation) @@ -494,6 +501,7 @@ String describe() { + ", fields=" + fields + ", unique=" + unique + ", sparse=" + sparse + + ", hidden=" + hidden + ", expireAfterSeconds=" + expireAfterSeconds + ", partialFilterExpression=" + partialFilterExpression + ", collation=" + collation @@ -501,6 +509,8 @@ String describe() { + ", min=" + min + ", max=" + max + ", wildcardProjection=" + wildcardProjection + + ", comment=" + comment + + ", commitQuorum=" + commitQuorum + '}'; } } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 84e3833bf57..2db80154280 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -153,13 +153,16 @@ public List listIndexes(MongoDatabase database, String colle List.copyOf(fields), indexDocument.getBoolean("unique", false), indexDocument.getBoolean("sparse", false), + indexDocument.getBoolean("hidden", false), indexDocument.getInteger("expireAfterSeconds"), normalizeJsonValue(indexDocument.get("partialFilterExpression")), normalizeJsonValue(indexDocument.get("collation")), toInteger(indexDocument.get("bits")), toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), - normalizeJsonValue(indexDocument.get("wildcardProjection")) + normalizeJsonValue(indexDocument.get("wildcardProjection")), + null, + null )); } return indexes; @@ -168,7 +171,22 @@ public List listIndexes(MongoDatabase database, String colle @Override public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { MongoCollection mongoCollection = database.getCollection(collection); + if (index.comment() != null || index.commitQuorum() != null) { + Document command = new Document("createIndexes", collection) + .append("indexes", List.of(toIndexCommandDocument(index))); + if (index.comment() != null) { + command.append("comment", index.comment()); + } + if (index.commitQuorum() != null) { + command.append("commitQuorum", toCommitQuorumValue(index.commitQuorum())); + } + database.runCommand(command); + return; + } IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); + if (index.hidden()) { + indexOptions.hidden(true); + } if (index.name() != null) { indexOptions.name(index.name()); } @@ -199,6 +217,52 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved }, mongoCollectionNameProvider); } + private Document toIndexCommandDocument(MongoResolvedIndex index) { + Document indexDocument = new Document("key", index.keysDocument()); + if (index.name() != null) { + indexDocument.append("name", index.name()); + } + if (index.unique()) { + indexDocument.append("unique", true); + } + if (index.sparse()) { + indexDocument.append("sparse", true); + } + if (index.hidden()) { + indexDocument.append("hidden", true); + } + if (index.expireAfterSeconds() != null) { + indexDocument.append("expireAfterSeconds", index.expireAfterSeconds()); + } + if (index.partialFilterExpression() != null) { + indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexDocument.append("collation", Document.parse(index.collation())); + } + if (index.bits() != null) { + indexDocument.append("bits", index.bits()); + } + if (index.min() != null) { + indexDocument.append("min", index.min()); + } + if (index.max() != null) { + indexDocument.append("max", index.max()); + } + if (index.wildcardProjection() != null) { + indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); + } + return indexDocument; + } + + private Object toCommitQuorumValue(String commitQuorum) { + try { + return Integer.parseInt(commitQuorum); + } catch (NumberFormatException ignored) { + return commitQuorum; + } + } + private Collation toCollation(Document document) { Collation.Builder builder = Collation.builder(); String locale = document.getString("locale"); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index 40cd01a6cd0..3b3c13a2af4 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -154,18 +154,21 @@ public List listIndexes(MongoDatabase database, String colle fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); } } - resolvedIndexes.add(new MongoResolvedIndex( - indexDocument.getString("name"), - List.copyOf(fields), - indexDocument.getBoolean("unique", false), - indexDocument.getBoolean("sparse", false), - indexDocument.getInteger("expireAfterSeconds"), - normalizeJsonValue(indexDocument.get("partialFilterExpression")), - normalizeJsonValue(indexDocument.get("collation")), + resolvedIndexes.add(new MongoResolvedIndex( + indexDocument.getString("name"), + List.copyOf(fields), + indexDocument.getBoolean("unique", false), + indexDocument.getBoolean("sparse", false), + indexDocument.getBoolean("hidden", false), + indexDocument.getInteger("expireAfterSeconds"), + normalizeJsonValue(indexDocument.get("partialFilterExpression")), + normalizeJsonValue(indexDocument.get("collation")), toInteger(indexDocument.get("bits")), toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), - normalizeJsonValue(indexDocument.get("wildcardProjection")) + normalizeJsonValue(indexDocument.get("wildcardProjection")), + null, + null )); } return resolvedIndexes; @@ -177,7 +180,22 @@ public List listIndexes(MongoDatabase database, String colle @Override public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { MongoCollection mongoCollection = database.getCollection(collection); + if (index.comment() != null || index.commitQuorum() != null) { + Document command = new Document("createIndexes", collection) + .append("indexes", List.of(toIndexCommandDocument(index))); + if (index.comment() != null) { + command.append("comment", index.comment()); + } + if (index.commitQuorum() != null) { + command.append("commitQuorum", toCommitQuorumValue(index.commitQuorum())); + } + Mono.from(database.runCommand(command)).block(); + return; + } IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); + if (index.hidden()) { + indexOptions.hidden(true); + } if (index.name() != null) { indexOptions.name(index.name()); } @@ -208,6 +226,52 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved }, mongoCollectionNameProvider); } + private Document toIndexCommandDocument(MongoResolvedIndex index) { + Document indexDocument = new Document("key", index.keysDocument()); + if (index.name() != null) { + indexDocument.append("name", index.name()); + } + if (index.unique()) { + indexDocument.append("unique", true); + } + if (index.sparse()) { + indexDocument.append("sparse", true); + } + if (index.hidden()) { + indexDocument.append("hidden", true); + } + if (index.expireAfterSeconds() != null) { + indexDocument.append("expireAfterSeconds", index.expireAfterSeconds()); + } + if (index.partialFilterExpression() != null) { + indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexDocument.append("collation", Document.parse(index.collation())); + } + if (index.bits() != null) { + indexDocument.append("bits", index.bits()); + } + if (index.min() != null) { + indexDocument.append("min", index.min()); + } + if (index.max() != null) { + indexDocument.append("max", index.max()); + } + if (index.wildcardProjection() != null) { + indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); + } + return indexDocument; + } + + private Object toCommitQuorumValue(String commitQuorum) { + try { + return Integer.parseInt(commitQuorum); + } catch (NumberFormatException ignored) { + return commitQuorum; + } + } + private Collation toCollation(Document document) { Collation.Builder builder = Collation.builder(); String locale = document.getString("locale"); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java index 60914b5d5ba..b55624176e5 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java @@ -408,8 +408,17 @@ public Object convert(@Nullable Object value, @Nullable RuntimePersistentPropert @Nullable @Override public Object convert(@Nullable Class converterClass, @Nullable Object value, @Nullable Argument argument) { - if (converterClass == null) { - return value; + if (converterClass == null || converterClass == Object.class) { + if (value == null) { + return value; + } + Class geoType = argument != null ? argument.getType() : value.getClass(); + if (!MongoGeoConverters.supportsImplicitGeoType(geoType)) { + return value; + } + AttributeConverter implicitConverter = attributeConverterRegistry.getConverter(MongoGeoConverters.resolveImplicitGeoConverterClass(geoType)); + ConversionContext conversionContext = argument != null ? ConversionContext.of(argument) : ConversionContext.of(Argument.of(geoType)); + return implicitConverter.convertToPersistedValue(value, conversionContext); } AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); ConversionContext conversionContext = createTypeConversionContext(null, argument); diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy index 4758d60ec77..cc198fb4240 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy @@ -32,6 +32,7 @@ final class MongoIndexInspector { fields : List.copyOf(fields), unique : indexDocument.getBoolean('unique', false), sparse : indexDocument.getBoolean('sparse', false), + hidden : indexDocument.getBoolean('hidden', false), expireAfterSeconds: indexDocument.getInteger('expireAfterSeconds'), partialFilterExpression: indexDocument.get('partialFilterExpression'), collation : indexDocument.get('collation'), diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy new file mode 100644 index 00000000000..4710abc0a57 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.comment + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoIndexCommentCreationSpec extends Specification implements MongoTestPropertyProvider { + + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.comment'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates index when comment option is declared'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'comment_indexed_entities') + assert indexes*.name.contains('comment_name_idx') + def index = indexes.find { it.name == 'comment_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface CommentIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('comment_indexed_entities') +class CommentIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'comment_name_idx', comment = 'comment-index-build') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy new file mode 100644 index 00000000000..179fc8cda9e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy @@ -0,0 +1,103 @@ +package io.micronaut.data.document.mongodb.geovalue.rawquery + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class MongoGeoRawQueryParameterBindingSpec extends Specification implements MongoTestPropertyProvider { + + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoGeoRawQueryEntityRepository repository + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.geovalue.rawquery'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + repository = applicationContext.getBean(MongoGeoRawQueryEntityRepository) + } + + void setup() { + repository.deleteAll() + } + + void 'binds modeled geospatial parameter in @MongoFindQuery as GeoJSON'() { + given: + def multiPoint = new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + + when: + def saved = repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) + def loaded = repository.findByIntersectsGeometry(new MongoGeoPoint(-73.99d, 40.75d)) + + then: + loaded.present + loaded.get().id == saved.id + } + + void 'binds modeled geospatial parameter typed as Object in @MongoFindQuery as GeoJSON'() { + given: + def multiPoint = new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + + when: + def saved = repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) + def loaded = repository.findByIntersectsGeometryObject(new MongoGeoPoint(-73.99d, 40.75d)) + + then: + loaded.present + loaded.get().id == saved.id + } + + void 'keeps null raw-query geospatial parameter as null binding'() { + given: + def multiPoint = new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) + + when: + def loaded = repository.findByLocationsRawNullable(null) + + then: + !loaded.present + } + + void 'respects explicit parameter converter and does not apply implicit geospatial fallback'() { + given: + def multiPoint = new MongoGeoMultiPoint([ + new MongoGeoPoint(-73.99d, 40.75d), + new MongoGeoPoint(-73.98d, 40.74d), + new MongoGeoPoint(-73.97d, 40.73d) + ]) + repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) + + when: + repository.findByIntersectsGeometryWithExplicitConverter(new MongoGeoPoint(-73.99d, 40.75d)) + + then: + def e = thrown(Exception) + e.message.contains('Longitude/latitude is out of bounds') + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy new file mode 100644 index 00000000000..467a40880fe --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy @@ -0,0 +1,119 @@ +package io.micronaut.data.document.mongodb.hidden + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoHiddenIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.hidden'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates hidden single-field index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'hidden_indexed_entities') + assert indexes*.name.contains('hidden_name_idx') + def index = indexes.find { it.name == 'hidden_name_idx' } + assert index.hidden + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + } + } + + void 'creates hidden compound index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'hidden_compound_indexed_entities') + assert indexes*.name.contains('hidden_compound_idx') + def index = indexes.find { it.name == 'hidden_compound_idx' } + assert index.hidden + assert index.fields.size() == 2 + assert index.fields[0].path() == 'first_name' + assert index.fields[0].order() == 1 + assert index.fields[1].path() == 'last_name' + assert index.fields[1].order() == -1 + } + } +} + +@MongoRepository +interface HiddenIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('hidden_indexed_entities') +class HiddenIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'hidden_name_idx', hidden = true) + String name +} + +@MongoRepository +interface HiddenCompoundIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'hidden_compound_idx', + hidden = true, + fields = [ + @MongoCompoundIndexField('firstName'), + @MongoCompoundIndexField(value = 'lastName', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('hidden_compound_indexed_entities') +class HiddenCompoundIndexedEntity { + @Id + @GeneratedValue + String id + + String firstName + + String lastName +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy new file mode 100644 index 00000000000..b53b556d1d2 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.comment + +import io.micronaut.data.document.mongodb.comment.MongoIndexCommentCreationSpec + +class MongoReactiveIndexCommentCreationSpec extends MongoIndexCommentCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy new file mode 100644 index 00000000000..4444d48710a --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.hidden + +import io.micronaut.data.document.mongodb.hidden.MongoHiddenIndexCreationSpec + +class MongoReactiveHiddenIndexCreationSpec extends MongoHiddenIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy new file mode 100644 index 00000000000..42e43871863 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel + +import io.micronaut.data.document.mongodb.wildcard.toplevel.MongoTopLevelWildcardIndexCreationSpec + +class MongoReactiveTopLevelWildcardIndexCreationSpec extends MongoTopLevelWildcardIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy new file mode 100644 index 00000000000..f5d24dc7aa1 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel + +import io.micronaut.data.document.mongodb.wildcard.toplevel.MongoTopLevelWildcardProjectionIndexCreationSpec + +class MongoReactiveTopLevelWildcardProjectionIndexCreationSpec extends MongoTopLevelWildcardProjectionIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy new file mode 100644 index 00000000000..c953cfee042 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -0,0 +1,110 @@ +package io.micronaut.data.document.mongodb.validation.options + +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.model.runtime.RuntimePersistentEntity +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.common.MongoEntityIndexes +import spock.lang.Shared +import spock.lang.Specification + +class MongoIndexAdvancedOptionsResolutionSpec extends Specification { + + @Shared + Map, RuntimePersistentEntity> entities = [:] + + void 'resolves comment for simple index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(CommentSimpleEntity)).indexes + def index = indexes.find { it.name() == 'comment_simple_idx' } + + then: + index != null + index.comment() == 'simple-comment' + index.commitQuorum() == null + } + + void 'resolves comment and commitQuorum for compound index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(CommentCommitQuorumCompoundEntity)).indexes + def index = indexes.find { it.name() == 'comment_commit_compound_idx' } + + then: + index != null + index.comment() == 'compound-comment' + index.commitQuorum() == 'majority' + } + + void 'resolves comment and commitQuorum for top-level wildcard index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(CommentCommitQuorumWildcardEntity)).indexes + def index = indexes.find { it.name() == 'comment_commit_wildcard_idx' } + + then: + index != null + index.comment() == 'wildcard-comment' + index.commitQuorum() == 'majority' + } + + void 'fails when text indexed fields define different comments'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidTextCommentEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must use the same comment option') + } + + private RuntimePersistentEntity getRuntimePersistentEntity(Class type) { + RuntimePersistentEntity entity = entities.get(type) + if (entity == null) { + entity = new RuntimePersistentEntity(type) { + @Override + protected RuntimePersistentEntity getEntity(Class t) { + return getRuntimePersistentEntity(t) + } + } + entities.put(type, entity) + } + return entity + } +} + +@MappedEntity('comment_simple_entity') +class CommentSimpleEntity { + @MongoIndexed(name = 'comment_simple_idx', comment = 'simple-comment') + String name +} + +@MongoCompoundIndex( + name = 'comment_commit_compound_idx', + fields = [ + @MongoCompoundIndexField('name'), + @MongoCompoundIndexField('city') + ], + comment = 'compound-comment', + commitQuorum = 'majority' +) +@MappedEntity('comment_commit_compound_entity') +class CommentCommitQuorumCompoundEntity { + String name + String city +} + +@MongoWildcardIndex(name = 'comment_commit_wildcard_idx', comment = 'wildcard-comment', commitQuorum = 'majority') +@MappedEntity('comment_commit_wildcard_entity') +class CommentCommitQuorumWildcardEntity { + String name +} + +@MappedEntity('invalid_text_comment_entity') +class InvalidTextCommentEntity { + @MongoTextIndexed(comment = 'a') + String first + + @MongoTextIndexed(comment = 'b') + String second +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy index 84fde1241f4..42084ce9aa6 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoTopLevelWildcardIndexCreationSpec extends Specification implements Mo ['io.micronaut.data.document.mongodb.wildcard.toplevel'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoTopLevelWildcardIndexCreationSpec extends Specification implements Mo def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_indexed_entities') assert indexes*.name.contains('top_level_wildcard_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy index 066d0ca1019..7e420429d4f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy @@ -29,6 +29,10 @@ class MongoTopLevelWildcardProjectionIndexCreationSpec extends Specification imp ['io.micronaut.data.document.mongodb.wildcard.toplevel'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -42,7 +46,7 @@ class MongoTopLevelWildcardProjectionIndexCreationSpec extends Specification imp def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_projection_indexed_entities') assert indexes*.name.contains('top_level_wildcard_projection_idx') diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java new file mode 100644 index 00000000000..a6aef1946f6 --- /dev/null +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java @@ -0,0 +1,37 @@ +package io.micronaut.data.document.mongodb.geovalue.rawquery; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; + +@MappedEntity("geo_raw_query_entities") +public class MongoGeoRawQueryEntity { + + @Id + @GeneratedValue + private String id; + + @TypeDef(type = DataType.OBJECT) + @MongoGeoIndexed(name = "geo_raw_query_idx") + private MongoGeoMultiPoint locations; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public MongoGeoMultiPoint getLocations() { + return locations; + } + + public void setLocations(MongoGeoMultiPoint locations) { + this.locations = locations; + } +} diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java new file mode 100644 index 00000000000..f1ce5875949 --- /dev/null +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java @@ -0,0 +1,33 @@ +package io.micronaut.data.document.mongodb.geovalue.rawquery; + +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.micronaut.data.mongodb.annotation.MongoFindQuery; +import io.micronaut.data.mongodb.annotation.MongoRepository; +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; +import io.micronaut.data.mongodb.geo.MongoGeoPoint; +import io.micronaut.data.repository.CrudRepository; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +@MongoRepository +public interface MongoGeoRawQueryEntityRepository extends CrudRepository { + + @MongoFindQuery(filter = "{'locations': {'$eq': :locations}}") + Optional findByLocationsRaw(MongoGeoMultiPoint locations); + + @MongoFindQuery(filter = "{'locations': {'$eq': :locations}}") + Optional findByLocationsRawNullable(@Nullable MongoGeoMultiPoint locations); + + @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") + Optional findByIntersectsGeometry(MongoGeoPoint geometry); + + @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") + Optional findByIntersectsGeometryObject(Object geometry); + + @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") + Optional findByIntersectsGeometryWithExplicitConverter( + @TypeDef(type = DataType.OBJECT, converter = ShiftedGeoPointConverter.class) MongoGeoPoint geometry + ); +} diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java new file mode 100644 index 00000000000..6032069d55e --- /dev/null +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java @@ -0,0 +1,32 @@ +package io.micronaut.data.document.mongodb.geovalue.rawquery; + +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.data.model.runtime.convert.AttributeConverter; +import io.micronaut.data.mongodb.geo.MongoGeoPoint; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.Map; + +@Singleton +final class ShiftedGeoPointConverter implements AttributeConverter> { + + @Override + public Map convertToPersistedValue(MongoGeoPoint entityValue, ConversionContext context) { + return Map.of( + "type", "Point", + "coordinates", List.of(entityValue.x() + 100, entityValue.y() + 100) + ); + } + + @Override + public MongoGeoPoint convertToEntityValue(Map persistedValue, ConversionContext context) { + Object coordinates = persistedValue.get("coordinates"); + if (coordinates instanceof List list && list.size() >= 2) { + Number x = (Number) list.get(0); + Number y = (Number) list.get(1); + return new MongoGeoPoint(x.doubleValue() - 100, y.doubleValue() - 100); + } + throw new IllegalArgumentException("Invalid persisted geo point: " + persistedValue); + } +} diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index 1543f34fdd8..b3e779db45a 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -21,7 +21,7 @@ Micronaut Data currently supports the following MongoDB index annotations: Use ann:data.mongodb.annotation.MongoIndexed[] to declare standard single-field indexes. -Supported options include direction, `unique`, `sparse`, `expireAfterSeconds` (TTL), `partialFilterExpression`, and `collation`. +Supported options include direction, `unique`, `sparse`, `hidden`, `expireAfterSeconds` (TTL), `partialFilterExpression`, `collation`, and index-build `comment`. [source,java] ---- @@ -40,6 +40,8 @@ class ProductEntity { Use ann:data.mongodb.annotation.MongoCompoundIndex[] for multi-field index declarations at entity level. +Compound declarations also support index-build `comment` and create-index command `commitQuorum`. + [source,java] ---- @MongoCompoundIndex( @@ -95,6 +97,8 @@ For geometry-collection modeled values, Micronaut Data applies `MongoGeoGeometry With `MongoGeoGeometryCollection` in place, modeled support now covers all standard GeoJSON geometry kinds (`Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`). +For raw Mongo query annotations (for example ann:data.mongodb.annotation.MongoFindQuery[]), supported modeled geospatial parameter types are also converted implicitly to GeoJSON persisted shapes (`type` + `coordinates`) when no explicit converter is specified. + NOTE: ann:data.mongodb.annotation.MongoGeoIndexed[] now validates property type compatibility during startup and fails fast for unsupported non-geospatial property types. == Wildcard indexes @@ -103,6 +107,8 @@ Use ann:data.mongodb.annotation.MongoWildcardIndexed[] for field-level wildcard Use ann:data.mongodb.annotation.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. +Top-level wildcard declarations also support index-build `comment` and create-index command `commitQuorum`. + NOTE: MongoDB allows `wildcardProjection` only for top-level wildcard indexes (`{"$**": 1}`). Using `wildcardProjection` on field-level ann:data.mongodb.annotation.MongoWildcardIndexed[] is rejected during startup validation. From 571a426749242acea6a64412ed79a1ce39a6b8ce Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sat, 28 Mar 2026 22:27:06 +0100 Subject: [PATCH 04/34] MongoDB indexes support --- .../annotation/MongoCompoundIndex.java | 5 ++ .../mongodb/annotation/MongoGeoIndexed.java | 5 ++ .../annotation/MongoHashedIndexed.java | 5 ++ .../data/mongodb/annotation/MongoIndexed.java | 5 ++ .../mongodb/annotation/MongoTextIndexed.java | 5 ++ .../annotation/MongoWildcardIndex.java | 7 ++ .../annotation/MongoWildcardIndexed.java | 5 ++ .../annotation/MongoWildcardIndexes.java | 41 +++++++++ .../mongodb/common/MongoEntityIndexes.java | 89 ++++++++++++++++--- .../init/AbstractMongoCollectionsCreator.java | 6 +- .../mongodb/init/MongoCollectionsCreator.java | 7 ++ .../init/MongoReactiveCollectionsCreator.java | 7 ++ ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 6 +- ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 18 ++++ ...tiveAggregatedTextIndexCreationSpec.groovy | 18 ++++ ...ltipleDeclarationsIndexCreationSpec.groovy | 18 ++++ ...ongoAggregatedTextIndexCreationSpec.groovy | 6 +- ...oIndexAdvancedOptionsResolutionSpec.groovy | 29 +++++- ...ReactiveStorageEngineValidationSpec.groovy | 46 ++++++++++ ...dMultipleDeclarationsValidationSpec.groovy | 46 ++++++++++ .../MongoStorageEngineValidationSpec.groovy | 46 ++++++++++ ...dMultipleDeclarationsValidationSpec.groovy | 46 ++++++++++ ...ltipleDeclarationsIndexCreationSpec.groovy | 71 +++++++++++++++ .../mongo/mongoMapping/mongoIndexes.adoc | 16 ++-- 24 files changed, 532 insertions(+), 21 deletions(-) create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java index 7cff187592d..742f2ddbe14 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java @@ -85,4 +85,9 @@ * @return The createIndexes commit quorum. */ String commitQuorum() default ""; + + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java index b74f3299b47..76be835d65c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java @@ -54,6 +54,11 @@ */ String comment() default ""; + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; + /** * @return The 2d index bits setting, or -1 if unset. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java index 0194e97d341..914f9d8d9ff 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java @@ -48,4 +48,9 @@ * @return The index creation command comment. */ String comment() default ""; + + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java index 7aa864ab679..8cad8522641 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java @@ -78,4 +78,9 @@ * @return The index creation command comment. */ String comment() default ""; + + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java index 6aa86b6e8fa..39940175bee 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java @@ -53,4 +53,9 @@ * @return The index creation command comment. */ String comment() default ""; + + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java index 4d8c32f732f..af90685c9ae 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java @@ -18,6 +18,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,6 +33,7 @@ @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Documented @Inherited +@Repeatable(MongoWildcardIndexes.class) public @interface MongoWildcardIndex { /** @@ -58,4 +60,9 @@ * @return The createIndexes commit quorum. */ String commitQuorum() default ""; + + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java index 8f08f94f4e3..919f2b25832 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java @@ -54,4 +54,9 @@ */ String comment() default ""; + /** + * @return The storage engine options as JSON. + */ + String storageEngine() default ""; + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java new file mode 100644 index 00000000000..d8fd45d460d --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.mongodb.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Repeatable annotation for {@link MongoWildcardIndex}. + * + * @author radovanradic + * @since 5.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface MongoWildcardIndexes { + + /** + * @return The indexes. + */ + MongoWildcardIndex[] value() default {}; +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index fd5d6843396..a8f05068a8d 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -34,15 +34,14 @@ import io.micronaut.data.model.PersistentEntityUtils; import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.model.runtime.RuntimePersistentProperty; +import org.bson.Document; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** - * Cached Mongo index metadata resolved at runtime. + * Mongo index metadata resolved at runtime. * * @author radovanradic * @since 5.0.0 @@ -50,8 +49,6 @@ @Internal public final class MongoEntityIndexes { - private static final Map, MongoEntityIndexes> INDEXES_BY_ENTITY = new ConcurrentHashMap<>(); - private final List indexes; private MongoEntityIndexes(List indexes) { @@ -65,7 +62,7 @@ private MongoEntityIndexes(List indexes) { * @return The resolved indexes */ public static MongoEntityIndexes create(RuntimePersistentEntity entity) { - return INDEXES_BY_ENTITY.computeIfAbsent(entity, MongoEntityIndexes::resolve); + return resolve(entity); } /** @@ -86,8 +83,7 @@ private static MongoEntityIndexes resolve(RuntimePersistentEntity entity) { private static List resolveTopLevelWildcardIndexes(RuntimePersistentEntity entity) { List indexes = new ArrayList<>(); - var annotation = entity.getAnnotationMetadata().getAnnotation(MongoWildcardIndex.class); - if (annotation != null) { + for (var annotation : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoWildcardIndex.class)) { indexes.add(new ResolvedIndex( annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField("$**", 1, null, null, null, null)), @@ -101,11 +97,57 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist null, null, annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(annotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), annotation.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) )); } - return indexes; + if (indexes.size() <= 1) { + return indexes; + } + ResolvedIndex first = indexes.getFirst(); + String mergedName = first.name(); + for (int i = 1; i < indexes.size(); i++) { + ResolvedIndex candidate = indexes.get(i); + if (!Objects.equals(first.fields(), candidate.fields()) + || first.unique() != candidate.unique() + || first.sparse() != candidate.sparse() + || first.hidden() != candidate.hidden() + || !Objects.equals(first.expireAfterSeconds(), candidate.expireAfterSeconds()) + || !Objects.equals(first.partialFilterExpression(), candidate.partialFilterExpression()) + || !Objects.equals(first.collation(), candidate.collation()) + || !Objects.equals(first.bits(), candidate.bits()) + || !Objects.equals(first.min(), candidate.min()) + || !Objects.equals(first.max(), candidate.max()) + || !Objects.equals(first.wildcardProjection(), candidate.wildcardProjection()) + || !Objects.equals(first.storageEngine(), candidate.storageEngine()) + || !Objects.equals(first.comment(), candidate.comment()) + || !Objects.equals(first.commitQuorum(), candidate.commitQuorum())) { + throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] declare conflicting options for key [$**]"); + } + if (mergedName == null) { + mergedName = candidate.name(); + } else if (candidate.name() != null && !mergedName.equals(candidate.name())) { + throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] must use the same index name when declaring equivalent key [$**]"); + } + } + return List.of(new ResolvedIndex( + mergedName, + first.fields(), + first.unique(), + first.sparse(), + first.hidden(), + first.expireAfterSeconds(), + first.partialFilterExpression(), + first.collation(), + first.bits(), + first.min(), + first.max(), + first.wildcardProjection(), + first.storageEngine(), + first.comment(), + first.commitQuorum() + )); } private static List resolveFieldIndexes(RuntimePersistentEntity entity) { @@ -131,6 +173,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null )); @@ -155,6 +198,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), hashedAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null )); @@ -183,6 +227,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), geoAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null )); @@ -207,6 +252,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), wildcardAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null )); @@ -232,6 +278,7 @@ private static List resolveTextIndexes(RuntimePersistentEntity List fields = new ArrayList<>(); String name = null; Boolean hidden = null; + String storageEngine = null; String comment = null; BeanIntrospection introspection = entity.getIntrospection(); for (BeanProperty beanProperty : introspection.getBeanProperties()) { @@ -263,13 +310,19 @@ private static List resolveTextIndexes(RuntimePersistentEntity } else if (!Objects.equals(comment, declaredComment)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same comment option"); } + String declaredStorageEngine = parseJsonOption(textAnnotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()); + if (storageEngine == null) { + storageEngine = declaredStorageEngine; + } else if (!Objects.equals(storageEngine, declaredStorageEngine)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same storageEngine option"); + } fields.add(new ResolvedIndexField(property.getPersistedName(), null, weight, "text", null, null)); } } if (fields.isEmpty()) { return List.of(); } - return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, null, comment, null)); + return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, null, storageEngine, comment, null)); } private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { @@ -370,6 +423,7 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit indexMin, indexMax, null, + parseJsonOption(annotationValue.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotationValue.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), annotationValue.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) )); @@ -392,6 +446,7 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit * @param min The geospatial min option for 2d indexes * @param max The geospatial max option for 2d indexes * @param wildcardProjection The wildcard projection JSON + * @param storageEngine The storage engine options JSON * @param comment The index creation comment * @param commitQuorum The createIndexes commit quorum */ @@ -407,10 +462,24 @@ public record ResolvedIndex(@Nullable String name, @Nullable Double min, @Nullable Double max, @Nullable String wildcardProjection, + @Nullable String storageEngine, @Nullable String comment, @Nullable String commitQuorum) { } + private static @Nullable String parseJsonOption(@Nullable String json, + String option, + String entityName) { + if (json == null || json.isBlank()) { + return null; + } + try { + return Document.parse(json).toJson(); + } catch (RuntimeException e) { + throw new IllegalStateException("Mongo " + option + " for entity [" + entityName + "] must be valid JSON", e); + } + } + /** * Resolved Mongo index field. * diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index c1e0fdc71b8..b2e634abe7e 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -264,6 +264,7 @@ private List resolveIndexes(PersistentEntity entity) { index.min(), index.max(), normalizeJsonString(index.wildcardProjection()), + normalizeJsonString(index.storageEngine()), index.comment(), index.commitQuorum() )); @@ -440,6 +441,7 @@ record MongoResolvedIndex(@Nullable String name, @Nullable Double min, @Nullable Double max, @Nullable String wildcardProjection, + @Nullable String storageEngine, @Nullable String comment, @Nullable String commitQuorum) { @@ -457,7 +459,8 @@ && collationMatches(collation, other.collation) && Objects.equals(bits, other.bits) && Objects.equals(min, other.min) && Objects.equals(max, other.max) - && Objects.equals(wildcardProjection, other.wildcardProjection); + && Objects.equals(wildcardProjection, other.wildcardProjection) + && Objects.equals(storageEngine, other.storageEngine); } private static boolean collationMatches(@Nullable String existingCollation, @@ -509,6 +512,7 @@ String describe() { + ", min=" + min + ", max=" + max + ", wildcardProjection=" + wildcardProjection + + ", storageEngine=" + storageEngine + ", comment=" + comment + ", commitQuorum=" + commitQuorum + '}'; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 2db80154280..328b84b71cd 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -161,6 +161,7 @@ public List listIndexes(MongoDatabase database, String colle toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), normalizeJsonValue(indexDocument.get("wildcardProjection")), + normalizeJsonValue(indexDocument.get("storageEngine")), null, null )); @@ -211,6 +212,9 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.wildcardProjection() != null) { indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); } + if (index.storageEngine() != null) { + indexOptions.storageEngine(Document.parse(index.storageEngine())); + } mongoCollection.createIndex(index.keysDocument(), indexOptions); } }; @@ -252,6 +256,9 @@ private Document toIndexCommandDocument(MongoResolvedIndex index) { if (index.wildcardProjection() != null) { indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); } + if (index.storageEngine() != null) { + indexDocument.append("storageEngine", Document.parse(index.storageEngine())); + } return indexDocument; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index 3b3c13a2af4..c3399071c2b 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -167,6 +167,7 @@ public List listIndexes(MongoDatabase database, String colle toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), normalizeJsonValue(indexDocument.get("wildcardProjection")), + normalizeJsonValue(indexDocument.get("storageEngine")), null, null )); @@ -220,6 +221,9 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.wildcardProjection() != null) { indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); } + if (index.storageEngine() != null) { + indexOptions.storageEngine(Document.parse(index.storageEngine())); + } Mono.from(mongoCollection.createIndex(index.keysDocument(), indexOptions)).block(); } }; @@ -261,6 +265,9 @@ private Document toIndexCommandDocument(MongoResolvedIndex index) { if (index.wildcardProjection() != null) { indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); } + if (index.storageEngine() != null) { + indexDocument.append("storageEngine", Document.parse(index.storageEngine())); + } return indexDocument; } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy index 820a6743e25..21ed2d6ca97 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -31,6 +31,10 @@ class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implement ['io.micronaut.data.document.mongodb.geocompound.options'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -44,7 +48,7 @@ class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implement def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'geo2d_compound_options_indexed_entities') assert indexes*.name.contains('geo2d_name_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy new file mode 100644 index 00000000000..cfac781868d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.geocompound.options + +import io.micronaut.data.document.mongodb.geocompound.options.MongoCompoundGeo2dOptionsIndexCreationSpec + +class MongoReactiveCompoundGeo2dOptionsIndexCreationSpec extends MongoCompoundGeo2dOptionsIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..72690b9fabe --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.text + +import io.micronaut.data.document.mongodb.text.MongoAggregatedTextIndexCreationSpec + +class MongoReactiveAggregatedTextIndexCreationSpec extends MongoAggregatedTextIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy new file mode 100644 index 00000000000..370be655789 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel.multiple + +import io.micronaut.data.document.mongodb.wildcard.toplevel.multiple.MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec + +class MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy index 39221a2ce72..f7e8d1d676b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -28,6 +28,10 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong ['io.micronaut.data.document.mongodb.text'] } + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + def setupSpec() { applicationContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', @@ -41,7 +45,7 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong def conditions = new PollingConditions(timeout: 10, delay: 0.25) expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'aggregated_text_indexed_entities') assert indexes*.name.contains('aggregated_text_idx') diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index c953cfee042..8fef794e942 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -8,6 +8,7 @@ import io.micronaut.data.mongodb.annotation.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoTextIndexed import io.micronaut.data.mongodb.annotation.MongoWildcardIndex import io.micronaut.data.mongodb.common.MongoEntityIndexes +import org.bson.Document import spock.lang.Shared import spock.lang.Specification @@ -25,6 +26,7 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { index != null index.comment() == 'simple-comment' index.commitQuorum() == null + Document.parse(index.storageEngine()) == Document.parse('{"wiredTiger":{}}') } void 'resolves comment and commitQuorum for compound index'() { @@ -36,6 +38,7 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { index != null index.comment() == 'compound-comment' index.commitQuorum() == 'majority' + Document.parse(index.storageEngine()) == Document.parse('{"wiredTiger":{}}') } void 'resolves comment and commitQuorum for top-level wildcard index'() { @@ -47,6 +50,7 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { index != null index.comment() == 'wildcard-comment' index.commitQuorum() == 'majority' + Document.parse(index.storageEngine()) == Document.parse('{"wiredTiger":{}}') } void 'fails when text indexed fields define different comments'() { @@ -58,6 +62,15 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { e.message.contains('must use the same comment option') } + void 'fails when text indexed fields define different storageEngine options'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidTextStorageEngineEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must use the same storageEngine option') + } + private RuntimePersistentEntity getRuntimePersistentEntity(Class type) { RuntimePersistentEntity entity = entities.get(type) if (entity == null) { @@ -75,7 +88,7 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { @MappedEntity('comment_simple_entity') class CommentSimpleEntity { - @MongoIndexed(name = 'comment_simple_idx', comment = 'simple-comment') + @MongoIndexed(name = 'comment_simple_idx', comment = 'simple-comment', storageEngine = '{ "wiredTiger": {} }') String name } @@ -86,7 +99,8 @@ class CommentSimpleEntity { @MongoCompoundIndexField('city') ], comment = 'compound-comment', - commitQuorum = 'majority' + commitQuorum = 'majority', + storageEngine = '{ "wiredTiger": {} }' ) @MappedEntity('comment_commit_compound_entity') class CommentCommitQuorumCompoundEntity { @@ -94,7 +108,7 @@ class CommentCommitQuorumCompoundEntity { String city } -@MongoWildcardIndex(name = 'comment_commit_wildcard_idx', comment = 'wildcard-comment', commitQuorum = 'majority') +@MongoWildcardIndex(name = 'comment_commit_wildcard_idx', comment = 'wildcard-comment', commitQuorum = 'majority', storageEngine = '{ "wiredTiger": {} }') @MappedEntity('comment_commit_wildcard_entity') class CommentCommitQuorumWildcardEntity { String name @@ -108,3 +122,12 @@ class InvalidTextCommentEntity { @MongoTextIndexed(comment = 'b') String second } + +@MappedEntity('invalid_text_storage_engine_entity') +class InvalidTextStorageEngineEntity { + @MongoTextIndexed(storageEngine = '{ "wiredTiger": {} }') + String first + + @MongoTextIndexed(storageEngine = '{ "wiredTiger": { "configString": "block_compressor=zlib" } }') + String second +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy new file mode 100644 index 00000000000..703f98e4daa --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.reactivestorageengine + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoReactiveStorageEngineValidationSpec extends Specification implements MongoSelectReactiveDriver { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.reactivestorageengine'] + } + + void 'fails fast for invalid storageEngine JSON in reactive mode'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('Mongo storageEngine for entity') + e.message.contains('must be valid JSON') + } +} + +@MongoRepository +interface InvalidReactiveStorageEngineEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_reactive_storage_engine_entities') +class InvalidReactiveStorageEngineEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'invalid_reactive_storage_engine_idx', storageEngine = '{ bad-json }') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy new file mode 100644 index 00000000000..9961f2f9bd8 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.reactivewildcardtoplevelmultiple + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec extends Specification implements MongoSelectReactiveDriver { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.reactivewildcardtoplevelmultiple'] + } + + void 'fails fast when reactive multiple top-level wildcard declarations conflict on options'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('declare conflicting options for key [$**]') + } +} + +@MongoRepository +interface InvalidReactiveTopLevelWildcardMultipleEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.secret": 0 }') +@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.internal": 0 }') +@MappedEntity('invalid_reactive_top_level_wildcard_multiple_entities') +class InvalidReactiveTopLevelWildcardMultipleEntity { + @Id + @GeneratedValue + String id + + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy new file mode 100644 index 00000000000..aa2af857b6d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.storageengine + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoStorageEngineValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.storageengine'] + } + + void 'fails fast for invalid storageEngine JSON'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('Mongo storageEngine for entity') + e.message.contains('must be valid JSON') + } +} + +@MongoRepository +interface InvalidStorageEngineEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_storage_engine_entities') +class InvalidStorageEngineEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'invalid_storage_engine_idx', storageEngine = '{ bad-json }') + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy new file mode 100644 index 00000000000..a513c580b5b --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.wildcardtoplevelmultiple + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoTopLevelWildcardMultipleDeclarationsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.wildcardtoplevelmultiple'] + } + + void 'fails fast when multiple top-level wildcard declarations conflict on options'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('declare conflicting options for key [$**]') + } +} + +@MongoRepository +interface InvalidTopLevelWildcardMultipleEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'invalid_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.secret": 0 }') +@MongoWildcardIndex(name = 'invalid_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.internal": 0 }') +@MappedEntity('invalid_top_level_wildcard_multiple_entities') +class InvalidTopLevelWildcardMultipleEntity { + @Id + @GeneratedValue + String id + + Map metadata +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy new file mode 100644 index 00000000000..aef7a5a50af --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.wildcard.toplevel.multiple + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.wildcard.toplevel.multiple'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'merges equivalent multiple top-level wildcard declarations into a single managed index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_multiple_indexed_entities') + def wildcardIndexes = indexes.findAll { it.fields.size() == 1 && it.fields[0].path() == '$**' } + assert wildcardIndexes.size() == 1 + assert wildcardIndexes[0].name == 'top_level_wildcard_multiple_idx' + } + } +} + +@MongoRepository +interface TopLevelWildcardMultipleIndexedEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') +@MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') +@MappedEntity('top_level_wildcard_multiple_indexed_entities') +class TopLevelWildcardMultipleIndexedEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index b3e779db45a..f541fbc2587 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -21,7 +21,7 @@ Micronaut Data currently supports the following MongoDB index annotations: Use ann:data.mongodb.annotation.MongoIndexed[] to declare standard single-field indexes. -Supported options include direction, `unique`, `sparse`, `hidden`, `expireAfterSeconds` (TTL), `partialFilterExpression`, `collation`, and index-build `comment`. +Supported options include direction, `unique`, `sparse`, `hidden`, `expireAfterSeconds` (TTL), `partialFilterExpression`, `collation`, index-build `comment`, and `storageEngine`. [source,java] ---- @@ -40,7 +40,7 @@ class ProductEntity { Use ann:data.mongodb.annotation.MongoCompoundIndex[] for multi-field index declarations at entity level. -Compound declarations also support index-build `comment` and create-index command `commitQuorum`. +Compound declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. [source,java] ---- @@ -62,7 +62,7 @@ class OrderEntity { } ---- -NOTE: TTL on compound indexes is rejected during startup validation. +NOTE: TTL on compound indexes is rejected during startup validation because MongoDB TTL semantics are single-field oriented. This is fail-fast validation: the application startup fails immediately with a clear error instead of silently creating a non-expiring index definition. == Text indexes @@ -107,10 +107,16 @@ Use ann:data.mongodb.annotation.MongoWildcardIndexed[] for field-level wildcard Use ann:data.mongodb.annotation.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. -Top-level wildcard declarations also support index-build `comment` and create-index command `commitQuorum`. +ann:data.mongodb.annotation.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key are merged into a single managed index definition. + +Top-level wildcard declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. + +NOTE: `storageEngine` must be valid JSON. Invalid declarations are rejected during startup validation (fail-fast). NOTE: MongoDB allows `wildcardProjection` only for top-level wildcard indexes (`{"$**": 1}`). -Using `wildcardProjection` on field-level ann:data.mongodb.annotation.MongoWildcardIndexed[] is rejected during startup validation. +Using `wildcardProjection` on field-level ann:data.mongodb.annotation.MongoWildcardIndexed[] is rejected during startup validation (fail-fast), with an explicit error that points to ann:data.mongodb.annotation.MongoWildcardIndex[]. + +NOTE: If multiple top-level wildcard declarations define conflicting options for `$**`, startup fails fast with an explicit conflict error. == TTL indexes From d9f2668f981fa8b53bbb6572096fabbbc06b1ff0 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 11:45:14 +0200 Subject: [PATCH 05/34] MongoDB indexes support --- .../mongodb/annotation/MongoGeoIndexed.java | 5 + .../mongodb/annotation/MongoTextIndexed.java | 15 + .../mongodb/common/MongoEntityIndexes.java | 97 ++++- .../init/AbstractMongoCollectionsCreator.java | 104 ++++- .../mongodb/init/MongoCollectionsCreator.java | 61 ++- .../init/MongoReactiveCollectionsCreator.java | 61 ++- .../mongodb/MongoIndexInspector.groovy | 4 + .../geo/MongoGeoIndexCreationSpec.groovy | 3 +- ...ongoAggregatedTextIndexCreationSpec.groovy | 7 +- ...goExistingIndexAdvancedConflictSpec.groovy | 389 ++++++++++++++++++ ...MongoExistingIndexCompatibilitySpec.groovy | 158 +++++++ .../MongoExistingIndexConflictSpec.groovy | 150 +++++++ ...ExistingIndexNameReconciliationSpec.groovy | 121 ++++++ ...MongoGeoSphereVersionValidationSpec.groovy | 46 +++ ...oIndexAdvancedOptionsResolutionSpec.groovy | 72 ++++ ...veExistingIndexAdvancedConflictSpec.groovy | 13 + ...ctiveExistingIndexCompatibilitySpec.groovy | 13 + ...goReactiveExistingIndexConflictSpec.groovy | 13 + ...ExistingIndexNameReconciliationSpec.groovy | 13 + ...ctiveGeoSphereVersionValidationSpec.groovy | 18 + ...eactiveTextIndexVersionConflictSpec.groovy | 13 + .../MongoTextIndexVersionConflictSpec.groovy | 73 ++++ .../mongo/mongoMapping/mongoIndexes.adoc | 14 + 23 files changed, 1430 insertions(+), 33 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java index 76be835d65c..743420efc37 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java @@ -59,6 +59,11 @@ */ String storageEngine() default ""; + /** + * @return The 2dsphere index version, or -1 if unset. + */ + int sphereVersion() default -1; + /** * @return The 2d index bits setting, or -1 if unset. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java index 39940175bee..8ce91958d3f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java @@ -58,4 +58,19 @@ * @return The storage engine options as JSON. */ String storageEngine() default ""; + + /** + * @return The text index default language. + */ + String defaultLanguage() default ""; + + /** + * @return The document field that overrides language for text processing. + */ + String languageOverride() default ""; + + /** + * @return The text index version, or -1 if unset. + */ + int textIndexVersion() default -1; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index a8f05068a8d..9b82677abd2 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -96,6 +96,10 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist null, null, null, + null, + null, + null, + null, annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null), parseJsonOption(annotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), @@ -119,6 +123,10 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist || !Objects.equals(first.bits(), candidate.bits()) || !Objects.equals(first.min(), candidate.min()) || !Objects.equals(first.max(), candidate.max()) + || !Objects.equals(first.defaultLanguage(), candidate.defaultLanguage()) + || !Objects.equals(first.languageOverride(), candidate.languageOverride()) + || !Objects.equals(first.textIndexVersion(), candidate.textIndexVersion()) + || !Objects.equals(first.sphereVersion(), candidate.sphereVersion()) || !Objects.equals(first.wildcardProjection(), candidate.wildcardProjection()) || !Objects.equals(first.storageEngine(), candidate.storageEngine()) || !Objects.equals(first.comment(), candidate.comment()) @@ -143,6 +151,10 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist first.bits(), first.min(), first.max(), + first.defaultLanguage(), + first.languageOverride(), + first.textIndexVersion(), + first.sphereVersion(), first.wildcardProjection(), first.storageEngine(), first.comment(), @@ -173,6 +185,10 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null @@ -198,6 +214,10 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), hashedAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null @@ -208,12 +228,16 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? geoAnnotation.intValue("sphereVersion").getAsInt() : null; Integer bits = geoAnnotation.intValue("bits").isPresent() && geoAnnotation.intValue("bits").getAsInt() >= 0 ? geoAnnotation.intValue("bits").getAsInt() : null; Double min = geoAnnotation.doubleValue("min").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("min").getAsDouble()) ? geoAnnotation.doubleValue("min").getAsDouble() : null; Double max = geoAnnotation.doubleValue("max").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("max").getAsDouble()) ? geoAnnotation.doubleValue("max").getAsDouble() : null; if (type != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d indexes on entity [" + entity.getName() + "]"); } + if (type != MongoGeoIndexType.GEO_2DSPHERE && sphereVersion != null) { + throw new IllegalStateException("2dsphere-specific geospatial options are only supported for Mongo 2dsphere indexes on entity [" + entity.getName() + "]"); + } indexes.add(new ResolvedIndex( geoAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(property.getPersistedName(), null, null, type.getKey(), min, max)), @@ -227,6 +251,10 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), geoAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null @@ -252,6 +280,10 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), wildcardAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), null @@ -280,6 +312,9 @@ private static List resolveTextIndexes(RuntimePersistentEntity Boolean hidden = null; String storageEngine = null; String comment = null; + String defaultLanguage = null; + String languageOverride = null; + Integer textIndexVersion = null; BeanIntrospection introspection = entity.getIntrospection(); for (BeanProperty beanProperty : introspection.getBeanProperties()) { RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); @@ -316,13 +351,35 @@ private static List resolveTextIndexes(RuntimePersistentEntity } else if (!Objects.equals(storageEngine, declaredStorageEngine)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same storageEngine option"); } + String declaredDefaultLanguage = textAnnotation.stringValue("defaultLanguage").filter(s -> !s.isEmpty()).orElse(null); + if (defaultLanguage == null) { + defaultLanguage = declaredDefaultLanguage; + } else if (!Objects.equals(defaultLanguage, declaredDefaultLanguage)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same defaultLanguage option"); + } + String declaredLanguageOverride = textAnnotation.stringValue("languageOverride").filter(s -> !s.isEmpty()).orElse(null); + if (languageOverride == null) { + languageOverride = declaredLanguageOverride; + } else if (!Objects.equals(languageOverride, declaredLanguageOverride)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same languageOverride option"); + } + Integer declaredTextIndexVersion = textAnnotation.intValue("textIndexVersion").isPresent() && textAnnotation.intValue("textIndexVersion").getAsInt() >= 0 + ? textAnnotation.intValue("textIndexVersion").getAsInt() : null; + if (declaredTextIndexVersion != null && declaredTextIndexVersion <= 0) { + throw new IllegalStateException("Mongo text index version must be greater than zero for entity [" + entity.getName() + "]"); + } + if (textIndexVersion == null) { + textIndexVersion = declaredTextIndexVersion; + } else if (!Objects.equals(textIndexVersion, declaredTextIndexVersion)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same textIndexVersion option"); + } fields.add(new ResolvedIndexField(property.getPersistedName(), null, weight, "text", null, null)); } } if (fields.isEmpty()) { return List.of(); } - return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, null, storageEngine, comment, null)); + return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, defaultLanguage, languageOverride, textIndexVersion, null, null, storageEngine, comment, null)); } private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { @@ -423,6 +480,10 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit indexMin, indexMax, null, + null, + null, + null, + null, parseJsonOption(annotationValue.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotationValue.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), annotationValue.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) @@ -431,6 +492,19 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit return indexes; } + private static @Nullable String parseJsonOption(@Nullable String json, + String option, + String entityName) { + if (json == null || json.isBlank()) { + return null; + } + try { + return Document.parse(json).toJson(); + } catch (RuntimeException e) { + throw new IllegalStateException("Mongo " + option + " for entity [" + entityName + "] must be valid JSON", e); + } + } + /** * Resolved Mongo index definition. * @@ -445,6 +519,10 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit * @param bits The geospatial bits option for 2d indexes * @param min The geospatial min option for 2d indexes * @param max The geospatial max option for 2d indexes + * @param defaultLanguage The text index default language + * @param languageOverride The text index language override field + * @param textIndexVersion The text index version + * @param sphereVersion The 2dsphere index version * @param wildcardProjection The wildcard projection JSON * @param storageEngine The storage engine options JSON * @param comment The index creation comment @@ -461,25 +539,16 @@ public record ResolvedIndex(@Nullable String name, @Nullable Integer bits, @Nullable Double min, @Nullable Double max, + @Nullable String defaultLanguage, + @Nullable String languageOverride, + @Nullable Integer textIndexVersion, + @Nullable Integer sphereVersion, @Nullable String wildcardProjection, @Nullable String storageEngine, @Nullable String comment, @Nullable String commitQuorum) { } - private static @Nullable String parseJsonOption(@Nullable String json, - String option, - String entityName) { - if (json == null || json.isBlank()) { - return null; - } - try { - return Document.parse(json).toJson(); - } catch (RuntimeException e) { - throw new IllegalStateException("Mongo " + option + " for entity [" + entityName + "] must be valid JSON", e); - } - } - /** * Resolved Mongo index field. * diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index b2e634abe7e..e156d5bb7e3 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -15,6 +15,10 @@ */ package io.micronaut.data.mongodb.init; +import com.mongodb.MongoException; +import com.mongodb.MongoCommandException; +import com.mongodb.MongoWriteException; +import com.mongodb.WriteError; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.configuration.mongo.core.DefaultMongoConfiguration; import io.micronaut.configuration.mongo.core.NamedMongoConfiguration; @@ -64,6 +68,10 @@ public class AbstractMongoCollectionsCreator { private static final Logger LOG = LoggerFactory.getLogger(AbstractMongoCollectionsCreator.class); + private static final int INDEX_OPTIONS_CONFLICT_CODE = 85; + private static final int INDEX_KEY_SPECS_CONFLICT_CODE = 86; + private static final String INDEX_OPTIONS_CONFLICT_CODE_NAME = "IndexOptionsConflict"; + private static final String INDEX_KEY_SPECS_CONFLICT_CODE_NAME = "IndexKeySpecsConflict"; /** * Get MongoDB database factory. @@ -222,7 +230,11 @@ private void createIndexes(DatabaseOperations databaseOperations, for (MongoResolvedIndex desiredIndex : desiredIndexes) { MongoResolvedIndex existingIndex = findMatchingIndex(existingIndexes, desiredIndex); if (existingIndex == null) { - databaseOperations.createIndex(database, collectionName, desiredIndex); + try { + databaseOperations.createIndex(database, collectionName, desiredIndex); + } catch (RuntimeException e) { + throw mapCreateIndexConflict(e, entity, collectionName, desiredIndex); + } continue; } if (!existingIndex.matchesManagedOptions(desiredIndex)) { @@ -263,6 +275,10 @@ private List resolveIndexes(PersistentEntity entity) { index.bits(), index.min(), index.max(), + index.defaultLanguage(), + index.languageOverride(), + index.textIndexVersion(), + index.sphereVersion(), normalizeJsonString(index.wildcardProjection()), normalizeJsonString(index.storageEngine()), index.comment(), @@ -317,6 +333,64 @@ private List resolveIndexes(PersistentEntity entity) { return null; } + private RuntimeException mapCreateIndexConflict(RuntimeException e, + PersistentEntity entity, + String collectionName, + MongoResolvedIndex desiredIndex) { + MongoIndexConflict mongoIndexConflict = findMongoIndexConflict(e); + if (mongoIndexConflict == null) { + return e; + } + return new IllegalStateException("Conflicting existing MongoDB index while creating desired index for entity [" + + entity.getName() + + "] and collection [" + + collectionName + + "]: desired " + + desiredIndex.describe() + + ", mongoErrorCodeName=" + + mongoIndexConflict.errorCodeName() + + ", mongoErrorCode=" + + mongoIndexConflict.errorCode() + + ", mongoMessage=" + + String.valueOf(mongoIndexConflict.message()), e); + } + + private @Nullable MongoIndexConflict findMongoIndexConflict(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof MongoCommandException mongoCommandException) { + int errorCode = mongoCommandException.getErrorCode(); + String errorCodeName = mongoCommandException.getErrorCodeName(); + if (isIndexConflict(errorCode, errorCodeName)) { + return new MongoIndexConflict(errorCode, errorCodeName, mongoCommandException.getMessage()); + } + } + if (current instanceof MongoWriteException mongoWriteException) { + WriteError writeError = mongoWriteException.getError(); + int errorCode = writeError.getCode(); + if (isIndexConflict(errorCode, null)) { + return new MongoIndexConflict(errorCode, null, mongoWriteException.getMessage()); + } + } + if (current instanceof MongoException mongoException) { + int errorCode = mongoException.getCode(); + if (isIndexConflict(errorCode, null)) { + return new MongoIndexConflict(errorCode, null, mongoException.getMessage()); + } + } + current = current.getCause(); + } + return null; + } + + private boolean isIndexConflict(int errorCode, + @Nullable String errorCodeName) { + return errorCode == INDEX_OPTIONS_CONFLICT_CODE + || errorCode == INDEX_KEY_SPECS_CONFLICT_CODE + || INDEX_OPTIONS_CONFLICT_CODE_NAME.equals(errorCodeName) + || INDEX_KEY_SPECS_CONFLICT_CODE_NAME.equals(errorCodeName); + } + /** * The MongoDB database operations provider. * @@ -428,6 +502,11 @@ String describe() { record MongoResolvedIndexField(String path, @Nullable Integer order, @Nullable Integer weight, @Nullable String kind, @Nullable Double min, @Nullable Double max) { } + private record MongoIndexConflict(int errorCode, + @Nullable String errorCodeName, + @Nullable String message) { + } + @Internal record MongoResolvedIndex(@Nullable String name, List fields, @@ -440,6 +519,10 @@ record MongoResolvedIndex(@Nullable String name, @Nullable Integer bits, @Nullable Double min, @Nullable Double max, + @Nullable String defaultLanguage, + @Nullable String languageOverride, + @Nullable Integer textIndexVersion, + @Nullable Integer sphereVersion, @Nullable String wildcardProjection, @Nullable String storageEngine, @Nullable String comment, @@ -456,13 +539,22 @@ boolean matchesManagedOptions(MongoResolvedIndex other) { && Objects.equals(expireAfterSeconds, other.expireAfterSeconds) && Objects.equals(partialFilterExpression, other.partialFilterExpression) && collationMatches(collation, other.collation) - && Objects.equals(bits, other.bits) - && Objects.equals(min, other.min) - && Objects.equals(max, other.max) + && optionMatches(other.bits, bits) + && optionMatches(other.min, min) + && optionMatches(other.max, max) + && optionMatches(other.defaultLanguage, defaultLanguage) + && optionMatches(other.languageOverride, languageOverride) + && optionMatches(other.textIndexVersion, textIndexVersion) + && optionMatches(other.sphereVersion, sphereVersion) && Objects.equals(wildcardProjection, other.wildcardProjection) && Objects.equals(storageEngine, other.storageEngine); } + private static boolean optionMatches(@Nullable T desired, + @Nullable T existing) { + return desired == null || Objects.equals(desired, existing); + } + private static boolean collationMatches(@Nullable String existingCollation, @Nullable String desiredCollation) { if (desiredCollation == null) { @@ -511,6 +603,10 @@ String describe() { + ", bits=" + bits + ", min=" + min + ", max=" + max + + ", defaultLanguage=" + defaultLanguage + + ", languageOverride=" + languageOverride + + ", textIndexVersion=" + textIndexVersion + + ", sphereVersion=" + sphereVersion + ", wildcardProjection=" + wildcardProjection + ", storageEngine=" + storageEngine + ", comment=" + comment diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 328b84b71cd..d0481b6417e 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -139,13 +139,34 @@ public List listIndexes(MongoDatabase database, String colle if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { continue; } - List fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + List fields; + if ("text".equals(keyDocument.getString("_fts"))) { + Document weights = indexDocument.get("weights", Document.class); + if (weights != null && !weights.isEmpty()) { + fields = new ArrayList<>(weights.size()); + for (Map.Entry entry : weights.entrySet()) { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); + } } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } + } + } + } else { + fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } } } indexes.add(new MongoResolvedIndex( @@ -160,6 +181,10 @@ public List listIndexes(MongoDatabase database, String colle toInteger(indexDocument.get("bits")), toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), + indexDocument.getString("default_language"), + indexDocument.getString("language_override"), + toInteger(indexDocument.get("textIndexVersion")), + toInteger(indexDocument.get("2dsphereIndexVersion")), normalizeJsonValue(indexDocument.get("wildcardProjection")), normalizeJsonValue(indexDocument.get("storageEngine")), null, @@ -209,6 +234,18 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.max() != null) { indexOptions.max(index.max()); } + if (index.defaultLanguage() != null) { + indexOptions.defaultLanguage(index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexOptions.languageOverride(index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexOptions.textVersion(index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexOptions.sphereVersion(index.sphereVersion()); + } if (index.wildcardProjection() != null) { indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); } @@ -253,6 +290,18 @@ private Document toIndexCommandDocument(MongoResolvedIndex index) { if (index.max() != null) { indexDocument.append("max", index.max()); } + if (index.defaultLanguage() != null) { + indexDocument.append("default_language", index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexDocument.append("language_override", index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexDocument.append("textIndexVersion", index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexDocument.append("2dsphereIndexVersion", index.sphereVersion()); + } if (index.wildcardProjection() != null) { indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index c3399071c2b..9c30be723ce 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -145,13 +145,34 @@ public List listIndexes(MongoDatabase database, String colle if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { continue; } - List fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + List fields; + if ("text".equals(keyDocument.getString("_fts"))) { + Document weights = indexDocument.get("weights", Document.class); + if (weights != null && !weights.isEmpty()) { + fields = new ArrayList<>(weights.size()); + for (Map.Entry entry : weights.entrySet()) { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); + } } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } + } + } + } else { + fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } } } resolvedIndexes.add(new MongoResolvedIndex( @@ -166,6 +187,10 @@ public List listIndexes(MongoDatabase database, String colle toInteger(indexDocument.get("bits")), toDouble(indexDocument.get("min")), toDouble(indexDocument.get("max")), + indexDocument.getString("default_language"), + indexDocument.getString("language_override"), + toInteger(indexDocument.get("textIndexVersion")), + toInteger(indexDocument.get("2dsphereIndexVersion")), normalizeJsonValue(indexDocument.get("wildcardProjection")), normalizeJsonValue(indexDocument.get("storageEngine")), null, @@ -218,6 +243,18 @@ public void createIndex(MongoDatabase database, String collection, MongoResolved if (index.max() != null) { indexOptions.max(index.max()); } + if (index.defaultLanguage() != null) { + indexOptions.defaultLanguage(index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexOptions.languageOverride(index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexOptions.textVersion(index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexOptions.sphereVersion(index.sphereVersion()); + } if (index.wildcardProjection() != null) { indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); } @@ -262,6 +299,18 @@ private Document toIndexCommandDocument(MongoResolvedIndex index) { if (index.max() != null) { indexDocument.append("max", index.max()); } + if (index.defaultLanguage() != null) { + indexDocument.append("default_language", index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexDocument.append("language_override", index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexDocument.append("textIndexVersion", index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexDocument.append("2dsphereIndexVersion", index.sphereVersion()); + } if (index.wildcardProjection() != null) { indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy index cc198fb4240..850951482d7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy @@ -39,6 +39,10 @@ final class MongoIndexInspector { wildcardProjection : indexDocument.get('wildcardProjection'), min : indexDocument.get('min'), max : indexDocument.get('max'), + defaultLanguage : indexDocument.getString('default_language'), + languageOverride : indexDocument.getString('language_override'), + textIndexVersion : indexDocument.getInteger('textIndexVersion'), + sphereVersion : indexDocument.getInteger('2dsphereIndexVersion'), ] } indexes diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy index 704da6c30f7..add0a2938bb 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy @@ -54,6 +54,7 @@ class MongoGeoIndexCreationSpec extends Specification implements MongoTestProper assert index.fields[0].path() == 'location' assert index.fields[0].order() == null assert index.fields[0].kind() == '2dsphere' + assert index.sphereVersion == 3 } } } @@ -68,6 +69,6 @@ class GeoIndexedEntity { @GeneratedValue String id - @MongoGeoIndexed(name = 'geo_location_idx') + @MongoGeoIndexed(name = 'geo_location_idx', sphereVersion = 3) Map location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy index f7e8d1d676b..5bf7b53ca5a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -53,6 +53,9 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong assert index.fields.size() == 2 assert index.fields*.path().contains('_fts') assert index.fields*.path().contains('_ftsx') + assert index.defaultLanguage == 'french' + assert index.languageOverride == 'lang' + assert index.textIndexVersion == 3 } } } @@ -67,9 +70,9 @@ class AggregatedTextIndexedEntity { @GeneratedValue String id - @MongoTextIndexed(name = 'aggregated_text_idx', weight = 2) + @MongoTextIndexed(name = 'aggregated_text_idx', weight = 2, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) String title - @MongoTextIndexed(name = 'aggregated_text_idx', weight = 5) + @MongoTextIndexed(name = 'aggregated_text_idx', weight = 5, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) String description } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy new file mode 100644 index 00000000000..a899f19774f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy @@ -0,0 +1,389 @@ +package io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingIndexAdvancedConflictSpec extends Specification implements MongoTestPropertyProvider { + + private static final List CONFLICT_COLLECTIONS = [ + 'existing_hidden_conflict_index_entities', + 'existing_sparse_conflict_index_entities', + 'existing_expire_conflict_index_entities', + 'existing_sphere_conflict_index_entities', + 'existing_geo2d_bits_conflict_index_entities', + 'existing_geo2d_minmax_conflict_index_entities', + 'existing_text_default_language_conflict_entities', + 'existing_text_language_override_conflict_entities', + 'existing_wildcard_projection_conflict_entities', + 'existing_storage_engine_conflict_entities' + ] + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict'] + } + + void 'fails fast when existing index has conflicting hidden option'() { + given: + prepareExistingIndex('existing_hidden_conflict_index_entities', + new Document('key', new Document('name', 1)) + .append('name', 'existing_hidden_conflict_idx') + .append('hidden', false) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_hidden_conflict_idx', 'hidden') + } + + void 'fails fast when existing index has conflicting sparse option'() { + given: + prepareExistingIndex('existing_sparse_conflict_index_entities', + new Document('key', new Document('code', 1)) + .append('name', 'existing_sparse_conflict_idx') + .append('sparse', false) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_sparse_conflict_idx', 'sparse') + } + + void 'fails fast when existing index has conflicting expireAfterSeconds option'() { + given: + prepareExistingIndex('existing_expire_conflict_index_entities', + new Document('key', new Document('expires_at', 1)) + .append('name', 'existing_expire_conflict_idx') + .append('expireAfterSeconds', 120) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_expire_conflict_idx', 'expireAfterSeconds') + } + + void 'fails fast when existing 2dsphere index has conflicting sphereVersion option'() { + given: + prepareExistingIndex('existing_sphere_conflict_index_entities', + new Document('key', new Document('location', '2dsphere')) + .append('name', 'existing_sphere_conflict_idx') + .append('2dsphereIndexVersion', 2) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_sphere_conflict_idx', 'sphereVersion') + } + + void 'fails fast when existing 2d index has conflicting bits option'() { + given: + prepareExistingIndex('existing_geo2d_bits_conflict_index_entities', + new Document('key', new Document('coordinates', '2d')) + .append('name', 'existing_geo2d_bits_conflict_idx') + .append('bits', 28) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_geo2d_bits_conflict_idx', 'bits') + } + + void 'fails fast when existing 2d index has conflicting min and max options'() { + given: + prepareExistingIndex('existing_geo2d_minmax_conflict_index_entities', + new Document('key', new Document('coordinates', '2d')) + .append('name', 'existing_geo2d_minmax_conflict_idx') + .append('bits', 26) + .append('min', -180d) + .append('max', 180d) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_geo2d_minmax_conflict_idx', 'min') + e.message.contains('max') + } + + void 'fails fast when existing text index has conflicting defaultLanguage option'() { + given: + prepareExistingIndex('existing_text_default_language_conflict_entities', + new Document('key', new Document('title', 'text')) + .append('name', 'existing_text_default_language_conflict_idx') + .append('default_language', 'english') + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_text_default_language_conflict_idx', 'defaultLanguage') + } + + void 'fails fast when existing text index has conflicting languageOverride option'() { + given: + prepareExistingIndex('existing_text_language_override_conflict_entities', + new Document('key', new Document('content', 'text')) + .append('name', 'existing_text_language_override_conflict_idx') + .append('default_language', 'spanish') + .append('language_override', 'docLang') + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_text_language_override_conflict_idx', 'languageOverride') + } + + void 'fails fast when existing wildcard index has conflicting wildcardProjection option'() { + given: + prepareExistingIndex('existing_wildcard_projection_conflict_entities', + new Document('key', new Document('$**', 1)) + .append('name', 'existing_wildcard_projection_conflict_idx') + .append('wildcardProjection', new Document('metadata.secret', 0)) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_wildcard_projection_conflict_idx', 'wildcardProjection') + } + + void 'fails fast when existing index has conflicting storageEngine option'() { + given: + prepareExistingIndex('existing_storage_engine_conflict_entities', + new Document('key', new Document('code', 1)) + .append('name', 'existing_storage_engine_conflict_idx') + .append('storageEngine', new Document('wiredTiger', new Document('configString', 'block_compressor=zlib'))) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_storage_engine_conflict_idx', 'storageEngine') + } + + protected void prepareExistingIndex(String collectionName, Document index) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false' + ]) + try { + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + CONFLICT_COLLECTIONS.each { database.getCollection(it).drop() } + def collection = database.getCollection(collectionName) + database.runCommand(new Document('createIndexes', collectionName) + .append('indexes', [index])) + + def existing = collection.listIndexes().find { it.getString('name') == index.getString('name') } + assert existing != null + } finally { + preContext.close() + } + } + + protected static void assertManagedConflict(RuntimeException e, String indexName, String optionToken) { + assert e.message != null + assert e.message.contains('Conflicting existing MongoDB index') + assert e.message.contains('desired MongoResolvedIndex') + assert e.message.contains(indexName) + assert e.message.contains(optionToken) + } +} + +@MongoRepository +interface ExistingHiddenConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_hidden_conflict_index_entities') +class ExistingHiddenConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_hidden_conflict_idx', hidden = true) + String name +} + +@MongoRepository +interface ExistingSparseConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_sparse_conflict_index_entities') +class ExistingSparseConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_sparse_conflict_idx', sparse = true) + String code +} + +@MongoRepository +interface ExistingExpireConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_expire_conflict_index_entities') +class ExistingExpireConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_expire_conflict_idx', expireAfterSeconds = 60) + Date expiresAt +} + +@MongoRepository +interface ExistingSphereConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_sphere_conflict_index_entities') +class ExistingSphereConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'existing_sphere_conflict_idx', sphereVersion = 3) + Map location +} + +@MongoRepository +interface ExistingGeo2dBitsConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_geo2d_bits_conflict_index_entities') +class ExistingGeo2dBitsConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'existing_geo2d_bits_conflict_idx', type = MongoGeoIndexType.GEO_2D, bits = 30) + Map coordinates +} + +@MongoRepository +interface ExistingGeo2dMinMaxConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_geo2d_minmax_conflict_index_entities') +class ExistingGeo2dMinMaxConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'existing_geo2d_minmax_conflict_idx', type = MongoGeoIndexType.GEO_2D, bits = 26, min = -90d, max = 90d) + Map coordinates +} + +@MongoRepository +interface ExistingTextDefaultLanguageConflictEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_text_default_language_conflict_entities') +class ExistingTextDefaultLanguageConflictEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'existing_text_default_language_conflict_idx', defaultLanguage = 'spanish') + String title +} + +@MongoRepository +interface ExistingTextLanguageOverrideConflictEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_text_language_override_conflict_entities') +class ExistingTextLanguageOverrideConflictEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'existing_text_language_override_conflict_idx', defaultLanguage = 'spanish', languageOverride = 'docLocale') + String content +} + +@MongoRepository +interface ExistingWildcardProjectionConflictEntityRepository extends CrudRepository { +} + +@MongoWildcardIndex(name = 'existing_wildcard_projection_conflict_idx', wildcardProjection = '{ "metadata.internal": 0 }') +@MappedEntity('existing_wildcard_projection_conflict_entities') +class ExistingWildcardProjectionConflictEntity { + @Id + @GeneratedValue + String id + + Map metadata +} + +@MongoRepository +interface ExistingStorageEngineConflictEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_storage_engine_conflict_entities') +class ExistingStorageEngineConflictEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_storage_engine_conflict_idx', storageEngine = '{ "wiredTiger": {} }') + String code +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy new file mode 100644 index 00000000000..b8c1684391d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy @@ -0,0 +1,158 @@ +package io.micronaut.data.document.mongodb.validation.existingindexcompatibility + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingIndexCompatibilitySpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingindexcompatibility'] + } + + void 'starts successfully when matching simple index already exists'() { + given: + prepareExistingIndex('existing_simple_index_entities', + new Document('key', new Document('name', 1)) + .append('name', 'existing_name_idx') + .append('unique', true) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + + void 'starts successfully when matching compound index already exists'() { + given: + prepareExistingIndex('existing_compound_index_entities', + new Document('key', new Document('name', 1).append('age', -1)) + .append('name', 'existing_name_age_idx') + .append('unique', true) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + + void 'starts successfully when matching 2dsphere index already exists'() { + given: + prepareExistingIndex('existing_geo_index_entities', + new Document('key', new Document('location', '2dsphere')) + .append('name', 'existing_geo_location_idx') + .append('2dsphereIndexVersion', 3) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + + protected void prepareExistingIndex(String collectionName, Document index) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false' + ]) + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + def collection = database.getCollection(collectionName) + collection.drop() + database.runCommand(new Document('createIndexes', collectionName) + .append('indexes', [index])) + + def existing = collection.listIndexes().find { it.getString('name') == index.getString('name') } + assert existing != null + preContext.close() + } +} + +@MongoRepository +interface ExistingSimpleIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_simple_index_entities') +class ExistingSimpleIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_name_idx', unique = true) + String name +} + +@MongoRepository +interface ExistingCompoundIndexEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'existing_name_age_idx', + unique = true, + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'age', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('existing_compound_index_entities') +class ExistingCompoundIndexEntity { + @Id + @GeneratedValue + String id + + String name + + Integer age +} + +@MongoRepository +interface ExistingGeoIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_geo_index_entities') +class ExistingGeoIndexEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'existing_geo_location_idx', sphereVersion = 3) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy new file mode 100644 index 00000000000..2a4d68bdc50 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy @@ -0,0 +1,150 @@ +package io.micronaut.data.document.mongodb.validation.existingindexconflict + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingIndexConflictSpec extends Specification implements MongoTestPropertyProvider { + + private static final List CONFLICT_COLLECTIONS = [ + 'existing_unique_conflict_index_entities', + 'existing_collation_conflict_index_entities', + 'existing_partial_filter_conflict_index_entities' + ] + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingindexconflict'] + } + + void 'fails fast when existing index has conflicting unique option'() { + given: + prepareExistingIndex('existing_unique_conflict_index_entities', + new Document('key', new Document('email', 1)) + .append('name', 'existing_unique_conflict_idx') + .append('unique', false) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_unique_conflict_idx', 'unique') + } + + void 'fails fast when existing index has conflicting collation option'() { + given: + prepareExistingIndex('existing_collation_conflict_index_entities', + new Document('key', new Document('name', 1)) + .append('name', 'existing_collation_conflict_idx') + .append('collation', new Document('locale', 'fr').append('strength', 2)) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_collation_conflict_idx', 'collation') + } + + void 'fails fast when existing index has conflicting partialFilterExpression option'() { + given: + prepareExistingIndex('existing_partial_filter_conflict_index_entities', + new Document('key', new Document('status', 1)) + .append('name', 'existing_partial_filter_conflict_idx') + .append('partialFilterExpression', new Document('status', new Document('$eq', 'ARCHIVED'))) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'existing_partial_filter_conflict_idx', 'partialFilterExpression') + } + + protected void prepareExistingIndex(String collectionName, Document index) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false' + ]) + try { + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + CONFLICT_COLLECTIONS.each { database.getCollection(it).drop() } + def collection = database.getCollection(collectionName) + database.runCommand(new Document('createIndexes', collectionName) + .append('indexes', [index])) + + def existing = collection.listIndexes().find { it.getString('name') == index.getString('name') } + assert existing != null + } finally { + preContext.close() + } + } + + protected static void assertManagedConflict(RuntimeException e, String indexName, String optionToken) { + assert e.message != null + assert e.message.contains('Conflicting existing MongoDB index') + assert e.message.contains('desired MongoResolvedIndex') + assert e.message.contains(indexName) + assert e.message.contains(optionToken) + } +} + +@MongoRepository +interface ExistingUniqueConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_unique_conflict_index_entities') +class ExistingUniqueConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_unique_conflict_idx', unique = true) + String email +} + +@MongoRepository +interface ExistingCollationConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_collation_conflict_index_entities') +class ExistingCollationConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_collation_conflict_idx', collation = '{ "locale": "en", "strength": 2 }') + String name +} + +@MongoRepository +interface ExistingPartialFilterConflictIndexEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_partial_filter_conflict_index_entities') +class ExistingPartialFilterConflictIndexEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_partial_filter_conflict_idx', partialFilterExpression = '{ "status": { "$eq": "ACTIVE" } }') + String status +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy new file mode 100644 index 00000000000..b19802dd8a6 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy @@ -0,0 +1,121 @@ +package io.micronaut.data.document.mongodb.validation.existingindexname + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingIndexNameReconciliationSpec extends Specification implements MongoTestPropertyProvider { + + private static final List NAME_COLLECTIONS = [ + 'existing_name_conflict_index_entities', + 'existing_name_reconciliation_index_entities' + ] + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingindexname'] + } + + void 'fails fast when existing index name differs for same key'() { + given: + prepareExistingIndex('existing_name_conflict_index_entities', + new Document('key', new Document('email', 1)) + .append('name', 'existing_name_idx') + .append('unique', true) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertNameConflict(e, 'existing_name_conflict_entities_idx', 'existing_name_idx') + } + + void 'starts successfully when desired index is unnamed and existing index has name for same key'() { + given: + prepareExistingIndex('existing_name_reconciliation_index_entities', + new Document('key', new Document('username', 1)) + .append('name', 'existing_named_username_idx') + .append('unique', true) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + + protected void prepareExistingIndex(String collectionName, Document index) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false' + ]) + try { + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + NAME_COLLECTIONS.each { database.getCollection(it).drop() } + def collection = database.getCollection(collectionName) + database.runCommand(new Document('createIndexes', collectionName) + .append('indexes', [index])) + + def existing = collection.listIndexes().find { it.getString('name') == index.getString('name') } + assert existing != null + } finally { + preContext.close() + } + } + + protected static void assertNameConflict(RuntimeException e, String desiredName, String existingName) { + assert e.message != null + assert e.message.contains('Conflicting existing MongoDB index name') + assert e.message.contains('desired MongoResolvedIndex') + assert e.message.contains(desiredName) + assert e.message.contains(existingName) + } +} + +@MongoRepository +interface ExistingNameConflictEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_name_conflict_index_entities') +class ExistingNameConflictEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(name = 'existing_name_conflict_entities_idx', unique = true) + String email +} + +@MongoRepository +interface ExistingNameReconciliationEntityRepository extends CrudRepository { +} + +@MappedEntity('existing_name_reconciliation_index_entities') +class ExistingNameReconciliationEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed(unique = true) + String username +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy new file mode 100644 index 00000000000..ae03640295e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.data.document.mongodb.validation.geosphereversion + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoGeoSphereVersionValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geosphereversion'] + } + + void 'fails fast when sphereVersion is used on non-2dsphere geospatial index'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('2dsphere-specific geospatial options are only supported for Mongo 2dsphere indexes') + } +} + +@MongoRepository +interface InvalidGeoSphereVersionEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_geo_sphere_version_entities') +class InvalidGeoSphereVersionEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'invalid_geo_sphere_version_idx', type = MongoGeoIndexType.GEO_2D, sphereVersion = 3) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index 8fef794e942..44aeff284f4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -4,6 +4,8 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.model.runtime.RuntimePersistentEntity import io.micronaut.data.mongodb.annotation.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoTextIndexed import io.micronaut.data.mongodb.annotation.MongoWildcardIndex @@ -71,6 +73,46 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { e.message.contains('must use the same storageEngine option') } + void 'resolves text default language, override, and text index version'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(TextLanguageOptionsEntity)).indexes + def index = indexes.find { it.name() == 'text_lang_idx' } + + then: + index != null + index.defaultLanguage() == 'french' + index.languageOverride() == 'lang' + index.textIndexVersion() == 3 + } + + void 'fails when text indexed fields define different defaultLanguage options'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidTextDefaultLanguageEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must use the same defaultLanguage option') + } + + void 'resolves 2dsphere sphereVersion'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(GeoSphereVersionEntity)).indexes + def index = indexes.find { it.name() == 'geo_sphere_version_idx' } + + then: + index != null + index.sphereVersion() == 3 + } + + void 'fails when 2dsphere sphereVersion is used on non-2dsphere index type'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidGeoSphereVersionOn2dEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('2dsphere-specific geospatial options are only supported for Mongo 2dsphere indexes') + } + private RuntimePersistentEntity getRuntimePersistentEntity(Class type) { RuntimePersistentEntity entity = entities.get(type) if (entity == null) { @@ -131,3 +173,33 @@ class InvalidTextStorageEngineEntity { @MongoTextIndexed(storageEngine = '{ "wiredTiger": { "configString": "block_compressor=zlib" } }') String second } + +@MappedEntity('text_language_options_entity') +class TextLanguageOptionsEntity { + @MongoTextIndexed(name = 'text_lang_idx', weight = 2, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) + String title + + @MongoTextIndexed(name = 'text_lang_idx', weight = 5, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) + String description +} + +@MappedEntity('invalid_text_default_language_entity') +class InvalidTextDefaultLanguageEntity { + @MongoTextIndexed(defaultLanguage = 'english') + String first + + @MongoTextIndexed(defaultLanguage = 'spanish') + String second +} + +@MappedEntity('geo_sphere_version_entity') +class GeoSphereVersionEntity { + @MongoGeoIndexed(name = 'geo_sphere_version_idx', sphereVersion = 3) + Map location +} + +@MappedEntity('invalid_geo_sphere_version_on_2d_entity') +class InvalidGeoSphereVersionOn2dEntity { + @MongoGeoIndexed(name = 'invalid_geo_sphere_version_idx', type = MongoGeoIndexType.GEO_2D, sphereVersion = 3) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy new file mode 100644 index 00000000000..39750feb493 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingindexadvancedconflict + +import io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict.MongoExistingIndexAdvancedConflictSpec + +class MongoReactiveExistingIndexAdvancedConflictSpec extends MongoExistingIndexAdvancedConflictSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy new file mode 100644 index 00000000000..786da52f0d0 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingindexcompatibility + +import io.micronaut.data.document.mongodb.validation.existingindexcompatibility.MongoExistingIndexCompatibilitySpec + +class MongoReactiveExistingIndexCompatibilitySpec extends MongoExistingIndexCompatibilitySpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy new file mode 100644 index 00000000000..70c51413e6c --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingindexconflict + +import io.micronaut.data.document.mongodb.validation.existingindexconflict.MongoExistingIndexConflictSpec + +class MongoReactiveExistingIndexConflictSpec extends MongoExistingIndexConflictSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy new file mode 100644 index 00000000000..de758d71421 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingindexname + +import io.micronaut.data.document.mongodb.validation.existingindexname.MongoExistingIndexNameReconciliationSpec + +class MongoReactiveExistingIndexNameReconciliationSpec extends MongoExistingIndexNameReconciliationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy new file mode 100644 index 00000000000..c6e731ae475 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.validation.reactivegeosphereversion + +import io.micronaut.data.document.mongodb.validation.geosphereversion.MongoGeoSphereVersionValidationSpec + +class MongoReactiveGeoSphereVersionValidationSpec extends MongoGeoSphereVersionValidationSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geosphereversion'] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy new file mode 100644 index 00000000000..8d5ea801601 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactivetextversionconflict + +import io.micronaut.data.document.mongodb.validation.textversionconflict.MongoTextIndexVersionConflictSpec + +class MongoReactiveTextIndexVersionConflictSpec extends MongoTextIndexVersionConflictSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy new file mode 100644 index 00000000000..ec264dd3b36 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.validation.textversionconflict + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoTextIndexVersionConflictSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.textversionconflict'] + } + + void 'fails fast when existing text index has conflicting textIndexVersion'() { + given: + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false' + ]) + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + def collection = database.getCollection('text_index_version_conflict_entities') + collection.drop() + database.runCommand(new Document('createIndexes', 'text_index_version_conflict_entities') + .append('indexes', [new Document('key', new Document('title', 'text')) + .append('name', 'text_version_idx') + .append('default_language', 'spanish') + .append('textIndexVersion', 2)])) + + def existing = collection.listIndexes().find { it.getString('name') == 'text_version_idx' } + assert existing != null + assert existing.getInteger('textIndexVersion') == 2 + preContext.close() + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assertManagedConflict(e, 'text_version_idx', 'textIndexVersion') + } + + protected static void assertManagedConflict(RuntimeException e, String indexName, String optionToken) { + assert e.message != null + assert e.message.contains('Conflicting existing MongoDB index') + assert e.message.contains('desired MongoResolvedIndex') + assert e.message.contains(indexName) + assert e.message.contains(optionToken) + } +} + +@MongoRepository +interface TextIndexVersionConflictEntityRepository extends CrudRepository { +} + +@MappedEntity('text_index_version_conflict_entities') +class TextIndexVersionConflictEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'text_version_idx', defaultLanguage = 'spanish', textIndexVersion = 3) + String title +} diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index f541fbc2587..fdd508763f4 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -70,6 +70,10 @@ Use ann:data.mongodb.annotation.MongoTextIndexed[] on text fields. Multiple text-indexed fields on the same entity are aggregated into the corresponding MongoDB text index definition. +Text declarations also support `defaultLanguage`, `languageOverride`, and `textIndexVersion`. + +NOTE: For aggregated multi-field text indexes, these text-specific options must be declared consistently across all participating ann:data.mongodb.annotation.MongoTextIndexed[] fields; conflicting declarations fail fast during startup validation. + == Hashed indexes Use ann:data.mongodb.annotation.MongoHashedIndexed[] to create hashed indexes. @@ -78,6 +82,10 @@ Use ann:data.mongodb.annotation.MongoHashedIndexed[] to create hashed indexes. Use ann:data.mongodb.annotation.MongoGeoIndexed[] for geospatial indexing (`2d` and `2dsphere`). +For `2dsphere` indexes, ann:data.mongodb.annotation.MongoGeoIndexed[] also supports `sphereVersion`. + +NOTE: `sphereVersion` is only valid for `2dsphere` indexes; declaring it for other geospatial kinds is rejected during startup validation (fail-fast). + Modeled geospatial values are supported for `MongoGeoPoint`, `MongoGeoMultiPoint`, `MongoGeoLineString`, `MongoGeoMultiLineString`, `MongoGeoPolygon`, `MongoGeoMultiPolygon`, `MongoGeoGeometryCollection`, and custom point-like modeled types. For properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[], Micronaut Data now applies `MongoGeoPointConverter` implicitly when no explicit `@MappedProperty(converter=...)` is declared. @@ -162,4 +170,10 @@ Clustered collection support follows MongoDB collection-level rules: When `create-indexes` is enabled, Micronaut Data checks existing collection indexes before creating new ones. If an index with the same key exists but managed options differ, startup fails fast with an explicit conflict message. +Managed reconciliation is key-first and option-aware: + +* existing indexes with matching key + managed options are accepted (startup continues) +* if a desired explicit name differs from an existing same-key index name, startup fails fast with an explicit name-conflict message +* if a desired name is omitted, existing same-key indexes are matched by key/options (generated or existing names are not used as primary identity) + For clustered declarations, existing collection options are also compared, and conflicting clustered options fail fast. From 14b1f1fc41c2baaed9166e9e052699e61ad83c3c Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 14:41:58 +0200 Subject: [PATCH 06/34] Branch build trigger --- .github/workflows/graalvm-latest.yml | 1 + .github/workflows/gradle.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml index 24405d031dd..39af250df25 100644 --- a/.github/workflows/graalvm-latest.yml +++ b/.github/workflows/graalvm-latest.yml @@ -9,6 +9,7 @@ on: branches: - master - '[0-9]+.[0-9]+.x' + - radovanradic/mongodb-indexes pull_request: branches: - master diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 605280bba09..a44ed30babb 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,6 +9,7 @@ on: branches: - master - '[0-9]+.[0-9]+.x' + - radovanradic/mongodb-indexes pull_request: branches: - master From e8f735f2d0ef0bad5ef99198059982636f5768d5 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 14:42:47 +0200 Subject: [PATCH 07/34] Support $text criteria --- .../query/builder/MongoQueryBuilder.java | 34 ++++++++++ .../PersistentEntityCriteriaBuilder.java | 24 +++++++ .../impl/AbstractCriteriaBuilder.java | 14 ++++ .../jpa/criteria/impl/CriteriaUtils.java | 15 +++++ .../jpa/criteria/impl/PredicateVisitor.java | 3 + .../impl/predicate/TextPredicate.java | 65 +++++++++++++++++++ .../model/jpa/criteria/impl/util/Joiner.java | 15 +++++ .../sql/AbstractSqlLikeQueryBuilder.java | 6 ++ .../query/impl/AdvancedPredicateVisitor.java | 6 ++ .../document/mongodb/MongoCriteriaSpec.groovy | 28 +++++++- 10 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java index d66118770c6..6e46129f554 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java @@ -54,6 +54,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; import io.micronaut.data.model.naming.NamingStrategy; @@ -119,6 +120,8 @@ public final class MongoQueryBuilder implements QueryBuilder { private static final String REGEX = "$regex"; private static final String NOT = "$not"; private static final String OPTIONS = "$options"; + private static final String TEXT = "$text"; + private static final String SEARCH = "$search"; @Nullable @Override @@ -1057,6 +1060,9 @@ private void visitDisjunctionPredicate(Collection @Override public void visit(NegatedPredicate negate) { IExpression negated = negate.getNegated(); + if (negated instanceof TextPredicate) { + throw new UnsupportedOperationException("MongoDB does not support negating a $text predicate."); + } if (negated instanceof InPredicate p) { visitIn(p.getExpression(), p.getValues(), true); return; @@ -1139,6 +1145,22 @@ public void visit(LikePredicate likePredicate) { pattern, true); } + @Override + public void visit(TextPredicate textPredicate) { + LinkedHashMap textClause = new LinkedHashMap<>(4); + textClause.put(SEARCH, valueRepresentation(textPredicate.getSearch())); + if (textPredicate.getLanguage() != null) { + textClause.put("$language", valueRepresentation(textPredicate.getLanguage())); + } + if (textPredicate.getCaseSensitive() != null) { + textClause.put("$caseSensitive", valueRepresentation(textPredicate.getCaseSensitive())); + } + if (textPredicate.getDiacriticSensitive() != null) { + textClause.put("$diacriticSensitive", valueRepresentation(textPredicate.getDiacriticSensitive())); + } + query.put(TEXT, textClause); + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { throw new UnsupportedOperationException("ExistsSubquery is not supported by this implementation."); @@ -1356,6 +1378,18 @@ private void handleRegexExpression(Expression leftExpression, query.put(getPropertyPersistName(propertyPath), filterValue); } + @Nullable + private Object valueRepresentation(Expression value) { + if (value instanceof LiteralExpression literalExpression) { + return literalExpression.getValue(); + } + if (value instanceof BindingParameter bindingParameter) { + int index = queryState.pushParameter(bindingParameter, newBindingContext(null, null)); + return Map.of(QUERY_PARAMETER_PLACEHOLDER, index); + } + return value; + } + @Nullable private Object valueRepresentation(PropertyParameterCreator parameterCreator, Expression leftExpression, @Nullable Object value) { PersistentPropertyPath propertyPath = requireProperty(leftExpression).getPropertyPath(); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 4f29ca3e7f4..22f00173839 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -16,6 +16,7 @@ package io.micronaut.data.model.jpa.criteria; import io.micronaut.core.annotation.Experimental; +import org.jspecify.annotations.Nullable; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; @@ -226,4 +227,27 @@ default Predicate ilike(Expression x, String pattern) { * @since 3.9.0 */ Predicate arrayContains(Expression x, Expression y); + + Predicate text(Expression search); + + Predicate text(Expression search, + @Nullable Expression language, + @Nullable Expression caseSensitive, + @Nullable Expression diacriticSensitive); + + default Predicate text(String search) { + return text(literal(search)); + } + + default Predicate text(String search, + @Nullable String language, + @Nullable Boolean caseSensitive, + @Nullable Boolean diacriticSensitive) { + return text( + literal(search), + language == null ? null : literal(language), + caseSensitive == null ? null : literal(caseSensitive), + diacriticSensitive == null ? null : literal(diacriticSensitive) + ); + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java index 2ec1ede8ed9..b3ec3bf1b3a 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java @@ -35,6 +35,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateUnaryOp; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpressionType; import jakarta.persistence.Tuple; @@ -1472,6 +1473,19 @@ public Predicate arrayContains(Expression x, Expression y) { return predicate(x, y, PredicateBinaryOp.ARRAY_CONTAINS); } + @Override + public Predicate text(Expression search) { + return text(search, null, null, null); + } + + @Override + public Predicate text(Expression search, + @Nullable Expression language, + @Nullable Expression caseSensitive, + @Nullable Expression diacriticSensitive) { + return new TextPredicate(search, language, caseSensitive, diacriticSensitive); + } + @Override public Expression localDate() { throw notSupportedOperation(); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java index 6eb0d7f084d..17c99ce4929 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java @@ -27,6 +27,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Subquery; @@ -200,6 +201,20 @@ private static void extractPredicateParameters(Expression predicate, Set { + if (textPredicate.getSearch() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (textPredicate.getLanguage() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (textPredicate.getCaseSensitive() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (textPredicate.getDiacriticSensitive() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + } case ConjunctionPredicate conjunctionPredicate -> { for (IExpression pred : conjunctionPredicate.getPredicates()) { extractPredicateParameters(pred, parameters); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java index d65cf6fe5b0..4a99c57deac 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java @@ -24,6 +24,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; /** @@ -91,6 +92,8 @@ public interface PredicateVisitor { */ void visit(LikePredicate likePredicate); + void visit(TextPredicate textPredicate); + /** * Visit {@link ExistsSubqueryPredicate}. * diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java new file mode 100644 index 00000000000..7985463816c --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; +import org.jspecify.annotations.Nullable; + +@Internal +public final class TextPredicate extends AbstractPredicate { + + private final Expression search; + @Nullable + private final Expression language; + @Nullable + private final Expression caseSensitive; + @Nullable + private final Expression diacriticSensitive; + + public TextPredicate(Expression search, + @Nullable Expression language, + @Nullable Expression caseSensitive, + @Nullable Expression diacriticSensitive) { + this.search = CriteriaUtils.requireStringExpression(search); + this.language = language == null ? null : CriteriaUtils.requireStringExpression(language); + this.caseSensitive = caseSensitive == null ? null : CriteriaUtils.requireBoolExpression(caseSensitive); + this.diacriticSensitive = diacriticSensitive == null ? null : CriteriaUtils.requireBoolExpression(diacriticSensitive); + } + + public Expression getSearch() { + return search; + } + + public @Nullable Expression getLanguage() { + return language; + } + + public @Nullable Expression getCaseSensitive() { + return caseSensitive; + } + + public @Nullable Expression getDiacriticSensitive() { + return diacriticSensitive; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java index 334ad3440ef..deb144eb500 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java @@ -43,6 +43,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; @@ -273,6 +274,20 @@ public void visit(LikePredicate likePredicate) { visitPredicateExpression(likePredicate.getExpression()); } + @Override + public void visit(TextPredicate textPredicate) { + visitPredicateExpression(textPredicate.getSearch()); + if (textPredicate.getLanguage() != null) { + visitPredicateExpression(textPredicate.getLanguage()); + } + if (textPredicate.getCaseSensitive() != null) { + visitPredicateExpression(textPredicate.getCaseSensitive()); + } + if (textPredicate.getDiacriticSensitive() != null) { + visitPredicateExpression(textPredicate.getDiacriticSensitive()); + } + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java index 444de52ad53..f3d8cb69bd5 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java @@ -71,6 +71,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; @@ -2115,6 +2116,11 @@ public void visit(LikePredicate likePredicate) { } } + @Override + public void visit(TextPredicate textPredicate) { + throw new UnsupportedOperationException("Text predicate is not supported by SQL query builder."); + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { query.append("EXISTS"); diff --git a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java index b8eb429065a..e861e802a2f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java @@ -22,6 +22,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import jakarta.persistence.criteria.Expression; @@ -145,6 +146,11 @@ default void visit(InPredicate inPredicate) { visitIn(inPredicate.getExpression(), inPredicate.getValues(), false); } + @Override + default void visit(TextPredicate textPredicate) { + throw new UnsupportedOperationException("Text predicate is not supported by this implementation."); + } + void visitIn(Expression expression, Collection values, boolean negated); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy index 2540a96b107..65cbc22dd2e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy @@ -142,6 +142,17 @@ class MongoCriteriaSpec extends Specification { def parameter = cb.literal(colors) ((PersistentEntityCriteriaBuilder)cb).arrayContains(root.get("colors"), parameter) } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).text("coffee shop") + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).text( + cb.parameter(String), + cb.literal("en"), + cb.literal(true), + cb.literal(false) + ) + } as Specification, ] expectedWhereQuery << [ '{enabled:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', @@ -153,7 +164,9 @@ class MongoCriteriaSpec extends Specification { '''{name:{$in:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', '''{name:{$in:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', '''{name:{$nin:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', - '{colors:{$all:[{$mn_qp:0}]}}' + '{colors:{$all:[{$mn_qp:0}]}}', + '{$text:{$search:{$mn_qp:0}}}', + '{$text:{$search:{$mn_qp:0},$language:{$mn_qp:1},$caseSensitive:{$mn_qp:2},$diacriticSensitive:{$mn_qp:3}}}' ] } @@ -202,6 +215,19 @@ class MongoCriteriaSpec extends Specification { ] } + void "test negated text predicate is unsupported"() { + given: + PersistentEntityRoot entityRoot = createRoot(criteriaQuery) + criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder).text("coffee").not()) + + when: + getQuery(criteriaQuery) + + then: + def e = thrown(UnsupportedOperationException) + e.message.contains('$text') + } + @Unroll void "test projection #projection"() { given: From c942288bb15199052184d79ec9135aa7500ac633 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 16:32:36 +0200 Subject: [PATCH 08/34] Support $text criteria --- .../query/builder/MongoQueryBuilder.java | 60 +++++++ .../PersistentEntityCriteriaBuilder.java | 54 ++++++ .../impl/AbstractCriteriaBuilder.java | 40 +++++ .../jpa/criteria/impl/CriteriaUtils.java | 48 +++++ .../jpa/criteria/impl/PredicateVisitor.java | 12 ++ .../predicate/GeoIntersectsPredicate.java | 51 ++++++ .../impl/predicate/GeoWithinPredicate.java | 51 ++++++ .../impl/predicate/NearPredicate.java | 75 ++++++++ .../impl/predicate/NearSpherePredicate.java | 75 ++++++++ .../impl/predicate/TextPredicate.java | 5 + .../model/jpa/criteria/impl/util/Joiner.java | 40 +++++ .../sql/AbstractSqlLikeQueryBuilder.java | 24 +++ .../query/impl/AdvancedPredicateVisitor.java | 24 +++ .../document/mongodb/MongoCriteriaSpec.groovy | 73 +++++++- ...CriteriaQueryOperatorsExecutionSpec.groovy | 165 ++++++++++++++++++ ...CriteriaQueryOperatorsExecutionSpec.groovy | 13 ++ ...lusteredCollectionCompatibilitySpec.groovy | 78 +++++++++ ...tingClusteredCollectionConflictSpec.groovy | 71 ++++++++ ...lusteredCollectionCompatibilitySpec.groovy | 13 ++ ...tingClusteredCollectionConflictSpec.groovy | 13 ++ .../data/document/mongodb/entities/Test.java | 10 ++ .../mongo/mongoCriteriaSpecifications.adoc | 9 + .../mongoCriteriaExecuteQuery.adoc | 74 ++++++++ .../mongo/mongoMapping/mongoIndexes.adoc | 2 + 24 files changed, 1079 insertions(+), 1 deletion(-) create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java index 6e46129f554..1a70aff2376 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java @@ -51,8 +51,12 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; @@ -122,6 +126,13 @@ public final class MongoQueryBuilder implements QueryBuilder { private static final String OPTIONS = "$options"; private static final String TEXT = "$text"; private static final String SEARCH = "$search"; + private static final String GEO_WITHIN = "$geoWithin"; + private static final String GEO_INTERSECTS = "$geoIntersects"; + private static final String GEO_NEAR = "$near"; + private static final String GEO_NEAR_SPHERE = "$nearSphere"; + private static final String GEOMETRY = "$geometry"; + private static final String MIN_DISTANCE = "$minDistance"; + private static final String MAX_DISTANCE = "$maxDistance"; @Nullable @Override @@ -1063,6 +1074,9 @@ public void visit(NegatedPredicate negate) { if (negated instanceof TextPredicate) { throw new UnsupportedOperationException("MongoDB does not support negating a $text predicate."); } + if (negated instanceof NearPredicate || negated instanceof NearSpherePredicate) { + throw new UnsupportedOperationException("MongoDB does not support negating $near/$nearSphere predicates."); + } if (negated instanceof InPredicate p) { visitIn(p.getExpression(), p.getValues(), true); return; @@ -1161,6 +1175,52 @@ public void visit(TextPredicate textPredicate) { query.put(TEXT, textClause); } + @Override + public void visit(GeoWithinPredicate geoWithinPredicate) { + PersistentPropertyPath propertyPath = requireProperty(geoWithinPredicate.getExpression()).getPropertyPath(); + query.put(getPropertyPersistName(propertyPath), + Map.of(GEO_WITHIN, + Map.of(GEOMETRY, valueRepresentation(queryState, propertyPath, geoWithinPredicate.getGeometry())))); + } + + @Override + public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { + PersistentPropertyPath propertyPath = requireProperty(geoIntersectsPredicate.getExpression()).getPropertyPath(); + query.put(getPropertyPersistName(propertyPath), + Map.of(GEO_INTERSECTS, + Map.of(GEOMETRY, valueRepresentation(queryState, propertyPath, geoIntersectsPredicate.getGeometry())))); + } + + @Override + public void visit(NearPredicate nearPredicate) { + PersistentPropertyPath propertyPath = requireProperty(nearPredicate.getExpression()).getPropertyPath(); + query.put(getPropertyPersistName(propertyPath), + Map.of(GEO_NEAR, buildNearClause(propertyPath, nearPredicate.getGeometry(), nearPredicate.getMinDistance(), nearPredicate.getMaxDistance()))); + } + + @Override + public void visit(NearSpherePredicate nearSpherePredicate) { + PersistentPropertyPath propertyPath = requireProperty(nearSpherePredicate.getExpression()).getPropertyPath(); + query.put(getPropertyPersistName(propertyPath), + Map.of(GEO_NEAR_SPHERE, + buildNearClause(propertyPath, nearSpherePredicate.getGeometry(), nearSpherePredicate.getMinDistance(), nearSpherePredicate.getMaxDistance()))); + } + + private Map buildNearClause(PersistentPropertyPath propertyPath, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance) { + LinkedHashMap clause = new LinkedHashMap<>(3); + clause.put(GEOMETRY, valueRepresentation(queryState, propertyPath, geometry)); + if (minDistance != null) { + clause.put(MIN_DISTANCE, valueRepresentation(queryState, propertyPath, minDistance)); + } + if (maxDistance != null) { + clause.put(MAX_DISTANCE, valueRepresentation(queryState, propertyPath, maxDistance)); + } + return clause; + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { throw new UnsupportedOperationException("ExistsSubquery is not supported by this implementation."); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 22f00173839..338a078ba13 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -250,4 +250,58 @@ default Predicate text(String search, diacriticSensitive == null ? null : literal(diacriticSensitive) ); } + + Predicate geoWithin(Expression expression, Expression geometry); + + default Predicate geoWithin(Expression expression, Object geometry) { + return geoWithin(expression, literal(geometry)); + } + + Predicate geoIntersects(Expression expression, Expression geometry); + + default Predicate geoIntersects(Expression expression, Object geometry) { + return geoIntersects(expression, literal(geometry)); + } + + Predicate near(Expression expression, Expression geometry); + + Predicate near(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance); + + default Predicate near(Expression expression, Object geometry) { + return near(expression, literal(geometry)); + } + + default Predicate near(Expression expression, + Object geometry, + @Nullable Number minDistance, + @Nullable Number maxDistance) { + return near(expression, + literal(geometry), + minDistance == null ? null : literal(minDistance), + maxDistance == null ? null : literal(maxDistance)); + } + + Predicate nearSphere(Expression expression, Expression geometry); + + Predicate nearSphere(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance); + + default Predicate nearSphere(Expression expression, Object geometry) { + return nearSphere(expression, literal(geometry)); + } + + default Predicate nearSphere(Expression expression, + Object geometry, + @Nullable Number minDistance, + @Nullable Number maxDistance) { + return nearSphere(expression, + literal(geometry), + minDistance == null ? null : literal(minDistance), + maxDistance == null ? null : literal(maxDistance)); + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java index b3ec3bf1b3a..1ae95b17f33 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java @@ -27,7 +27,11 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; @@ -1486,6 +1490,42 @@ public Predicate text(Expression search, return new TextPredicate(search, language, caseSensitive, diacriticSensitive); } + @Override + public Predicate geoWithin(Expression expression, Expression geometry) { + return new GeoWithinPredicate(expression, geometry); + } + + @Override + public Predicate geoIntersects(Expression expression, Expression geometry) { + return new GeoIntersectsPredicate(expression, geometry); + } + + @Override + public Predicate near(Expression expression, Expression geometry) { + return near(expression, geometry, (Expression) null, (Expression) null); + } + + @Override + public Predicate near(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance) { + return new NearPredicate(expression, geometry, minDistance, maxDistance); + } + + @Override + public Predicate nearSphere(Expression expression, Expression geometry) { + return nearSphere(expression, geometry, (Expression) null, (Expression) null); + } + + @Override + public Predicate nearSphere(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance) { + return new NearSpherePredicate(expression, geometry, minDistance, maxDistance); + } + @Override public Expression localDate() { throw notSupportedOperation(); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java index 17c99ce4929..5a73c89ca10 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java @@ -26,7 +26,11 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.ParameterExpression; @@ -215,6 +219,50 @@ private static void extractPredicateParameters(Expression predicate, Set { + if (geoWithinPredicate.getExpression() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (geoWithinPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + } + case GeoIntersectsPredicate geoIntersectsPredicate -> { + if (geoIntersectsPredicate.getExpression() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (geoIntersectsPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + } + case NearPredicate nearPredicate -> { + if (nearPredicate.getExpression() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearPredicate.getMinDistance() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearPredicate.getMaxDistance() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + } + case NearSpherePredicate nearSpherePredicate -> { + if (nearSpherePredicate.getExpression() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearSpherePredicate.getGeometry() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearSpherePredicate.getMinDistance() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + if (nearSpherePredicate.getMaxDistance() instanceof ParameterExpression parameterExpression) { + parameters.add(parameterExpression); + } + } case ConjunctionPredicate conjunctionPredicate -> { for (IExpression pred : conjunctionPredicate.getPredicates()) { extractPredicateParameters(pred, parameters); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java index 4a99c57deac..eb96e608eb8 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java @@ -19,7 +19,11 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; @@ -94,6 +98,14 @@ public interface PredicateVisitor { void visit(TextPredicate textPredicate); + void visit(GeoWithinPredicate geoWithinPredicate); + + void visit(GeoIntersectsPredicate geoIntersectsPredicate); + + void visit(NearPredicate nearPredicate); + + void visit(NearSpherePredicate nearSpherePredicate); + /** * Visit {@link ExistsSubqueryPredicate}. * diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java new file mode 100644 index 00000000000..da34831ae1e --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; + +/** + * MongoDB geospatial predicate for {@code $geoIntersects} queries. + * + * @since 5.0.0 + */ +@Internal +public final class GeoIntersectsPredicate extends AbstractPredicate { + + private final Expression expression; + private final Expression geometry; + + public GeoIntersectsPredicate(Expression expression, Expression geometry) { + this.expression = CriteriaUtils.requireProperty(expression); + this.geometry = CriteriaUtils.requireIExpression(geometry); + } + + public Expression getExpression() { + return expression; + } + + public Expression getGeometry() { + return geometry; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java new file mode 100644 index 00000000000..45d803c2d80 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; + +/** + * MongoDB geospatial predicate for {@code $geoWithin} queries. + * + * @since 5.0.0 + */ +@Internal +public final class GeoWithinPredicate extends AbstractPredicate { + + private final Expression expression; + private final Expression geometry; + + public GeoWithinPredicate(Expression expression, Expression geometry) { + this.expression = CriteriaUtils.requireProperty(expression); + this.geometry = CriteriaUtils.requireIExpression(geometry); + } + + public Expression getExpression() { + return expression; + } + + public Expression getGeometry() { + return geometry; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java new file mode 100644 index 00000000000..df8c3baa9ab --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; +import org.jspecify.annotations.Nullable; + +/** + * MongoDB geospatial predicate for {@code $near} queries. + * + * @since 5.0.0 + */ +@Internal +public final class NearPredicate extends AbstractPredicate { + + private final Expression expression; + private final Expression geometry; + @Nullable + private final Expression minDistance; + @Nullable + private final Expression maxDistance; + + public NearPredicate(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance) { + this.expression = CriteriaUtils.requireProperty(expression); + this.geometry = CriteriaUtils.requireIExpression(geometry); + this.minDistance = minDistance; + if (minDistance != null) { + CriteriaUtils.requireNumericExpression(minDistance); + } + this.maxDistance = maxDistance; + if (maxDistance != null) { + CriteriaUtils.requireNumericExpression(maxDistance); + } + } + + public Expression getExpression() { + return expression; + } + + public Expression getGeometry() { + return geometry; + } + + public @Nullable Expression getMinDistance() { + return minDistance; + } + + public @Nullable Expression getMaxDistance() { + return maxDistance; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java new file mode 100644 index 00000000000..e652b88671c --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; +import org.jspecify.annotations.Nullable; + +/** + * MongoDB geospatial predicate for {@code $nearSphere} queries. + * + * @since 5.0.0 + */ +@Internal +public final class NearSpherePredicate extends AbstractPredicate { + + private final Expression expression; + private final Expression geometry; + @Nullable + private final Expression minDistance; + @Nullable + private final Expression maxDistance; + + public NearSpherePredicate(Expression expression, + Expression geometry, + @Nullable Expression minDistance, + @Nullable Expression maxDistance) { + this.expression = CriteriaUtils.requireProperty(expression); + this.geometry = CriteriaUtils.requireIExpression(geometry); + this.minDistance = minDistance; + if (minDistance != null) { + CriteriaUtils.requireNumericExpression(minDistance); + } + this.maxDistance = maxDistance; + if (maxDistance != null) { + CriteriaUtils.requireNumericExpression(maxDistance); + } + } + + public Expression getExpression() { + return expression; + } + + public Expression getGeometry() { + return geometry; + } + + public @Nullable Expression getMinDistance() { + return minDistance; + } + + public @Nullable Expression getMaxDistance() { + return maxDistance; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java index 7985463816c..2a4822d996d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java @@ -21,6 +21,11 @@ import jakarta.persistence.criteria.Expression; import org.jspecify.annotations.Nullable; +/** + * MongoDB full-text search predicate. + * + * @since 5.0.0 + */ @Internal public final class TextPredicate extends AbstractPredicate { diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java index deb144eb500..ae822eeae70 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java @@ -40,8 +40,12 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; @@ -288,6 +292,42 @@ public void visit(TextPredicate textPredicate) { } } + @Override + public void visit(GeoWithinPredicate geoWithinPredicate) { + visitPredicateExpression(geoWithinPredicate.getExpression()); + visitPredicateExpression(geoWithinPredicate.getGeometry()); + } + + @Override + public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { + visitPredicateExpression(geoIntersectsPredicate.getExpression()); + visitPredicateExpression(geoIntersectsPredicate.getGeometry()); + } + + @Override + public void visit(NearPredicate nearPredicate) { + visitPredicateExpression(nearPredicate.getExpression()); + visitPredicateExpression(nearPredicate.getGeometry()); + if (nearPredicate.getMinDistance() != null) { + visitPredicateExpression(nearPredicate.getMinDistance()); + } + if (nearPredicate.getMaxDistance() != null) { + visitPredicateExpression(nearPredicate.getMaxDistance()); + } + } + + @Override + public void visit(NearSpherePredicate nearSpherePredicate) { + visitPredicateExpression(nearSpherePredicate.getExpression()); + visitPredicateExpression(nearSpherePredicate.getGeometry()); + if (nearSpherePredicate.getMinDistance() != null) { + visitPredicateExpression(nearSpherePredicate.getMinDistance()); + } + if (nearSpherePredicate.getMaxDistance() != null) { + visitPredicateExpression(nearSpherePredicate.getMaxDistance()); + } + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java index f3d8cb69bd5..f9922f3f54b 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java @@ -68,8 +68,12 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; @@ -2121,6 +2125,26 @@ public void visit(TextPredicate textPredicate) { throw new UnsupportedOperationException("Text predicate is not supported by SQL query builder."); } + @Override + public void visit(GeoWithinPredicate geoWithinPredicate) { + throw new UnsupportedOperationException("GeoWithin predicate is not supported by SQL query builder."); + } + + @Override + public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { + throw new UnsupportedOperationException("GeoIntersects predicate is not supported by SQL query builder."); + } + + @Override + public void visit(NearPredicate nearPredicate) { + throw new UnsupportedOperationException("Near predicate is not supported by SQL query builder."); + } + + @Override + public void visit(NearSpherePredicate nearSpherePredicate) { + throw new UnsupportedOperationException("NearSphere predicate is not supported by SQL query builder."); + } + @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { query.append("EXISTS"); diff --git a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java index e861e802a2f..886a826e750 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java @@ -21,7 +21,11 @@ import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; @@ -151,6 +155,26 @@ default void visit(TextPredicate textPredicate) { throw new UnsupportedOperationException("Text predicate is not supported by this implementation."); } + @Override + default void visit(GeoWithinPredicate geoWithinPredicate) { + throw new UnsupportedOperationException("GeoWithin predicate is not supported by this implementation."); + } + + @Override + default void visit(GeoIntersectsPredicate geoIntersectsPredicate) { + throw new UnsupportedOperationException("GeoIntersects predicate is not supported by this implementation."); + } + + @Override + default void visit(NearPredicate nearPredicate) { + throw new UnsupportedOperationException("Near predicate is not supported by this implementation."); + } + + @Override + default void visit(NearSpherePredicate nearSpherePredicate) { + throw new UnsupportedOperationException("NearSphere predicate is not supported by this implementation."); + } + void visitIn(Expression expression, Collection values, boolean negated); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy index 65cbc22dd2e..55a320905f5 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy @@ -22,6 +22,8 @@ import io.micronaut.data.document.model.query.builder.MongoQueryBuilder import io.micronaut.data.document.mongodb.entities.Test import io.micronaut.data.document.tck.entities.Settlement import io.micronaut.data.document.tck.entities.SettlementPk +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.mongodb.geo.MongoGeoPolygon import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery @@ -153,6 +155,41 @@ class MongoCriteriaSpec extends Specification { cb.literal(false) ) } as Specification, + { root, query, cb -> + def polygon = new MongoGeoPolygon([[ + new MongoGeoPoint(-74.0d, 40.0d), + new MongoGeoPoint(-74.0d, 41.0d), + new MongoGeoPoint(-73.0d, 41.0d), + new MongoGeoPoint(-73.0d, 40.0d), + new MongoGeoPoint(-74.0d, 40.0d) + ]]) + ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get("locations"), cb.literal(polygon)) + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get("locations"), cb.literal(new MongoGeoPoint(-73.99d, 40.75d))) + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).near( + root.get("locations"), + cb.literal(new MongoGeoPoint(-73.98d, 40.74d)), + cb.literal(10d), + cb.literal(100d) + ) + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).near(root.get("locations"), cb.literal(new MongoGeoPoint(-73.97d, 40.73d))) + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).nearSphere(root.get("locations"), cb.parameter(MongoGeoPoint)) + } as Specification, + { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).nearSphere( + root.get("locations"), + cb.literal(new MongoGeoPoint(-73.96d, 40.72d)), + cb.literal(5d), + cb.literal(50d) + ) + } as Specification, ] expectedWhereQuery << [ '{enabled:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', @@ -166,7 +203,13 @@ class MongoCriteriaSpec extends Specification { '''{name:{$nin:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', '{colors:{$all:[{$mn_qp:0}]}}', '{$text:{$search:{$mn_qp:0}}}', - '{$text:{$search:{$mn_qp:0},$language:{$mn_qp:1},$caseSensitive:{$mn_qp:2},$diacriticSensitive:{$mn_qp:3}}}' + '{$text:{$search:{$mn_qp:0},$language:{$mn_qp:1},$caseSensitive:{$mn_qp:2},$diacriticSensitive:{$mn_qp:3}}}', + '{locations:{$geoWithin:{$geometry:{$mn_qp:0}}}}', + '{locations:{$geoIntersects:{$geometry:{$mn_qp:0}}}}', + '{locations:{$near:{$geometry:{$mn_qp:0},$minDistance:{$mn_qp:1},$maxDistance:{$mn_qp:2}}}}', + '{locations:{$near:{$geometry:{$mn_qp:0}}}}', + '{locations:{$nearSphere:{$geometry:{$mn_qp:0}}}}', + '{locations:{$nearSphere:{$geometry:{$mn_qp:0},$minDistance:{$mn_qp:1},$maxDistance:{$mn_qp:2}}}}' ] } @@ -228,6 +271,34 @@ class MongoCriteriaSpec extends Specification { e.message.contains('$text') } + void "test negated near predicate is unsupported"() { + given: + PersistentEntityRoot entityRoot = createRoot(criteriaQuery) + criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder) + .near(entityRoot.get("locations"), new MongoGeoPoint(-73.98d, 40.74d)).not()) + + when: + getQuery(criteriaQuery) + + then: + def e = thrown(UnsupportedOperationException) + e.message.contains('$near/$nearSphere') + } + + void "test negated nearSphere predicate is unsupported"() { + given: + PersistentEntityRoot entityRoot = createRoot(criteriaQuery) + criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder) + .nearSphere(entityRoot.get("locations"), new MongoGeoPoint(-73.98d, 40.74d)).not()) + + when: + getQuery(criteriaQuery) + + then: + def e = thrown(UnsupportedOperationException) + e.message.contains('$near/$nearSphere') + } + @Unroll void "test projection #projection"() { given: diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy new file mode 100644 index 00000000000..395721a7257 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy @@ -0,0 +1,165 @@ +package io.micronaut.data.document.mongodb.query + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder +import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.repository.CrudRepository +import io.micronaut.data.repository.jpa.JpaSpecificationExecutor +import io.micronaut.data.repository.jpa.criteria.QuerySpecification +import jakarta.persistence.criteria.Predicate +import org.jspecify.annotations.NonNull +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class MongoCriteriaQueryOperatorsExecutionSpec extends Specification implements MongoTestPropertyProvider { + + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.query'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + } + + def cleanup() { + repository.deleteAll() + } + + void 'criteria text predicate filters persisted entities'() { + given: + repository.saveAll([ + new QueryOperatorEntity(description: 'coffee shop downtown', location: new MongoGeoPoint(-73.9857d, 40.7484d)), + new QueryOperatorEntity(description: 'tea house uptown', location: new MongoGeoPoint(-74.1200d, 40.7200d)), + new QueryOperatorEntity(description: 'coffee roastery', location: new MongoGeoPoint(-73.9800d, 40.7500d)) + ]) + + QuerySpecification specification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).text('coffee') + } as QuerySpecification + + when: + def results = repository.findAll(specification) + + then: + results*.description as Set == ['coffee shop downtown', 'coffee roastery'] as Set + } + + void 'criteria geoWithin and geoIntersects filter persisted entities'() { + given: + def inside = new MongoGeoPoint(-73.9857d, 40.7484d) + def outside = new MongoGeoPoint(-74.3000d, 40.6000d) + repository.saveAll([ + new QueryOperatorEntity(description: 'inside', location: inside), + new QueryOperatorEntity(description: 'outside', location: outside) + ]) + + def polygon = [ + type : 'Polygon', + coordinates: [[ + [-74.0500d, 40.7000d], + [-74.0500d, 40.8000d], + [-73.9000d, 40.8000d], + [-73.9000d, 40.7000d], + [-74.0500d, 40.7000d] + ]] + ] + + def intersectPoint = [ + type : 'Point', + coordinates: [-73.9857d, 40.7484d] + ] + + QuerySpecification withinSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get('location'), cb.literal(polygon)) + } as QuerySpecification + + QuerySpecification intersectsSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get('location'), cb.literal(intersectPoint)) + } as QuerySpecification + + when: + def withinResults = repository.findAll(withinSpecification) + def intersectsResults = repository.findAll(intersectsSpecification) + + then: + withinResults*.description == ['inside'] + intersectsResults*.description == ['inside'] + } + + void 'criteria near and nearSphere filter by distance'() { + given: + def center = new MongoGeoPoint(-73.9857d, 40.7484d) + def centerGeometry = [ + type : 'Point', + coordinates: [-73.9857d, 40.7484d] + ] + repository.saveAll([ + new QueryOperatorEntity(description: 'near', location: center), + new QueryOperatorEntity(description: 'far', location: new MongoGeoPoint(-74.3000d, 40.6000d)) + ]) + + QuerySpecification nearSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).near( + root.get('location'), + cb.literal(centerGeometry), + cb.literal(0d), + cb.literal(2_000d) + ) + } as QuerySpecification + + QuerySpecification nearSphereSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).nearSphere( + root.get('location'), + cb.literal(centerGeometry), + cb.literal(0d), + cb.literal(2_000d) + ) + } as QuerySpecification + + when: + def nearResults = repository.findAll(nearSpecification) + def nearSphereResults = repository.findAll(nearSphereSpecification) + + then: + nearResults*.description == ['near'] + nearSphereResults*.description == ['near'] + } + + @NonNull + QueryOperatorRepository getRepository() { + applicationContext.getBean(QueryOperatorRepository) + } +} + +@MongoRepository +interface QueryOperatorRepository extends CrudRepository, JpaSpecificationExecutor { +} + +@MappedEntity('query_operator_entities') +class QueryOperatorEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'query_operator_description_text_idx') + String description + + @MongoGeoIndexed(name = 'query_operator_location_geo_idx') + MongoGeoPoint location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy new file mode 100644 index 00000000000..84f34f2b492 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.reactive.query + +import io.micronaut.data.document.mongodb.query.MongoCriteriaQueryOperatorsExecutionSpec + +class MongoReactiveCriteriaQueryOperatorsExecutionSpec extends MongoCriteriaQueryOperatorsExecutionSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy new file mode 100644 index 00000000000..575651b7f98 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy @@ -0,0 +1,78 @@ +package io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingClusteredCollectionCompatibilitySpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility'] + } + + void 'starts successfully when matching clustered collection options already exist'() { + given: + prepareExistingClusteredCollection('existing_clustered_compatibility_entities', + new Document('key', new Document('_id', 1)) + .append('name', 'existing_clustered_compatibility_idx') + .append('unique', true) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + + protected void prepareExistingClusteredCollection(String collectionName, Document clusteredIndex) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false', + 'micronaut.data.mongodb.driver-type' : 'sync', + 'mongodb.package-names' : 'io.micronaut.data.document.mongodb._none_' + ]) + try { + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + database.getCollection(collectionName).drop() + database.runCommand(new Document('create', collectionName) + .append('clusteredIndex', clusteredIndex)) + + def existingCollection = database.listCollections().into([]).find { it.getString('name') == collectionName } + assert existingCollection != null + def options = existingCollection.get('options', Document) + assert options != null + assert options.get('clusteredIndex', Document) != null + } finally { + preContext.close() + } + } +} + +@MongoRepository +interface ExistingClusteredCompatibilityEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'existing_clustered_compatibility_idx') +@MappedEntity('existing_clustered_compatibility_entities') +class ExistingClusteredCompatibilityEntity { + @Id + java.time.Instant id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy new file mode 100644 index 00000000000..cd9d418166d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy @@ -0,0 +1,71 @@ +package io.micronaut.data.document.mongodb.validation.existingclusteredconflict + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import org.bson.Document +import spock.lang.Specification + +class MongoExistingClusteredCollectionConflictSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.existingclusteredconflict'] + } + + void 'fails fast when existing clustered collection options conflict'() { + given: + prepareExistingClusteredCollection('existing_clustered_conflict_entities', + new Document('key', new Document('_id', 1)) + .append('name', 'different_clustered_name') + .append('unique', true) + ) + + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + def e = thrown(RuntimeException) + assert e.message != null + assert e.message.contains('Conflicting existing MongoDB collection options') + assert e.message.contains('existing_clustered_conflict_entities') + assert e.message.contains('clusteredIndexName') + } + + protected void prepareExistingClusteredCollection(String collectionName, Document clusteredIndex) { + ApplicationContext preContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'false', + 'micronaut.data.mongodb.driver-type' : 'sync', + 'mongodb.package-names' : 'io.micronaut.data.document.mongodb._none_' + ]) + try { + MongoClient mongoClient = preContext.getBean(MongoClient) + def database = mongoClient.getDatabase('test') + database.getCollection(collectionName).drop() + database.runCommand(new Document('create', collectionName) + .append('clusteredIndex', clusteredIndex)) + } finally { + preContext.close() + } + } +} + +@MongoRepository +interface ExistingClusteredConflictEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'expected_clustered_name') +@MappedEntity('existing_clustered_conflict_entities') +class ExistingClusteredConflictEntity { + @Id + java.time.Instant id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy new file mode 100644 index 00000000000..7db20f0678a --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingclusteredcompatibility + +import io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility.MongoExistingClusteredCollectionCompatibilitySpec + +class MongoReactiveExistingClusteredCollectionCompatibilitySpec extends MongoExistingClusteredCollectionCompatibilitySpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy new file mode 100644 index 00000000000..1c506b779c8 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveexistingclusteredconflict + +import io.micronaut.data.document.mongodb.validation.existingclusteredconflict.MongoExistingClusteredCollectionConflictSpec + +class MongoReactiveExistingClusteredCollectionConflictSpec extends MongoExistingClusteredCollectionConflictSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java index 386b6778f4f..d135b4c1031 100644 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java @@ -4,6 +4,7 @@ import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; +import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; import java.math.BigDecimal; import java.util.List; @@ -30,6 +31,7 @@ class Test { private OtherEntity manyToOneOther; private List colors; + private MongoGeoMultiPoint locations; public Test(String name) { this.name = name; @@ -118,4 +120,12 @@ public List getColors() { public void setColors(List colors) { this.colors = colors; } + + public MongoGeoMultiPoint getLocations() { + return locations; + } + + public void setLocations(MongoGeoMultiPoint locations) { + this.locations = locations; + } } diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc index 58442c04afa..83d9b77020c 100644 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc @@ -10,6 +10,15 @@ Each method expects a "specification" which is a functional interface with a set Micronaut Criteria API currently implements only a subset of the API. Most of it is internally used to create queries with predicates and projections. +For MongoDB-specific criteria queries, Micronaut Data also supports: + +- text search predicates (`$text`) +- geospatial predicates (`$geoWithin`, `$geoIntersects`, `$near`, `$nearSphere`) + +See query usage details and examples in xref:mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc[Mongo criteria execute query]. + +NOTE: Negated `$text`, `$near`, and `$nearSphere` predicates are not supported. + Currently, not supported JPA Criteria API features: - Joins with custom `ON` expressions and typed join methods like `joinSet` etc diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc index abba9584cdc..d565c75ee18 100644 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc @@ -65,3 +65,77 @@ snippet::example.PersonRepositorySpec[project-base="doc-examples/mongo-example", NOTE: The examples use compile-known values, and in this case, it would be better to create custom repository methods which would come with compile-time generates queries and eliminate runtime overhead. It's recommended to use criteria only for dynamic queries where the query structure is not known at the build-time. +== MongoDB-specific query operators + +When using MongoDB criteria runtime queries, you can use MongoDB-specific operators through api:data.model.jpa.criteria.PersistentEntityCriteriaBuilder[]. + +=== Text search + +[source,java] +---- +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; + +PredicateSpecification textSearch(String term) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).text(term); +} + +PredicateSpecification textSearchAdvanced(String term) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).text( + cb.literal(term), + cb.literal("en"), + cb.literal(false), + cb.literal(false) + ); +} +---- + +NOTE: MongoDB text search requires a text index on the queried collection (for example ann:data.mongodb.annotation.MongoTextIndexed[]). + +=== Geospatial predicates + +[source,java] +---- +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; +import io.micronaut.data.mongodb.geo.MongoGeoPoint; +import io.micronaut.data.mongodb.geo.MongoGeoPolygon; + +PredicateSpecification inArea(MongoGeoPolygon polygon) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).geoWithin( + root.get("location"), + cb.literal(polygon) + ); +} + +PredicateSpecification intersects(MongoGeoPoint point) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).geoIntersects( + root.get("location"), + cb.literal(point) + ); +} + +PredicateSpecification near(MongoGeoPoint point, double minDistance, double maxDistance) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).near( + root.get("location"), + cb.literal(point), + cb.literal(minDistance), + cb.literal(maxDistance) + ); +} + +PredicateSpecification nearSphere(MongoGeoPoint point) { + return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).nearSphere( + root.get("location"), + cb.literal(point) + ); +} +---- + +NOTE: Geospatial operators require compatible geospatial indexes and data modeling (for example ann:data.mongodb.annotation.MongoGeoIndexed[]). + +=== Unsupported negated operators + +Negated MongoDB-specific predicates below are intentionally rejected at query build time: + +- negated `$text` +- negated `$near` +- negated `$nearSphere` diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index fdd508763f4..d22860bad07 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -4,6 +4,8 @@ To enable startup index creation, set `micronaut.data.mongodb.create-indexes=tru NOTE: Index creation is opt-in and runs during startup collection initialization. +NOTE: Query-side usage for MongoDB text/geospatial operators is documented in xref:mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc[Mongo criteria execute query]. + == Supported index annotations Micronaut Data currently supports the following MongoDB index annotations: From fdb19c0ea1e1edb1a4970f50f644b595a92c43a4 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 18:21:28 +0200 Subject: [PATCH 09/34] MongoDB index support. Refactor package, javadocs. --- .../PersistentEntityCriteriaBuilder.java | 138 ++++++++++++++++++ .../jpa/criteria/impl/PredicateVisitor.java | 25 ++++ .../predicate/GeoIntersectsPredicate.java | 6 + .../impl/predicate/GeoWithinPredicate.java | 6 + .../impl/predicate/NearPredicate.java | 12 ++ .../impl/predicate/NearSpherePredicate.java | 12 ++ .../impl/predicate/TextPredicate.java | 12 ++ .../{ => index}/MongoClusteredIndex.java | 2 +- .../{ => index}/MongoCompoundIndex.java | 2 +- .../{ => index}/MongoCompoundIndexField.java | 2 +- .../{ => index}/MongoCompoundIndexes.java | 2 +- .../{ => index}/MongoGeoIndexType.java | 2 +- .../{ => index}/MongoGeoIndexed.java | 2 +- .../{ => index}/MongoHashedIndexed.java | 2 +- .../{ => index}/MongoIndexDirection.java | 2 +- .../annotation/{ => index}/MongoIndexed.java | 2 +- .../{ => index}/MongoTextIndexed.java | 2 +- .../{ => index}/MongoWildcardIndex.java | 2 +- .../{ => index}/MongoWildcardIndexed.java | 2 +- .../{ => index}/MongoWildcardIndexes.java | 2 +- .../mongodb/common/MongoEntityIndexes.java | 20 +-- .../init/AbstractMongoCollectionsCreator.java | 2 +- .../operations/DefaultMongoStoredQuery.java | 2 +- .../mongodb/serde/DataDecoderContext.java | 2 +- .../mongodb/serde/DataEncoderContext.java | 2 +- ...ongoClusteredCollectionCreationSpec.groovy | 2 +- .../MongoCollationIndexCreationSpec.groovy | 2 +- .../MongoIndexCommentCreationSpec.groovy | 2 +- .../MongoCompoundIndexCreationSpec.groovy | 8 +- .../geo/MongoGeoIndexCreationSpec.groovy | 2 +- .../geo2d/MongoGeo2dIndexCreationSpec.groovy | 4 +- .../MongoGeo2dOptionsIndexCreationSpec.groovy | 4 +- .../MongoCompoundGeoIndexCreationSpec.groovy | 8 +- ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 8 +- ...MongoGeoPointValueIndexCreationSpec.groovy | 2 +- ...ustomGeoPointValueIndexCreationSpec.groovy | 2 +- ...tryCollectionValueIndexCreationSpec.groovy | 2 +- ...ustomGeoPointValueIndexCreationSpec.groovy | 2 +- ...GeoLineStringValueIndexCreationSpec.groovy | 2 +- ...ltiLineStringValueIndexCreationSpec.groovy | 2 +- ...GeoMultiPointValueIndexCreationSpec.groovy | 2 +- ...oMultiPolygonValueIndexCreationSpec.groovy | 2 +- ...ngoGeoPolygonValueIndexCreationSpec.groovy | 2 +- .../MongoHashedIndexCreationSpec.groovy | 2 +- .../MongoHiddenIndexCreationSpec.groovy | 8 +- ...MongoPartialFilterIndexCreationSpec.groovy | 2 +- ...CriteriaQueryOperatorsExecutionSpec.groovy | 5 +- ...tiveClusteredCollectionCreationSpec.groovy | 2 +- .../simple/MongoIndexCreationSpec.groovy | 2 +- ...ongoAggregatedTextIndexCreationSpec.groovy | 2 +- .../text/MongoTextIndexCreationSpec.groovy | 2 +- .../ttl/MongoTtlIndexCreationSpec.groovy | 2 +- .../MongoClusteredTtlValidationSpec.groovy | 2 +- .../MongoClusteredUniqueValidationSpec.groovy | 2 +- .../MongoCollationValidationSpec.groovy | 2 +- ...ndIndexDuplicateFieldValidationSpec.groovy | 6 +- ...poundIndexEmptyFieldsValidationSpec.groovy | 2 +- ...lusteredCollectionCompatibilitySpec.groovy | 2 +- ...tingClusteredCollectionConflictSpec.groovy | 2 +- ...goExistingIndexAdvancedConflictSpec.groovy | 10 +- ...MongoExistingIndexCompatibilitySpec.groovy | 10 +- .../MongoExistingIndexConflictSpec.groovy | 2 +- ...ExistingIndexNameReconciliationSpec.groovy | 2 +- .../MongoCompoundGeoValidationSpec.groovy | 8 +- ...oCompoundGeo2dOptionsValidationSpec.groovy | 6 +- ...ngoCompoundGeoOptionsValidationSpec.groovy | 6 +- ...oCompoundGeo2dOptionsValidationSpec.groovy | 6 +- .../MongoGeoIndexValidationSpec.groovy | 4 +- ...MongoGeoSphereVersionValidationSpec.groovy | 4 +- .../geotype/MongoGeoTypeValidationSpec.groovy | 2 +- .../MongoCompoundIndexValidationSpec.groovy | 6 +- ...oIndexAdvancedOptionsResolutionSpec.groovy | 14 +- ...undIndexPartialFilterValidationSpec.groovy | 6 +- ...ReactiveStorageEngineValidationSpec.groovy | 2 +- ...iveWildcardProjectionValidationSpec.groovy | 2 +- ...dMultipleDeclarationsValidationSpec.groovy | 2 +- .../MongoStorageEngineValidationSpec.groovy | 2 +- .../text/MongoTextIndexValidationSpec.groovy | 2 +- .../MongoTextIndexVersionConflictSpec.groovy | 2 +- ...MongoCompoundIndexTtlValidationSpec.groovy | 6 +- ...ngoWildcardProjectionValidationSpec.groovy | 2 +- ...dMultipleDeclarationsValidationSpec.groovy | 2 +- .../MongoWildcardIndexCreationSpec.groovy | 2 +- ...goTopLevelWildcardIndexCreationSpec.groovy | 2 +- ...WildcardProjectionIndexCreationSpec.groovy | 2 +- ...ltipleDeclarationsIndexCreationSpec.groovy | 2 +- .../rawquery/MongoGeoRawQueryEntity.java | 2 +- .../mongoCriteriaExecuteQuery.adoc | 4 +- .../mongo/mongoMapping/mongoIndexes.adoc | 58 ++++---- 89 files changed, 378 insertions(+), 170 deletions(-) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoClusteredIndex.java (96%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoCompoundIndex.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoCompoundIndexField.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoCompoundIndexes.java (95%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoGeoIndexType.java (95%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoGeoIndexed.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoHashedIndexed.java (96%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoIndexDirection.java (93%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoIndexed.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoTextIndexed.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoWildcardIndex.java (97%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoWildcardIndexed.java (96%) rename data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/{ => index}/MongoWildcardIndexes.java (95%) diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 338a078ba13..17b529edecb 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -228,17 +228,51 @@ default Predicate ilike(Expression x, String pattern) { */ Predicate arrayContains(Expression x, Expression y); + /** + * Creates a MongoDB {@code $text} predicate. + * + * @param search The full-text search expression + * @return a new predicate + * @since 5.0.0 + */ Predicate text(Expression search); + /** + * Creates a MongoDB {@code $text} predicate with optional text-search options. + * + * @param search The full-text search expression + * @param language The optional language override expression + * @param caseSensitive The optional case-sensitive flag expression + * @param diacriticSensitive The optional diacritic-sensitive flag expression + * @return a new predicate + * @since 5.0.0 + */ Predicate text(Expression search, @Nullable Expression language, @Nullable Expression caseSensitive, @Nullable Expression diacriticSensitive); + /** + * Creates a MongoDB {@code $text} predicate. + * + * @param search The full-text search term + * @return a new predicate + * @since 5.0.0 + */ default Predicate text(String search) { return text(literal(search)); } + /** + * Creates a MongoDB {@code $text} predicate with optional text-search options. + * + * @param search The full-text search term + * @param language The optional language override + * @param caseSensitive The optional case-sensitive flag + * @param diacriticSensitive The optional diacritic-sensitive flag + * @return a new predicate + * @since 5.0.0 + */ default Predicate text(String search, @Nullable String language, @Nullable Boolean caseSensitive, @@ -251,29 +285,97 @@ default Predicate text(String search, ); } + /** + * Creates a MongoDB {@code $geoWithin} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @return a new predicate + * @since 5.0.0 + */ Predicate geoWithin(Expression expression, Expression geometry); + /** + * Creates a MongoDB {@code $geoWithin} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @return a new predicate + * @since 5.0.0 + */ default Predicate geoWithin(Expression expression, Object geometry) { return geoWithin(expression, literal(geometry)); } + /** + * Creates a MongoDB {@code $geoIntersects} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @return a new predicate + * @since 5.0.0 + */ Predicate geoIntersects(Expression expression, Expression geometry); + /** + * Creates a MongoDB {@code $geoIntersects} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @return a new predicate + * @since 5.0.0 + */ default Predicate geoIntersects(Expression expression, Object geometry) { return geoIntersects(expression, literal(geometry)); } + /** + * Creates a MongoDB {@code $near} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @return a new predicate + * @since 5.0.0 + */ Predicate near(Expression expression, Expression geometry); + /** + * Creates a MongoDB {@code $near} predicate with optional distance bounds. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @param minDistance The optional minimum distance expression + * @param maxDistance The optional maximum distance expression + * @return a new predicate + * @since 5.0.0 + */ Predicate near(Expression expression, Expression geometry, @Nullable Expression minDistance, @Nullable Expression maxDistance); + /** + * Creates a MongoDB {@code $near} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @return a new predicate + * @since 5.0.0 + */ default Predicate near(Expression expression, Object geometry) { return near(expression, literal(geometry)); } + /** + * Creates a MongoDB {@code $near} predicate with optional distance bounds. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @param minDistance The optional minimum distance + * @param maxDistance The optional maximum distance + * @return a new predicate + * @since 5.0.0 + */ default Predicate near(Expression expression, Object geometry, @Nullable Number minDistance, @@ -284,17 +386,53 @@ default Predicate near(Expression expression, maxDistance == null ? null : literal(maxDistance)); } + /** + * Creates a MongoDB {@code $nearSphere} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @return a new predicate + * @since 5.0.0 + */ Predicate nearSphere(Expression expression, Expression geometry); + /** + * Creates a MongoDB {@code $nearSphere} predicate with optional distance bounds. + * + * @param expression The geospatial property expression + * @param geometry The geometry expression + * @param minDistance The optional minimum distance expression + * @param maxDistance The optional maximum distance expression + * @return a new predicate + * @since 5.0.0 + */ Predicate nearSphere(Expression expression, Expression geometry, @Nullable Expression minDistance, @Nullable Expression maxDistance); + /** + * Creates a MongoDB {@code $nearSphere} predicate. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @return a new predicate + * @since 5.0.0 + */ default Predicate nearSphere(Expression expression, Object geometry) { return nearSphere(expression, literal(geometry)); } + /** + * Creates a MongoDB {@code $nearSphere} predicate with optional distance bounds. + * + * @param expression The geospatial property expression + * @param geometry The geometry value + * @param minDistance The optional minimum distance + * @param maxDistance The optional maximum distance + * @return a new predicate + * @since 5.0.0 + */ default Predicate nearSphere(Expression expression, Object geometry, @Nullable Number minDistance, diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java index eb96e608eb8..ddb85c2c0ca 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java @@ -96,14 +96,39 @@ public interface PredicateVisitor { */ void visit(LikePredicate likePredicate); + /** + * Visit {@link TextPredicate}. + * + * @param textPredicate The text predicate + */ void visit(TextPredicate textPredicate); + /** + * Visit {@link GeoWithinPredicate}. + * + * @param geoWithinPredicate The geo-within predicate + */ void visit(GeoWithinPredicate geoWithinPredicate); + /** + * Visit {@link GeoIntersectsPredicate}. + * + * @param geoIntersectsPredicate The geo-intersects predicate + */ void visit(GeoIntersectsPredicate geoIntersectsPredicate); + /** + * Visit {@link NearPredicate}. + * + * @param nearPredicate The near predicate + */ void visit(NearPredicate nearPredicate); + /** + * Visit {@link NearSpherePredicate}. + * + * @param nearSpherePredicate The near-sphere predicate + */ void visit(NearSpherePredicate nearSpherePredicate); /** diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java index da34831ae1e..a8b9a6237f1 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java @@ -36,10 +36,16 @@ public GeoIntersectsPredicate(Expression expression, Expression geometry) this.geometry = CriteriaUtils.requireIExpression(geometry); } + /** + * @return The geospatial property expression. + */ public Expression getExpression() { return expression; } + /** + * @return The geometry expression. + */ public Expression getGeometry() { return geometry; } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java index 45d803c2d80..2afd3e44fee 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java @@ -36,10 +36,16 @@ public GeoWithinPredicate(Expression expression, Expression geometry) { this.geometry = CriteriaUtils.requireIExpression(geometry); } + /** + * @return The geospatial property expression. + */ public Expression getExpression() { return expression; } + /** + * @return The geometry expression. + */ public Expression getGeometry() { return geometry; } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java index df8c3baa9ab..224eb42ef01 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java @@ -52,18 +52,30 @@ public NearPredicate(Expression expression, } } + /** + * @return The geospatial property expression. + */ public Expression getExpression() { return expression; } + /** + * @return The geometry expression. + */ public Expression getGeometry() { return geometry; } + /** + * @return The optional minimum distance expression. + */ public @Nullable Expression getMinDistance() { return minDistance; } + /** + * @return The optional maximum distance expression. + */ public @Nullable Expression getMaxDistance() { return maxDistance; } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java index e652b88671c..448339ec638 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java @@ -52,18 +52,30 @@ public NearSpherePredicate(Expression expression, } } + /** + * @return The geospatial property expression. + */ public Expression getExpression() { return expression; } + /** + * @return The geometry expression. + */ public Expression getGeometry() { return geometry; } + /** + * @return The optional minimum distance expression. + */ public @Nullable Expression getMinDistance() { return minDistance; } + /** + * @return The optional maximum distance expression. + */ public @Nullable Expression getMaxDistance() { return maxDistance; } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java index 2a4822d996d..8684d6245df 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java @@ -47,18 +47,30 @@ public TextPredicate(Expression search, this.diacriticSensitive = diacriticSensitive == null ? null : CriteriaUtils.requireBoolExpression(diacriticSensitive); } + /** + * @return The search expression. + */ public Expression getSearch() { return search; } + /** + * @return The optional language expression. + */ public @Nullable Expression getLanguage() { return language; } + /** + * @return The optional case-sensitive flag expression. + */ public @Nullable Expression getCaseSensitive() { return caseSensitive; } + /** + * @return The optional diacritic-sensitive flag expression. + */ public @Nullable Expression getDiacriticSensitive() { return diacriticSensitive; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java similarity index 96% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java index f596e924686..2c903476343 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoClusteredIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java index 742f2ddbe14..5e627d08c58 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java index b9beeb5e470..98b168f7493 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexField.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java similarity index 95% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java index 068bc9cf3d4..dee2b96db68 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoCompoundIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java similarity index 95% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java index f9133a84dc9..6e6e20d2987 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexType.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; /** * Supported MongoDB geospatial index kinds. diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java index 743420efc37..af86266b30d 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java similarity index 96% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java index 914f9d8d9ff..36b968c9a55 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoHashedIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java similarity index 93% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java index 3143697cce0..966da2a6b3a 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexDirection.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; /** * MongoDB index direction. diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java index 8cad8522641..7da415223f5 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java index 8ce91958d3f..4aab24d0ade 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java similarity index 97% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java index af90685c9ae..285fe64cb8d 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java similarity index 96% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java index 919f2b25832..6143b46416f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java similarity index 95% rename from data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java rename to data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java index d8fd45d460d..9d40ca251e8 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/MongoWildcardIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.data.mongodb.annotation; +package io.micronaut.data.mongodb.annotation.index; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 9b82677abd2..e3f5d5058fe 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -19,16 +19,16 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.BeanProperty; -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex; -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField; -import io.micronaut.data.mongodb.annotation.MongoIndexDirection; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType; -import io.micronaut.data.mongodb.annotation.MongoHashedIndexed; -import io.micronaut.data.mongodb.annotation.MongoIndexed; -import io.micronaut.data.mongodb.annotation.MongoTextIndexed; -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex; -import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex; +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField; +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType; +import io.micronaut.data.mongodb.annotation.index.MongoHashedIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex; +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed; import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.model.Association; import io.micronaut.data.model.PersistentEntityUtils; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index e156d5bb7e3..a93211894b6 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -36,7 +36,7 @@ import io.micronaut.data.model.naming.NamingStrategy; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.model.runtime.RuntimePersistentEntity; -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex; +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex; import io.micronaut.data.mongodb.common.MongoEntityIndexes; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; import io.micronaut.data.mongodb.operations.MongoCollectionNameProvider; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java index b55624176e5..30fb9bd9bea 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java @@ -42,7 +42,7 @@ import io.micronaut.data.model.runtime.StoredQuery; import io.micronaut.data.model.runtime.convert.AttributeConverter; import io.micronaut.data.mongodb.annotation.MongoCollation; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.mongodb.annotation.MongoProjection; import io.micronaut.data.mongodb.annotation.MongoSort; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java index 52d8abbf2a0..f3722dd3710 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java @@ -27,7 +27,7 @@ import io.micronaut.data.document.serde.OneRelationDeserializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Decoder; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java index e7ade010a95..a05d63b2fc5 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java @@ -26,7 +26,7 @@ import io.micronaut.data.document.serde.IdSerializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Encoder; diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy index 52cc18431d0..ec43cc76f6b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy index 50da0568a6b..cf695e4ee5b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy index 4710abc0a57..74d581522df 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy index 3aac17a9af7..09c15bbc1da 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy @@ -5,13 +5,11 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy index add0a2938bb..7c3ed88050f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy index 8e8c2428095..cd6b6463f4b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy @@ -7,8 +7,8 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy index 12b316bc09d..82cb87abf55 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy @@ -7,8 +7,8 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy index 0f49300c1bf..93276cd3a34 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy @@ -7,10 +7,10 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy index 21ed2d6ca97..a696c16527d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -7,10 +7,10 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy index 7137bc8ed78..90d5ecd3ece 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.MappedProperty import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoPoint diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy index cc526890d2a..b7e90d50323 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy @@ -10,7 +10,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoPointConverter import io.micronaut.data.repository.CrudRepository diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy index b0872f3086f..a8f834cf63c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoGeometryCollection import io.micronaut.data.mongodb.geo.MongoGeoLineString diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy index 1a01b25f3a3..449025b9527 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy index 5548c20dd0e..bee28dec98e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoLineString import io.micronaut.data.mongodb.geo.MongoGeoPoint diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy index ca4430cbdb3..41b4c55a31c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoMultiLineString import io.micronaut.data.mongodb.geo.MongoGeoPoint diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy index b5987888d54..d213c1108f1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint import io.micronaut.data.mongodb.geo.MongoGeoPoint diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy index 8481b7ded6d..694a22cfc6f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoMultiPolygon import io.micronaut.data.mongodb.geo.MongoGeoPoint diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy index 4a1d2a25425..c882680111a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy @@ -9,7 +9,7 @@ import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.mongodb.geo.MongoGeoPolygon diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy index b07cd08e401..a12667cb88e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoHashedIndexed +import io.micronaut.data.mongodb.annotation.index.MongoHashedIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy index 467a40880fe..15a1c4ccde4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy @@ -7,10 +7,10 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy index 1c1198dba51..e658ff56721 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy index 395721a7257..ed27a58440f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy @@ -6,14 +6,13 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import io.micronaut.data.repository.jpa.JpaSpecificationExecutor import io.micronaut.data.repository.jpa.criteria.QuerySpecification -import jakarta.persistence.criteria.Predicate import org.jspecify.annotations.NonNull import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy index f4bf2a77ca5..dc1813cf7a1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy index 3181d53378d..86e3c6d6160 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy index 5bf7b53ca5a..8faa5cf2124 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy index 425a41f17cc..85935d7bd39 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy index b5ea24dd7af..e32b2f5a451 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy index 5aec931df91..ee47680f517 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy @@ -4,7 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy index e85da25855b..385af84a1f0 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy @@ -4,7 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy index cbc7788beb4..3353b739ece 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy index bb72f1e5245..4635b695241 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy index 9da32c4950a..875d3150faf 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy index 575651b7f98..1f82d1ce64a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy index cd9d418166d..c7a94ed503c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoClusteredIndex +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy index a899f19774f..f85b1b44a2d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy @@ -6,12 +6,12 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy index b8c1684391d..de789e53ea2 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy @@ -6,11 +6,11 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy index 2a4d68bdc50..ed777f4e819 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy index b19802dd8a6..f8d154f9e59 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import org.bson.Document diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy index ce096d108ff..190e1411002 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy @@ -5,10 +5,10 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy index 3b314134e04..f109124cf24 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy index f9a6853c902..bb8deb91a8d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy index 5f490df5ece..1f9c20133b8 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy index a3a9779aca5..5796839e07c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -5,8 +5,8 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy index ae03640295e..3f50454be5a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy @@ -5,8 +5,8 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy index 8ee6c967ef3..11dd281fbf8 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy index 59f2af0d83c..195977a8830 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index 44aeff284f4..1a47308f4e7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -2,13 +2,13 @@ package io.micronaut.data.document.mongodb.validation.options import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.model.runtime.RuntimePersistentEntity -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoTextIndexed -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.mongodb.common.MongoEntityIndexes import org.bson.Document import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy index afffb063b6e..118d4a2a4b8 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy index 703f98e4daa..07fc25cc57f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy index 396201eaf97..287f833a3fa 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index 9961f2f9bd8..c2292031e79 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy index aa2af857b6d..1376525fce2 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy index eff250c2eb6..264a3e2f57d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy index ec264dd3b36..332832bcf59 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy @@ -7,7 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoTextIndexed +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy index 9a4039baa33..14df61927a1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy @@ -5,9 +5,9 @@ import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.MongoIndexDirection +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy index a372a9c5ad4..bd30cbbe29b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index a513c580b5b..6256605815b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import spock.lang.Specification diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy index 412dbd7474d..6b925a30549 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndexed +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy index 42084ce9aa6..9160861053e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy index 7e420429d4f..da212af92df 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.AutoCleanup diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy index aef7a5a50af..4ae441f7023 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy @@ -8,7 +8,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java index a6aef1946f6..1fcdfac6842 100644 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.model.DataType; -import io.micronaut.data.mongodb.annotation.MongoGeoIndexed; +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; @MappedEntity("geo_raw_query_entities") diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc index d565c75ee18..57fd64e594c 100644 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc @@ -89,7 +89,7 @@ PredicateSpecification textSearchAdvanced(String term) { } ---- -NOTE: MongoDB text search requires a text index on the queried collection (for example ann:data.mongodb.annotation.MongoTextIndexed[]). +NOTE: MongoDB text search requires a text index on the queried collection (for example ann:data.mongodb.annotation.index.MongoTextIndexed[]). === Geospatial predicates @@ -130,7 +130,7 @@ PredicateSpecification nearSphere(MongoGeoPoint point) { } ---- -NOTE: Geospatial operators require compatible geospatial indexes and data modeling (for example ann:data.mongodb.annotation.MongoGeoIndexed[]). +NOTE: Geospatial operators require compatible geospatial indexes and data modeling (for example ann:data.mongodb.annotation.index.MongoGeoIndexed[]). === Unsupported negated operators diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index d22860bad07..d78a03d2efa 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -10,18 +10,18 @@ NOTE: Query-side usage for MongoDB text/geospatial operators is documented in xr Micronaut Data currently supports the following MongoDB index annotations: -* ann:data.mongodb.annotation.MongoIndexed[] (single-field indexes) -* ann:data.mongodb.annotation.MongoCompoundIndex[] / ann:data.mongodb.annotation.MongoCompoundIndexes[] (compound indexes) -* ann:data.mongodb.annotation.MongoTextIndexed[] (text indexes, including multi-field aggregation) -* ann:data.mongodb.annotation.MongoHashedIndexed[] (hashed indexes) -* ann:data.mongodb.annotation.MongoGeoIndexed[] (geospatial indexes: `2d`, `2dsphere`) -* ann:data.mongodb.annotation.MongoWildcardIndexed[] (field-level wildcard indexes) -* ann:data.mongodb.annotation.MongoWildcardIndex[] (top-level wildcard indexes) -* ann:data.mongodb.annotation.MongoClusteredIndex[] (clustered collection options) +* ann:data.mongodb.annotation.index.MongoIndexed[] (single-field indexes) +* ann:data.mongodb.annotation.index.MongoCompoundIndex[] / ann:data.mongodb.annotation.index.MongoCompoundIndexes[] (compound indexes) +* ann:data.mongodb.annotation.index.MongoTextIndexed[] (text indexes, including multi-field aggregation) +* ann:data.mongodb.annotation.index.MongoHashedIndexed[] (hashed indexes) +* ann:data.mongodb.annotation.index.MongoGeoIndexed[] (geospatial indexes: `2d`, `2dsphere`) +* ann:data.mongodb.annotation.index.MongoWildcardIndexed[] (field-level wildcard indexes) +* ann:data.mongodb.annotation.index.MongoWildcardIndex[] (top-level wildcard indexes) +* ann:data.mongodb.annotation.index.MongoClusteredIndex[] (clustered collection options) == Single-field indexes -Use ann:data.mongodb.annotation.MongoIndexed[] to declare standard single-field indexes. +Use ann:data.mongodb.annotation.index.MongoIndexed[] to declare standard single-field indexes. Supported options include direction, `unique`, `sparse`, `hidden`, `expireAfterSeconds` (TTL), `partialFilterExpression`, `collation`, index-build `comment`, and `storageEngine`. @@ -40,7 +40,7 @@ class ProductEntity { == Compound indexes -Use ann:data.mongodb.annotation.MongoCompoundIndex[] for multi-field index declarations at entity level. +Use ann:data.mongodb.annotation.index.MongoCompoundIndex[] for multi-field index declarations at entity level. Compound declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. @@ -68,69 +68,69 @@ NOTE: TTL on compound indexes is rejected during startup validation because Mong == Text indexes -Use ann:data.mongodb.annotation.MongoTextIndexed[] on text fields. +Use ann:data.mongodb.annotation.index.MongoTextIndexed[] on text fields. Multiple text-indexed fields on the same entity are aggregated into the corresponding MongoDB text index definition. Text declarations also support `defaultLanguage`, `languageOverride`, and `textIndexVersion`. -NOTE: For aggregated multi-field text indexes, these text-specific options must be declared consistently across all participating ann:data.mongodb.annotation.MongoTextIndexed[] fields; conflicting declarations fail fast during startup validation. +NOTE: For aggregated multi-field text indexes, these text-specific options must be declared consistently across all participating ann:data.mongodb.annotation.index.MongoTextIndexed[] fields; conflicting declarations fail fast during startup validation. == Hashed indexes -Use ann:data.mongodb.annotation.MongoHashedIndexed[] to create hashed indexes. +Use ann:data.mongodb.annotation.index.MongoHashedIndexed[] to create hashed indexes. == Geospatial indexes -Use ann:data.mongodb.annotation.MongoGeoIndexed[] for geospatial indexing (`2d` and `2dsphere`). +Use ann:data.mongodb.annotation.index.MongoGeoIndexed[] for geospatial indexing (`2d` and `2dsphere`). -For `2dsphere` indexes, ann:data.mongodb.annotation.MongoGeoIndexed[] also supports `sphereVersion`. +For `2dsphere` indexes, ann:data.mongodb.annotation.index.MongoGeoIndexed[] also supports `sphereVersion`. NOTE: `sphereVersion` is only valid for `2dsphere` indexes; declaring it for other geospatial kinds is rejected during startup validation (fail-fast). Modeled geospatial values are supported for `MongoGeoPoint`, `MongoGeoMultiPoint`, `MongoGeoLineString`, `MongoGeoMultiLineString`, `MongoGeoPolygon`, `MongoGeoMultiPolygon`, `MongoGeoGeometryCollection`, and custom point-like modeled types. -For properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[], Micronaut Data now applies `MongoGeoPointConverter` implicitly when no explicit `@MappedProperty(converter=...)` is declared. +For properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[], Micronaut Data now applies `MongoGeoPointConverter` implicitly when no explicit `@MappedProperty(converter=...)` is declared. Point-like modeled types can map coordinates through numeric properties such as `x/y`, `longitude/latitude`, or `lng|lon/lat`. -For polygon modeled values, Micronaut Data applies `MongoGeoPolygonConverter` implicitly for `MongoGeoPolygon` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For polygon modeled values, Micronaut Data applies `MongoGeoPolygonConverter` implicitly for `MongoGeoPolygon` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. -For line-string modeled values, Micronaut Data applies `MongoGeoLineStringConverter` implicitly for `MongoGeoLineString` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For line-string modeled values, Micronaut Data applies `MongoGeoLineStringConverter` implicitly for `MongoGeoLineString` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. -For multi-line-string modeled values, Micronaut Data applies `MongoGeoMultiLineStringConverter` implicitly for `MongoGeoMultiLineString` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For multi-line-string modeled values, Micronaut Data applies `MongoGeoMultiLineStringConverter` implicitly for `MongoGeoMultiLineString` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. -For multi-point modeled values, Micronaut Data applies `MongoGeoMultiPointConverter` implicitly for `MongoGeoMultiPoint` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For multi-point modeled values, Micronaut Data applies `MongoGeoMultiPointConverter` implicitly for `MongoGeoMultiPoint` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. -For multi-polygon modeled values, Micronaut Data applies `MongoGeoMultiPolygonConverter` implicitly for `MongoGeoMultiPolygon` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For multi-polygon modeled values, Micronaut Data applies `MongoGeoMultiPolygonConverter` implicitly for `MongoGeoMultiPolygon` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. -For geometry-collection modeled values, Micronaut Data applies `MongoGeoGeometryCollectionConverter` implicitly for `MongoGeoGeometryCollection` properties annotated with ann:data.mongodb.annotation.MongoGeoIndexed[]. +For geometry-collection modeled values, Micronaut Data applies `MongoGeoGeometryCollectionConverter` implicitly for `MongoGeoGeometryCollection` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. With `MongoGeoGeometryCollection` in place, modeled support now covers all standard GeoJSON geometry kinds (`Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`). For raw Mongo query annotations (for example ann:data.mongodb.annotation.MongoFindQuery[]), supported modeled geospatial parameter types are also converted implicitly to GeoJSON persisted shapes (`type` + `coordinates`) when no explicit converter is specified. -NOTE: ann:data.mongodb.annotation.MongoGeoIndexed[] now validates property type compatibility during startup and fails fast for unsupported non-geospatial property types. +NOTE: ann:data.mongodb.annotation.index.MongoGeoIndexed[] now validates property type compatibility during startup and fails fast for unsupported non-geospatial property types. == Wildcard indexes -Use ann:data.mongodb.annotation.MongoWildcardIndexed[] for field-level wildcard indexes. +Use ann:data.mongodb.annotation.index.MongoWildcardIndexed[] for field-level wildcard indexes. -Use ann:data.mongodb.annotation.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. +Use ann:data.mongodb.annotation.index.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. -ann:data.mongodb.annotation.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key are merged into a single managed index definition. +ann:data.mongodb.annotation.index.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key are merged into a single managed index definition. Top-level wildcard declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. NOTE: `storageEngine` must be valid JSON. Invalid declarations are rejected during startup validation (fail-fast). NOTE: MongoDB allows `wildcardProjection` only for top-level wildcard indexes (`{"$**": 1}`). -Using `wildcardProjection` on field-level ann:data.mongodb.annotation.MongoWildcardIndexed[] is rejected during startup validation (fail-fast), with an explicit error that points to ann:data.mongodb.annotation.MongoWildcardIndex[]. +Using `wildcardProjection` on field-level ann:data.mongodb.annotation.index.MongoWildcardIndexed[] is rejected during startup validation (fail-fast), with an explicit error that points to ann:data.mongodb.annotation.index.MongoWildcardIndex[]. NOTE: If multiple top-level wildcard declarations define conflicting options for `$**`, startup fails fast with an explicit conflict error. == TTL indexes -Use ann:data.mongodb.annotation.MongoIndexed[] with `expireAfterSeconds` to create TTL indexes: +Use ann:data.mongodb.annotation.index.MongoIndexed[] with `expireAfterSeconds` to create TTL indexes: [source,java] ---- @@ -147,7 +147,7 @@ class SessionEntity { == Clustered collections -Use ann:data.mongodb.annotation.MongoClusteredIndex[] on the entity to configure clustered collection options during startup collection initialization. +Use ann:data.mongodb.annotation.index.MongoClusteredIndex[] on the entity to configure clustered collection options during startup collection initialization. [source,java] ---- From f04cb1513fbe949caa1f6b48e721395cd92e2433 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 29 Mar 2026 20:07:44 +0200 Subject: [PATCH 10/34] MongoDB index support. Refactor package, javadocs. --- .../mongodb/common/MongoEntityIndexes.java | 3 +- .../mongodb/conf/MongoDataConfiguration.java | 29 ++++++ .../init/AbstractMongoCollectionsCreator.java | 48 +++++++++- ...ddedCollectionPathIndexCreationSpec.groovy | 77 ++++++++++++++++ ...mpoundEmbeddedPathIndexCreationSpec.groovy | 86 ++++++++++++++++++ ...CriteriaQueryOperatorsExecutionSpec.groovy | 85 ++++++++++++++++++ ...ddedCollectionPathIndexCreationSpec.groovy | 73 +++++++++++++++ ...mpoundEmbeddedPathIndexCreationSpec.groovy | 82 +++++++++++++++++ ...ndexBootstrapWithoutCollectionsSpec.groovy | 89 +++++++++++++++++++ ...oIndexFailurePolicyWarnContinueSpec.groovy | 56 ++++++++++++ ...ndexBootstrapWithoutCollectionsSpec.groovy | 18 ++++ ...eIndexFailurePolicyWarnContinueSpec.groovy | 13 +++ .../mongo/mongoMapping/mongoIndexes.adoc | 54 +++++++++++ 13 files changed, 710 insertions(+), 3 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index e3f5d5058fe..54aa282eb3b 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -392,7 +392,8 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit Double indexMax = null; for (var fieldAnnotation : annotationValue.getAnnotations("fields", MongoCompoundIndexField.class)) { String path = fieldAnnotation.stringValue().orElseThrow(); - String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, path) + String pathForLookup = path.contains(".") ? path.replace('.', '_') : path; + String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, pathForLookup) .map(persistentPath -> { var propertyPath = entity.getPropertyPath(persistentPath); if (propertyPath == null) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java index 1c1b89753e2..5a96d63dbfd 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java @@ -32,6 +32,7 @@ public final class MongoDataConfiguration { public static final String PREFIX = "micronaut.data.mongodb"; public static final String CREATE_COLLECTIONS_PROPERTY = PREFIX + ".create-collections"; public static final String CREATE_INDEXES_PROPERTY = PREFIX + ".create-indexes"; + public static final String CREATE_INDEXES_FAILURE_POLICY_PROPERTY = PREFIX + ".create-indexes-failure-policy"; public static final String DRIVER_TYPE_PROPERTY = PREFIX + ".driver-type"; public static final String JSON_VIEWS_PROPERTY = PREFIX + ".ignore-json-views"; public static final String DRIVER_TYPE_SYNC = DriverType.SYNC.name(); @@ -47,6 +48,11 @@ public final class MongoDataConfiguration { */ private boolean createIndexes; + /** + * Policy controlling application startup behavior when MongoDB index initialization fails. + */ + private IndexCreationFailurePolicy createIndexesFailurePolicy = IndexCreationFailurePolicy.FAIL_FAST; + /** * Choose the appropriate driver type when both are on classpath. */ @@ -81,6 +87,14 @@ public void setIgnoreJsonViews(boolean ignoreJsonViews) { this.ignoreJsonViews = ignoreJsonViews; } + public IndexCreationFailurePolicy getCreateIndexesFailurePolicy() { + return createIndexesFailurePolicy; + } + + public void setCreateIndexesFailurePolicy(IndexCreationFailurePolicy createIndexesFailurePolicy) { + this.createIndexesFailurePolicy = createIndexesFailurePolicy; + } + public DriverType getDriverType() { return driverType; } @@ -96,6 +110,21 @@ public enum DriverType { SYNC, REACTIVE } + /** + * Startup behavior policy when index creation fails. + */ + public enum IndexCreationFailurePolicy { + /** + * Fail startup immediately when index initialization fails. + */ + FAIL_FAST, + + /** + * Log and continue startup when index initialization fails. + */ + WARN_AND_CONTINUE + } + /** * Not reactive driver condition. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index a93211894b6..e20aa1cfe39 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -39,6 +39,7 @@ import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex; import io.micronaut.data.mongodb.common.MongoEntityIndexes; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; +import io.micronaut.data.mongodb.conf.MongoDataConfiguration.IndexCreationFailurePolicy; import io.micronaut.data.mongodb.operations.MongoCollectionNameProvider; import io.micronaut.inject.qualifiers.Qualifiers; import org.bson.Document; @@ -112,6 +113,7 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, boolean createCollections = mongoDataConfiguration.isCreateCollections(); boolean createIndexes = mongoDataConfiguration.isCreateIndexes(); + IndexCreationFailurePolicy indexCreationFailurePolicy = mongoDataConfiguration.getCreateIndexesFailurePolicy(); if (!createCollections && !createIndexes) { return; } @@ -131,9 +133,13 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, .toArray(PersistentEntity[]::new); DatabaseOperations databaseOperations = databaseOperationsProvider.get(mongoConfiguration); + int indexProcessedCount = 0; + int indexFailureCount = 0; + String telemetryDatabaseName = ""; for (PersistentEntity entity : entities) { Dtbs database = databaseOperations.find(entity); + telemetryDatabaseName = databaseOperations.getDatabaseName(database); Set collections = databaseOperations.listCollectionNames(database); String persistedName = mongoCollectionNameProvider.provide(entity); MongoResolvedCollectionOptions desiredCollectionOptions = resolveCollectionOptions(entity); @@ -151,13 +157,51 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, databaseOperations.createCollection(database, persistedName, desiredCollectionOptions); collections.add(persistedName); } - if ((collectionExists || createCollections) && createIndexes) { - createIndexes(databaseOperations, database, entity, persistedName); + if (createIndexes) { + if (collectionExists || createCollections) { + try { + createIndexes(databaseOperations, database, entity, persistedName); + indexProcessedCount++; + } catch (RuntimeException e) { + if (indexCreationFailurePolicy == IndexCreationFailurePolicy.WARN_AND_CONTINUE) { + indexFailureCount++; + LOG.warn("MongoDB index initialization failed for entity: {} in collection: {} in database: {}. Continuing due to policy {}.", + entity.getName(), + persistedName, + databaseOperations.getDatabaseName(database), + indexCreationFailurePolicy, + e); + } else { + throw e; + } + } + } else if (LOG.isDebugEnabled()) { + LOG.debug("Skipping MongoDB index initialization for entity: {} in collection: {} in database: {} because collection does not exist and {} is disabled.", + entity.getName(), + persistedName, + databaseOperations.getDatabaseName(database), + MongoDataConfiguration.CREATE_COLLECTIONS_PROPERTY); + } } if (createCollections) { createJoinCollections(databaseOperations, database, collections, entity, persistedName); } } + if (createIndexes) { + if (indexFailureCount > 0 && indexCreationFailurePolicy == IndexCreationFailurePolicy.WARN_AND_CONTINUE) { + LOG.warn("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", + telemetryDatabaseName, + indexProcessedCount, + indexFailureCount, + indexCreationFailurePolicy); + } else if (LOG.isInfoEnabled()) { + LOG.info("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", + telemetryDatabaseName, + indexProcessedCount, + indexFailureCount, + indexCreationFailurePolicy); + } + } } } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy new file mode 100644 index 00000000000..c0cb2a0c594 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.data.document.mongodb.compound.collectionpath + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundCollectionFieldIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.compound.collectionpath'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates index for collection field (multikey) path'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_embedded_collection_path_entities') + assert indexes*.name.contains('tags_idx') + def index = indexes.find { it.name == 'tags_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'tags' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface CompoundCollectionFieldIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'tags_idx', + fields = [ + @MongoCompoundIndexField(value = 'tags', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('compound_embedded_collection_path_entities') +class CompoundCollectionFieldIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + List tags +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy new file mode 100644 index 00000000000..6f235197131 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy @@ -0,0 +1,86 @@ +package io.micronaut.data.document.mongodb.compound.dotpath + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Embeddable +import io.micronaut.data.annotation.Relation +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundEmbeddedPathIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.compound.dotpath'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates index for valid embedded dot-notation path'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_embedded_path_indexed_entities') + assert indexes*.name.contains('location_state_idx') + def index = indexes.find { it.name == 'location_state_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location.state' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface CompoundEmbeddedPathIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'location_state_idx', + fields = [ + @MongoCompoundIndexField(value = 'location.state', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('compound_embedded_path_indexed_entities') +class CompoundEmbeddedPathIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + @Relation(Relation.Kind.EMBEDDED) + EmbeddedLocation location +} + +@Embeddable +class EmbeddedLocation { + String state + String city +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy index ed27a58440f..0f65d070897 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.data.document.mongodb.query import io.micronaut.context.ApplicationContext +import io.micronaut.core.convert.ConversionContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity @@ -10,8 +11,12 @@ import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.mongodb.geo.MongoGeoPoint +import io.micronaut.data.mongodb.geo.MongoGeoPointConverter +import io.micronaut.data.mongodb.geo.MongoGeoPolygon +import io.micronaut.data.mongodb.geo.MongoGeoPolygonConverter import io.micronaut.data.repository.CrudRepository import io.micronaut.data.repository.jpa.JpaSpecificationExecutor +import io.micronaut.data.repository.jpa.criteria.PredicateSpecification import io.micronaut.data.repository.jpa.criteria.QuerySpecification import org.jspecify.annotations.NonNull import spock.lang.AutoCleanup @@ -140,6 +145,86 @@ class MongoCriteriaQueryOperatorsExecutionSpec extends Specification implements nearSphereResults*.description == ['near'] } + void 'predicate specification supports text and near operators'() { + given: + def center = new MongoGeoPoint(-73.9857d, 40.7484d) + def centerGeometry = new MongoGeoPointConverter().convertToPersistedValue(center, ConversionContext.DEFAULT) + repository.saveAll([ + new QueryOperatorEntity(description: 'coffee near center', location: center), + new QueryOperatorEntity(description: 'coffee far away', location: new MongoGeoPoint(-74.3000d, 40.6000d)), + new QueryOperatorEntity(description: 'tea near center', location: new MongoGeoPoint(-73.9856d, 40.7485d)) + ]) + + PredicateSpecification textSpecification = { root, cb -> + ((PersistentEntityCriteriaBuilder) cb).text('coffee') + } as PredicateSpecification + + PredicateSpecification nearSpecification = { root, cb -> + ((PersistentEntityCriteriaBuilder) cb).near( + root.get('location'), + cb.literal(centerGeometry), + cb.literal(0d), + cb.literal(2_000d) + ) + } as PredicateSpecification + + when: + def textResults = repository.findAll(textSpecification) + def nearResults = repository.findAll(nearSpecification) + + then: + textResults*.description as Set == ['coffee near center', 'coffee far away'] as Set + nearResults*.description as Set == ['coffee near center', 'tea near center'] as Set + } + + void 'criteria execution supports modeled geospatial values via converters'() { + given: + def inside = new MongoGeoPoint(-73.9857d, 40.7484d) + def outside = new MongoGeoPoint(-74.3000d, 40.6000d) + repository.saveAll([ + new QueryOperatorEntity(description: 'inside-modeled', location: inside), + new QueryOperatorEntity(description: 'outside-modeled', location: outside) + ]) + + def modeledPolygon = new MongoGeoPolygon([[ + new MongoGeoPoint(-74.0500d, 40.7000d), + new MongoGeoPoint(-74.0500d, 40.8000d), + new MongoGeoPoint(-73.9000d, 40.8000d), + new MongoGeoPoint(-73.9000d, 40.7000d), + new MongoGeoPoint(-74.0500d, 40.7000d) + ]]) + def pointGeometry = new MongoGeoPointConverter().convertToPersistedValue(inside, ConversionContext.DEFAULT) + def polygonGeometry = new MongoGeoPolygonConverter().convertToPersistedValue(modeledPolygon, ConversionContext.DEFAULT) + + QuerySpecification withinSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get('location'), cb.literal(polygonGeometry)) + } as QuerySpecification + + QuerySpecification intersectsSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get('location'), cb.literal(pointGeometry)) + } as QuerySpecification + + QuerySpecification nearSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).near(root.get('location'), cb.literal(pointGeometry), cb.literal(0d), cb.literal(2_000d)) + } as QuerySpecification + + QuerySpecification nearSphereSpecification = { root, query, cb -> + ((PersistentEntityCriteriaBuilder) cb).nearSphere(root.get('location'), cb.literal(pointGeometry), cb.literal(0d), cb.literal(2_000d)) + } as QuerySpecification + + when: + def withinResults = repository.findAll(withinSpecification) + def intersectsResults = repository.findAll(intersectsSpecification) + def nearResults = repository.findAll(nearSpecification) + def nearSphereResults = repository.findAll(nearSphereSpecification) + + then: + withinResults*.description == ['inside-modeled'] + intersectsResults*.description == ['inside-modeled'] + nearResults*.description == ['inside-modeled'] + nearSphereResults*.description == ['inside-modeled'] + } + @NonNull QueryOperatorRepository getRepository() { applicationContext.getBean(QueryOperatorRepository) diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy new file mode 100644 index 00000000000..4d096b73ebc --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.data.document.mongodb.reactive.compound.collectionpath + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCompoundCollectionFieldIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.compound.collectionpath'] + } + + void 'creates index for collection field (multikey) path in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_embedded_collection_path_entities') + assert indexes*.name.contains('reactive_tags_idx') + def index = indexes.find { it.name == 'reactive_tags_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'tags' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface ReactiveCompoundCollectionFieldIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'reactive_tags_idx', + fields = [ + @MongoCompoundIndexField(value = 'tags', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('reactive_compound_embedded_collection_path_entities') +class ReactiveCompoundCollectionFieldIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + List tags +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy new file mode 100644 index 00000000000..0a1cf4d6b83 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy @@ -0,0 +1,82 @@ +package io.micronaut.data.document.mongodb.reactive.compound.dotpath + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Embeddable +import io.micronaut.data.annotation.Relation +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoReactiveCompoundEmbeddedPathIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.reactive.compound.dotpath'] + } + + void 'creates index for valid embedded dot-notation path in reactive mode'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_embedded_path_indexed_entities') + assert indexes*.name.contains('reactive_location_state_idx') + def index = indexes.find { it.name == 'reactive_location_state_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location.state' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface ReactiveCompoundEmbeddedPathIndexedEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'reactive_location_state_idx', + fields = [ + @MongoCompoundIndexField(value = 'location.state', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('reactive_compound_embedded_path_indexed_entities') +class ReactiveCompoundEmbeddedPathIndexedEntity { + @Id + @GeneratedValue + String id + + String name + + @Relation(Relation.Kind.EMBEDDED) + ReactiveEmbeddedLocation location +} + +@Embeddable +class ReactiveEmbeddedLocation { + String state + String city +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy new file mode 100644 index 00000000000..5b43857dc26 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy @@ -0,0 +1,89 @@ +package io.micronaut.data.document.mongodb.validation.indexbootstrap + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoIndexBootstrapWithoutCollectionsSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.indexbootstrap'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + void 'creates indexes for existing collections when create collections is disabled'() { + given: + ApplicationContext collectionBootstrapContext + ApplicationContext indexBootstrapContext + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + when: 'the collection is created first without index creation' + collectionBootstrapContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'false' + ]) + + then: + noExceptionThrown() + MongoClient collectionBootstrapClient = collectionBootstrapContext.getBean(MongoClient) + conditions.eventually { + assert collectionBootstrapClient.getDatabase('test').listCollectionNames().into([]).contains('index_bootstrap_entities') + } + + when: 'index bootstrap runs with collection creation disabled' + indexBootstrapContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'false', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + indexBootstrapContext.containsBean(expectedCollectionsCreatorBeanType()) + MongoClient indexBootstrapClient = indexBootstrapContext.getBean(MongoClient) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(indexBootstrapClient, 'test', 'index_bootstrap_entities') + assert indexes*.name.contains('index_bootstrap_name_idx') + def index = indexes.find { it.name == 'index_bootstrap_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + } + + cleanup: + collectionBootstrapContext?.close() + indexBootstrapContext?.close() + } +} + +@MongoRepository +interface IndexBootstrapEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'index_bootstrap_name_idx', + fields = [ + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('index_bootstrap_entities') +class IndexBootstrapEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy new file mode 100644 index 00000000000..c772c616407 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.data.document.mongodb.validation.indexfailurepolicy + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoIndexFailurePolicyWarnContinueSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.indexfailurepolicy'] + } + + void 'starts when index initialization fails and policy is warn and continue'() { + when: + ApplicationContext context = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections' : 'true', + 'micronaut.data.mongodb.create-indexes' : 'true', + 'micronaut.data.mongodb.create-indexes-failure-policy': 'WARN_AND_CONTINUE' + ]) + + then: + noExceptionThrown() + context.getBean(IndexFailurePolicyEntityRepository) + + cleanup: + context?.close() + } +} + +@MongoRepository +interface IndexFailurePolicyEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'index_failure_policy_invalid_path_idx', + fields = [ + @MongoCompoundIndexField(value = 'missing', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('index_failure_policy_entities') +class IndexFailurePolicyEntity { + @Id + @GeneratedValue + String id + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy new file mode 100644 index 00000000000..c3da0aa344d --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy @@ -0,0 +1,18 @@ +package io.micronaut.data.document.mongodb.validation.reactiveindexbootstrap + +import io.micronaut.data.document.mongodb.validation.indexbootstrap.MongoIndexBootstrapWithoutCollectionsSpec + +class MongoReactiveIndexBootstrapWithoutCollectionsSpec extends MongoIndexBootstrapWithoutCollectionsSpec { + + @Override + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator + } + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy new file mode 100644 index 00000000000..98143dcce39 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy @@ -0,0 +1,13 @@ +package io.micronaut.data.document.mongodb.validation.reactiveindexfailurepolicy + +import io.micronaut.data.document.mongodb.validation.indexfailurepolicy.MongoIndexFailurePolicyWarnContinueSpec + +class MongoReactiveIndexFailurePolicyWarnContinueSpec extends MongoIndexFailurePolicyWarnContinueSpec { + + @Override + Map getProperties() { + super.getProperties() + [ + 'micronaut.data.mongodb.driver-type': 'reactive' + ] + } +} diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index d78a03d2efa..da6aaa5f0d8 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -2,8 +2,16 @@ Micronaut Data MongoDB supports declaration-based index creation for mapped enti To enable startup index creation, set `micronaut.data.mongodb.create-indexes=true`. +Use `micronaut.data.mongodb.create-indexes-failure-policy` to control startup behavior when index initialization fails: + +* `FAIL_FAST` (default): fail startup immediately +* `WARN_AND_CONTINUE`: log the failure and continue startup + NOTE: Index creation is opt-in and runs during startup collection initialization. +When `micronaut.data.mongodb.create-indexes=true` and `micronaut.data.mongodb.create-collections=false`, Micronaut Data still performs index reconciliation/creation for collections that already exist. +If a mapped collection does not exist and collection creation is disabled, index initialization for that entity is skipped. + NOTE: Query-side usage for MongoDB text/geospatial operators is documented in xref:mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc[Mongo criteria execute query]. == Supported index annotations @@ -66,6 +74,48 @@ class OrderEntity { NOTE: TTL on compound indexes is rejected during startup validation because MongoDB TTL semantics are single-field oriented. This is fail-fast validation: the application startup fails immediately with a clear error instead of silently creating a non-expiring index definition. +=== Compound path examples + +Embedded dot-notation path: + +[source,java] +---- +@MongoCompoundIndex( + name = "location_state_idx", + fields = { + @MongoCompoundIndexField(value = "location.state", direction = MongoIndexDirection.ASC) + } +) +@MappedEntity("addresses") +class AddressEntity { + @Id + @GeneratedValue + String id; + + AddressLocation location; +} +---- + +Collection/array field (multikey) path: + +[source,java] +---- +@MongoCompoundIndex( + name = "tags_idx", + fields = { + @MongoCompoundIndexField(value = "tags", direction = MongoIndexDirection.ASC) + } +) +@MappedEntity("articles") +class ArticleEntity { + @Id + @GeneratedValue + String id; + + List tags; +} +---- + == Text indexes Use ann:data.mongodb.annotation.index.MongoTextIndexed[] on text fields. @@ -178,4 +228,8 @@ Managed reconciliation is key-first and option-aware: * if a desired explicit name differs from an existing same-key index name, startup fails fast with an explicit name-conflict message * if a desired name is omitted, existing same-key indexes are matched by key/options (generated or existing names are not used as primary identity) +Managed option matching includes: uniqueness/sparse/hidden, TTL, partial filter, collation, geospatial/text-specific options, wildcard projection, and storage engine. + +`comment` and `commitQuorum` are treated as index-creation command options and are not part of existing-index managed option reconciliation. + For clustered declarations, existing collection options are also compared, and conflicting clustered options fail fast. From 3f308fd82347e5965e72fd9c56a910b06b044de5 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 30 Mar 2026 15:13:23 +0200 Subject: [PATCH 11/34] Mongo indexes, some small improvements --- data-model/build.gradle | 1 + .../jpa/criteria/impl/CriteriaUtils.java | 12 --- .../criteria/impl/CriteriaUtilsSpec.groovy | 75 +++++++++++++++++++ .../MongoCompoundIndexCreationSpec.groovy | 5 -- .../simple/MongoIndexCreationSpec.groovy | 2 - 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy diff --git a/data-model/build.gradle b/data-model/build.gradle index bf3d178f18e..1f84e7a99bb 100644 --- a/data-model/build.gradle +++ b/data-model/build.gradle @@ -21,6 +21,7 @@ dependencies { compileOnly mnSerde.micronaut.serde.api testCompileOnly mnSerde.micronaut.serde.processor + testImplementation mnSql.jakarta.persistence.api testImplementation mn.micronaut.inject.java.test testImplementation mnSerde.micronaut.serde.jackson testImplementation mn.micronaut.jackson.databind diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java index 5a73c89ca10..902fe7ddebc 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java @@ -220,25 +220,16 @@ private static void extractPredicateParameters(Expression predicate, Set { - if (geoWithinPredicate.getExpression() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } if (geoWithinPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { parameters.add(parameterExpression); } } case GeoIntersectsPredicate geoIntersectsPredicate -> { - if (geoIntersectsPredicate.getExpression() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } if (geoIntersectsPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { parameters.add(parameterExpression); } } case NearPredicate nearPredicate -> { - if (nearPredicate.getExpression() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } if (nearPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { parameters.add(parameterExpression); } @@ -250,9 +241,6 @@ private static void extractPredicateParameters(Expression predicate, Set { - if (nearSpherePredicate.getExpression() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } if (nearSpherePredicate.getGeometry() instanceof ParameterExpression parameterExpression) { parameters.add(parameterExpression); } diff --git a/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy new file mode 100644 index 00000000000..6001596a746 --- /dev/null +++ b/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2026 original authors + * + * 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 + * + * https://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 io.micronaut.data.model.jpa.criteria.impl + +import io.micronaut.data.model.PersistentProperty +import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression +import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate +import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate +import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate +import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate +import spock.lang.Specification + +class CriteriaUtilsSpec extends Specification { + + void 'extracts parameters from text predicate in declared order'() { + given: + def search = parameter(String, 'search') + def language = parameter(String, 'language') + def caseSensitive = parameter(Boolean, 'caseSensitive') + def diacriticSensitive = parameter(Boolean, 'diacriticSensitive') + + when: + def parameters = CriteriaUtils.extractPredicateParameters(new TextPredicate(search, language, caseSensitive, diacriticSensitive)) + + then: + parameters as List == [search, language, caseSensitive, diacriticSensitive] + } + + void 'extracts only parameter expressions from mongo-specific predicates and nested conjunctions'() { + given: + def property = Stub(DefaultPersistentPropertyPath) { + getProperty() >> Stub(PersistentProperty) + } + def textSearch = parameter(String, 'textSearch') + def geoWithinGeometry = parameter(Map, 'geoWithinGeometry') + def geoIntersectsGeometry = parameter(Map, 'geoIntersectsGeometry') + def nearGeometry = parameter(Map, 'nearGeometry') + def nearMin = parameter(Double, 'nearMin') + def nearSphereGeometry = parameter(Map, 'nearSphereGeometry') + def nearSphereMax = parameter(Double, 'nearSphereMax') + + def predicate = new ConjunctionPredicate([ + new TextPredicate(textSearch, new LiteralExpression<>('en'), null, null), + new GeoWithinPredicate(property, geoWithinGeometry), + new GeoIntersectsPredicate(property, geoIntersectsGeometry), + new NearPredicate(property, nearGeometry, nearMin, new LiteralExpression<>(2000d)), + new NearSpherePredicate(property, nearSphereGeometry, new LiteralExpression<>(0d), nearSphereMax) + ]) + + when: + def parameters = CriteriaUtils.extractPredicateParameters(predicate) + + then: + parameters as List == [textSearch, geoWithinGeometry, geoIntersectsGeometry, nearGeometry, nearMin, nearSphereGeometry, nearSphereMax] + } + + private static IParameterExpression parameter(Class type, String name) { + new DefaultParameterExpression<>(type, name, null) + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy index 09c15bbc1da..b2534f34287 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy @@ -37,11 +37,6 @@ class MongoCompoundIndexCreationSpec extends Specification implements MongoTestP @Shared MongoClient mongoClient = applicationContext.getBean(MongoClient) - def setupSpec() { - // mongoClient.getDatabase('test').getCollection('compound_indexed_entities').drop() - // mongoClient.getDatabase('test').createCollection('compound_indexed_entities') - } - void 'creates declared compound indexes for existing collections'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy index 86e3c6d6160..94881c804e4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy @@ -38,8 +38,6 @@ class MongoIndexCreationSpec extends Specification implements MongoTestPropertyP 'micronaut.data.mongodb.create-indexes' : 'true' ]) mongoClient = applicationContext.getBean(MongoClient) - // mongoClient.getDatabase('test').getCollection('indexed_entities').drop() - // mongoClient.getDatabase('test').createCollection('indexed_entities') } void 'creates declared indexes for existing collections'() { From dda7d8e7fc9bfdae4fc80ac514c402faf5fd68e0 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 30 Mar 2026 15:39:47 +0200 Subject: [PATCH 12/34] Mongo indexes, some small improvements --- .../impl/predicate/NearPredicate.java | 14 +++++ .../impl/predicate/NearSpherePredicate.java | 14 +++++ .../criteria/impl/CriteriaUtilsSpec.groovy | 60 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java index 224eb42ef01..07559c02481 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression; import jakarta.persistence.criteria.Expression; import org.jspecify.annotations.Nullable; @@ -50,6 +51,19 @@ public NearPredicate(Expression expression, if (maxDistance != null) { CriteriaUtils.requireNumericExpression(maxDistance); } + validateDistanceRange(minDistance, maxDistance); + } + + private static void validateDistanceRange(@Nullable Expression minDistance, + @Nullable Expression maxDistance) { + if (minDistance instanceof LiteralExpression minLiteral + && maxDistance instanceof LiteralExpression maxLiteral) { + Number minValue = minLiteral.getValue(); + Number maxValue = maxLiteral.getValue(); + if (minValue != null && maxValue != null && Double.compare(maxValue.doubleValue(), minValue.doubleValue()) < 0) { + throw new IllegalArgumentException("The maximum distance must be greater than or equal to the minimum distance"); + } + } } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java index 448339ec638..df753e7db31 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression; import jakarta.persistence.criteria.Expression; import org.jspecify.annotations.Nullable; @@ -50,6 +51,19 @@ public NearSpherePredicate(Expression expression, if (maxDistance != null) { CriteriaUtils.requireNumericExpression(maxDistance); } + validateDistanceRange(minDistance, maxDistance); + } + + private static void validateDistanceRange(@Nullable Expression minDistance, + @Nullable Expression maxDistance) { + if (minDistance instanceof LiteralExpression minLiteral + && maxDistance instanceof LiteralExpression maxLiteral) { + Number minValue = minLiteral.getValue(); + Number maxValue = maxLiteral.getValue(); + if (minValue != null && maxValue != null && Double.compare(maxValue.doubleValue(), minValue.doubleValue()) < 0) { + throw new IllegalArgumentException("The maximum distance must be greater than or equal to the minimum distance"); + } + } } /** diff --git a/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy index 6001596a746..d2edcdc9cbb 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy @@ -69,6 +69,66 @@ class CriteriaUtilsSpec extends Specification { parameters as List == [textSearch, geoWithinGeometry, geoIntersectsGeometry, nearGeometry, nearMin, nearSphereGeometry, nearSphereMax] } + void 'rejects near predicate when max distance is less than min distance'() { + given: + def property = Stub(DefaultPersistentPropertyPath) { + getProperty() >> Stub(PersistentProperty) + } + def geometry = parameter(Map, 'geometry') + + when: + new NearPredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(5d)) + + then: + def e = thrown(IllegalArgumentException) + e.message == 'The maximum distance must be greater than or equal to the minimum distance' + } + + void 'allows near predicate when max distance equals min distance'() { + given: + def property = Stub(DefaultPersistentPropertyPath) { + getProperty() >> Stub(PersistentProperty) + } + def geometry = parameter(Map, 'geometry') + + when: + def predicate = new NearPredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(10d)) + + then: + predicate.minDistance.value == 10d + predicate.maxDistance.value == 10d + } + + void 'rejects near sphere predicate when max distance is less than min distance'() { + given: + def property = Stub(DefaultPersistentPropertyPath) { + getProperty() >> Stub(PersistentProperty) + } + def geometry = parameter(Map, 'geometry') + + when: + new NearSpherePredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(5d)) + + then: + def e = thrown(IllegalArgumentException) + e.message == 'The maximum distance must be greater than or equal to the minimum distance' + } + + void 'allows near sphere predicate when max distance equals min distance'() { + given: + def property = Stub(DefaultPersistentPropertyPath) { + getProperty() >> Stub(PersistentProperty) + } + def geometry = parameter(Map, 'geometry') + + when: + def predicate = new NearSpherePredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(10d)) + + then: + predicate.minDistance.value == 10d + predicate.maxDistance.value == 10d + } + private static IParameterExpression parameter(Class type, String name) { new DefaultParameterExpression<>(type, name, null) } From 942bda9bbd18b796bc202d7a54a1e4cb4235f945 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 30 Mar 2026 16:43:44 +0200 Subject: [PATCH 13/34] Mongo indexes improvements --- .../mongodb/common/MongoEntityIndexes.java | 226 +++++++++-------- .../init/AbstractMongoCollectionsCreator.java | 232 +++++++++++++++++- .../mongodb/init/MongoCollectionsCreator.java | 222 +---------------- .../init/MongoReactiveCollectionsCreator.java | 222 +---------------- .../MongoJoinCollectionCreationSpec.groovy | 86 +++++++ ...MongoEmbeddedFieldIndexCreationSpec.groovy | 78 ++++++ .../MongoEmbeddedTextIndexCreationSpec.groovy | 78 ++++++ ...oIndexAdvancedOptionsResolutionSpec.groovy | 53 ++++ ...AbstractMongoCollectionsCreatorSpec.groovy | 157 ++++++++++++ 9 files changed, 816 insertions(+), 538 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 54aa282eb3b..0f0f085d526 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -17,8 +17,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.beans.BeanProperty; import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex; import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField; import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection; @@ -32,6 +30,8 @@ import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.model.Association; import io.micronaut.data.model.PersistentEntityUtils; +import io.micronaut.data.model.PersistentProperty; +import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.model.runtime.RuntimePersistentProperty; import org.bson.Document; @@ -164,17 +164,17 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist private static List resolveFieldIndexes(RuntimePersistentEntity entity) { List indexes = new ArrayList<>(); - BeanIntrospection introspection = entity.getIntrospection(); - for (BeanProperty beanProperty : introspection.getBeanProperties()) { - RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); - if (property == null || property instanceof Association) { - continue; + PersistentEntityUtils.traversePersistentProperties(entity, false, false, (associations, property) -> { + if (!(property instanceof RuntimePersistentProperty runtimeProperty) || containsNonEmbeddedAssociation(associations)) { + return; } - var annotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoIndexed.class); + String persistedPath = toPersistedPath(associations, property); + var annotationMetadata = property.getAnnotationMetadata(); + var annotation = annotationMetadata.getAnnotation(MongoIndexed.class); if (annotation != null) { indexes.add(new ResolvedIndex( annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), - List.of(new ResolvedIndexField(property.getPersistedName(), annotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC) == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)), + List.of(new ResolvedIndexField(persistedPath, annotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC) == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)), annotation.booleanValue("unique").orElse(false), annotation.booleanValue("sparse").orElse(false), annotation.booleanValue("hidden").orElse(false), @@ -193,17 +193,17 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); - continue; + return; } - var textAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); + var textAnnotation = annotationMetadata.getAnnotation(MongoTextIndexed.class); if (textAnnotation != null) { - continue; + return; } - var hashedAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoHashedIndexed.class); + var hashedAnnotation = annotationMetadata.getAnnotation(MongoHashedIndexed.class); if (hashedAnnotation != null) { indexes.add(new ResolvedIndex( hashedAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), - List.of(new ResolvedIndexField(property.getPersistedName(), null, null, "hashed", null, null)), + List.of(new ResolvedIndexField(persistedPath, null, null, "hashed", null, null)), false, false, hashedAnnotation.booleanValue("hidden").orElse(false), @@ -222,11 +222,11 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); - continue; + return; } - var geoAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoGeoIndexed.class); + var geoAnnotation = annotationMetadata.getAnnotation(MongoGeoIndexed.class); if (geoAnnotation != null) { - validateGeoIndexedType(entity, property); + validateGeoIndexedType(entity, runtimeProperty); MongoGeoIndexType type = geoAnnotation.enumValue("type", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); Integer sphereVersion = geoAnnotation.intValue("sphereVersion").isPresent() && geoAnnotation.intValue("sphereVersion").getAsInt() >= 0 ? geoAnnotation.intValue("sphereVersion").getAsInt() : null; Integer bits = geoAnnotation.intValue("bits").isPresent() && geoAnnotation.intValue("bits").getAsInt() >= 0 ? geoAnnotation.intValue("bits").getAsInt() : null; @@ -240,7 +240,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), - List.of(new ResolvedIndexField(property.getPersistedName(), null, null, type.getKey(), min, max)), + List.of(new ResolvedIndexField(persistedPath, null, null, type.getKey(), min, max)), false, false, geoAnnotation.booleanValue("hidden").orElse(false), @@ -259,9 +259,9 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), null )); - continue; + return; } - var wildcardAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoWildcardIndexed.class); + var wildcardAnnotation = annotationMetadata.getAnnotation(MongoWildcardIndexed.class); if (wildcardAnnotation != null) { String wildcardProjection = wildcardAnnotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null); if (wildcardProjection != null) { @@ -269,7 +269,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), - List.of(new ResolvedIndexField(property.getPersistedName() + ".$**", 1, null, null, null, null)), + List.of(new ResolvedIndexField(persistedPath + ".$**", 1, null, null, null, null)), false, false, wildcardAnnotation.booleanValue("hidden").orElse(false), @@ -289,7 +289,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity entity, } private static List resolveTextIndexes(RuntimePersistentEntity entity) { - List fields = new ArrayList<>(); - String name = null; - Boolean hidden = null; - String storageEngine = null; - String comment = null; - String defaultLanguage = null; - String languageOverride = null; - Integer textIndexVersion = null; - BeanIntrospection introspection = entity.getIntrospection(); - for (BeanProperty beanProperty : introspection.getBeanProperties()) { - RuntimePersistentProperty property = entity.getPropertyByName(beanProperty.getName()); - if (property == null || property instanceof Association) { - continue; + TextIndexState state = new TextIndexState(); + PersistentEntityUtils.traversePersistentProperties(entity, false, false, (associations, property) -> { + if (!(property instanceof RuntimePersistentProperty) || containsNonEmbeddedAssociation(associations)) { + return; } - var textAnnotation = beanProperty.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); - if (textAnnotation != null) { - int weight = textAnnotation.intValue("weight").orElse(1); - if (weight <= 0) { - throw new IllegalStateException("Mongo text index weight must be greater than zero for entity [" + entity.getName() + "]"); - } - String declaredName = textAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null); - if (name == null) { - name = declaredName; - } else if (declaredName != null && !declaredName.equals(name)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same index name"); - } - boolean declaredHidden = textAnnotation.booleanValue("hidden").orElse(false); - if (hidden == null) { - hidden = declaredHidden; - } else if (!hidden.equals(declaredHidden)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same hidden option"); - } - String declaredComment = textAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null); - if (comment == null) { - comment = declaredComment; - } else if (!Objects.equals(comment, declaredComment)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same comment option"); - } - String declaredStorageEngine = parseJsonOption(textAnnotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()); - if (storageEngine == null) { - storageEngine = declaredStorageEngine; - } else if (!Objects.equals(storageEngine, declaredStorageEngine)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same storageEngine option"); - } - String declaredDefaultLanguage = textAnnotation.stringValue("defaultLanguage").filter(s -> !s.isEmpty()).orElse(null); - if (defaultLanguage == null) { - defaultLanguage = declaredDefaultLanguage; - } else if (!Objects.equals(defaultLanguage, declaredDefaultLanguage)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same defaultLanguage option"); - } - String declaredLanguageOverride = textAnnotation.stringValue("languageOverride").filter(s -> !s.isEmpty()).orElse(null); - if (languageOverride == null) { - languageOverride = declaredLanguageOverride; - } else if (!Objects.equals(languageOverride, declaredLanguageOverride)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same languageOverride option"); - } - Integer declaredTextIndexVersion = textAnnotation.intValue("textIndexVersion").isPresent() && textAnnotation.intValue("textIndexVersion").getAsInt() >= 0 - ? textAnnotation.intValue("textIndexVersion").getAsInt() : null; - if (declaredTextIndexVersion != null && declaredTextIndexVersion <= 0) { - throw new IllegalStateException("Mongo text index version must be greater than zero for entity [" + entity.getName() + "]"); - } - if (textIndexVersion == null) { - textIndexVersion = declaredTextIndexVersion; - } else if (!Objects.equals(textIndexVersion, declaredTextIndexVersion)) { - throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same textIndexVersion option"); - } - fields.add(new ResolvedIndexField(property.getPersistedName(), null, weight, "text", null, null)); + var textAnnotation = property.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); + if (textAnnotation == null) { + return; } - } - if (fields.isEmpty()) { + int weight = textAnnotation.intValue("weight").orElse(1); + if (weight <= 0) { + throw new IllegalStateException("Mongo text index weight must be greater than zero for entity [" + entity.getName() + "]"); + } + String declaredName = textAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null); + if (state.name == null) { + state.name = declaredName; + } else if (declaredName != null && !declaredName.equals(state.name)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same index name"); + } + boolean declaredHidden = textAnnotation.booleanValue("hidden").orElse(false); + if (state.hidden == null) { + state.hidden = declaredHidden; + } else if (!state.hidden.equals(declaredHidden)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same hidden option"); + } + String declaredComment = textAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null); + if (state.comment == null) { + state.comment = declaredComment; + } else if (!Objects.equals(state.comment, declaredComment)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same comment option"); + } + String declaredStorageEngine = parseJsonOption(textAnnotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()); + if (state.storageEngine == null) { + state.storageEngine = declaredStorageEngine; + } else if (!Objects.equals(state.storageEngine, declaredStorageEngine)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same storageEngine option"); + } + String declaredDefaultLanguage = textAnnotation.stringValue("defaultLanguage").filter(s -> !s.isEmpty()).orElse(null); + if (state.defaultLanguage == null) { + state.defaultLanguage = declaredDefaultLanguage; + } else if (!Objects.equals(state.defaultLanguage, declaredDefaultLanguage)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same defaultLanguage option"); + } + String declaredLanguageOverride = textAnnotation.stringValue("languageOverride").filter(s -> !s.isEmpty()).orElse(null); + if (state.languageOverride == null) { + state.languageOverride = declaredLanguageOverride; + } else if (!Objects.equals(state.languageOverride, declaredLanguageOverride)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same languageOverride option"); + } + Integer declaredTextIndexVersion = textAnnotation.intValue("textIndexVersion").isPresent() && textAnnotation.intValue("textIndexVersion").getAsInt() >= 0 + ? textAnnotation.intValue("textIndexVersion").getAsInt() : null; + if (declaredTextIndexVersion != null && declaredTextIndexVersion <= 0) { + throw new IllegalStateException("Mongo text index version must be greater than zero for entity [" + entity.getName() + "]"); + } + if (state.textIndexVersion == null) { + state.textIndexVersion = declaredTextIndexVersion; + } else if (!Objects.equals(state.textIndexVersion, declaredTextIndexVersion)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same textIndexVersion option"); + } + state.fields.add(new ResolvedIndexField(toPersistedPath(associations, property), null, weight, "text", null, null)); + }); + if (state.fields.isEmpty()) { return List.of(); } - return List.of(new ResolvedIndex(name, List.copyOf(fields), false, false, hidden != null && hidden, null, null, null, null, null, null, defaultLanguage, languageOverride, textIndexVersion, null, null, storageEngine, comment, null)); + return List.of(new ResolvedIndex(state.name, List.copyOf(state.fields), false, false, state.hidden != null && state.hidden, null, null, null, null, null, null, state.defaultLanguage, state.languageOverride, state.textIndexVersion, null, null, state.storageEngine, state.comment, null)); } private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { @@ -399,18 +391,7 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (propertyPath == null) { throw new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]"); } - StringBuilder resolved = new StringBuilder(); - for (Association association : propertyPath.getAssociations()) { - if (!resolved.isEmpty()) { - resolved.append('.'); - } - resolved.append(association.getPersistedName()); - } - if (!resolved.isEmpty()) { - resolved.append('.'); - } - resolved.append(propertyPath.getProperty().getPersistedName()); - return resolved.toString(); + return toPersistedPath(propertyPath); }) .orElseThrow(() -> new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]")); if (!seenPaths.add(persistedPath)) { @@ -493,6 +474,34 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit return indexes; } + private static boolean containsNonEmbeddedAssociation(List associations) { + for (Association association : associations) { + if (!(association instanceof io.micronaut.data.model.Embedded)) { + return true; + } + } + return false; + } + + private static String toPersistedPath(List associations, PersistentProperty property) { + StringBuilder resolved = new StringBuilder(); + for (Association association : associations) { + if (!resolved.isEmpty()) { + resolved.append('.'); + } + resolved.append(association.getPersistedName()); + } + if (!resolved.isEmpty()) { + resolved.append('.'); + } + resolved.append(property.getPersistedName()); + return resolved.toString(); + } + + private static String toPersistedPath(PersistentPropertyPath propertyPath) { + return toPersistedPath(propertyPath.getAssociations(), propertyPath.getProperty()); + } + private static @Nullable String parseJsonOption(@Nullable String json, String option, String entityName) { @@ -506,6 +515,17 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit } } + private static final class TextIndexState { + private final List fields = new ArrayList<>(); + private @Nullable String name; + private @Nullable Boolean hidden; + private @Nullable String storageEngine; + private @Nullable String comment; + private @Nullable String defaultLanguage; + private @Nullable String languageOverride; + private @Nullable Integer textIndexVersion; + } + /** * Resolved Mongo index definition. * diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index e20aa1cfe39..968f20d1c5c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -18,6 +18,11 @@ import com.mongodb.MongoException; import com.mongodb.MongoCommandException; import com.mongodb.MongoWriteException; +import com.mongodb.client.model.Collation; +import com.mongodb.client.model.CollationStrength; +import com.mongodb.client.model.ClusteredIndexOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.IndexOptions; import com.mongodb.WriteError; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.configuration.mongo.core.DefaultMongoConfiguration; @@ -48,6 +53,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -184,7 +190,7 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, } } if (createCollections) { - createJoinCollections(databaseOperations, database, collections, entity, persistedName); + createJoinCollections(databaseOperations, database, collections, entity); } } if (createIndexes) { @@ -208,8 +214,7 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, private void createJoinCollections(DatabaseOperations databaseOperations, Dtbs database, Set collections, - PersistentEntity entity, - String persistedName) { + PersistentEntity entity) { for (PersistentProperty persistentProperty : entity.getPersistentProperties()) { if (persistentProperty instanceof Association association) { Optional inverseSide = association.getInverseSide().map(Function.identity()); @@ -219,7 +224,7 @@ private void createJoinCollections(DatabaseOperations databaseOperations, String joinCollectionName = namingStrategy.mappedName(owningAssociation); if (collections.add(joinCollectionName)) { if (LOG.isInfoEnabled()) { - LOG.info("Creating collection: {} in database: {}", persistedName, databaseOperations.getDatabaseName(database)); + LOG.info("Creating collection: {} in database: {}", joinCollectionName, databaseOperations.getDatabaseName(database)); } databaseOperations.createCollection(database, joinCollectionName, null); } @@ -242,7 +247,7 @@ private MongoResolvedCollectionOptions resolveCollectionOptions(PersistentEntity Integer expireAfterSeconds = annotation.intValue("expireAfterSeconds").isPresent() && annotation.intValue("expireAfterSeconds").getAsInt() >= 0 ? annotation.intValue("expireAfterSeconds").getAsInt() : null; if (expireAfterSeconds != null) { - PersistentProperty identity = entity.getIdentity(); + PersistentProperty identity = entity.hasIdentity() ? entity.getIdentity() : null; if (identity == null) { throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires an identity property"); } @@ -332,6 +337,223 @@ private List resolveIndexes(PersistentEntity entity) { return indexes; } + static CreateCollectionOptions toCreateCollectionOptions(MongoResolvedCollectionOptions options) { + CreateCollectionOptions collectionOptions = new CreateCollectionOptions(); + ClusteredIndexOptions clusteredIndexOptions = new ClusteredIndexOptions(new Document("_id", 1), options.clusteredIndexUnique()); + if (options.clusteredIndexName() != null) { + clusteredIndexOptions.name(options.clusteredIndexName()); + } + collectionOptions.clusteredIndexOptions(clusteredIndexOptions); + if (options.expireAfterSeconds() != null) { + collectionOptions.expireAfter(options.expireAfterSeconds().longValue(), java.util.concurrent.TimeUnit.SECONDS); + } + return collectionOptions; + } + + static @Nullable MongoResolvedCollectionOptions toResolvedCollectionOptions(Document collectionDocument) { + Document options = collectionDocument.get("options", Document.class); + if (options == null) { + return null; + } + Document clustered = options.get("clusteredIndex", Document.class); + if (clustered == null) { + return null; + } + return new MongoResolvedCollectionOptions( + clustered.getString("name"), + clustered.getBoolean("unique", true), + options.getInteger("expireAfterSeconds") + ); + } + + static @Nullable MongoResolvedIndex toResolvedIndex(Document indexDocument) { + Document keyDocument = indexDocument.get("key", Document.class); + if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { + return null; + } + return new MongoResolvedIndex( + indexDocument.getString("name"), + List.copyOf(toResolvedIndexFields(indexDocument, keyDocument)), + indexDocument.getBoolean("unique", false), + indexDocument.getBoolean("sparse", false), + indexDocument.getBoolean("hidden", false), + indexDocument.getInteger("expireAfterSeconds"), + normalizeJsonValue(indexDocument.get("partialFilterExpression")), + normalizeJsonValue(indexDocument.get("collation")), + toInteger(indexDocument.get("bits")), + toDouble(indexDocument.get("min")), + toDouble(indexDocument.get("max")), + indexDocument.getString("default_language"), + indexDocument.getString("language_override"), + toInteger(indexDocument.get("textIndexVersion")), + toInteger(indexDocument.get("2dsphereIndexVersion")), + normalizeJsonValue(indexDocument.get("wildcardProjection")), + normalizeJsonValue(indexDocument.get("storageEngine")), + null, + null + ); + } + + static List toResolvedIndexFields(Document indexDocument, Document keyDocument) { + if ("text".equals(keyDocument.getString("_fts"))) { + Document weights = indexDocument.get("weights", Document.class); + if (weights != null && !weights.isEmpty()) { + List fields = new ArrayList<>(weights.size()); + for (Map.Entry entry : weights.entrySet()) { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); + } + return fields; + } + } + List fields = new ArrayList<>(keyDocument.size()); + for (Map.Entry entry : keyDocument.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); + } + } + return fields; + } + + static IndexOptions toIndexOptions(MongoResolvedIndex index) { + IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); + if (index.hidden()) { + indexOptions.hidden(true); + } + if (index.name() != null) { + indexOptions.name(index.name()); + } + if (index.expireAfterSeconds() != null) { + indexOptions.expireAfter((long) index.expireAfterSeconds(), java.util.concurrent.TimeUnit.SECONDS); + } + if (index.partialFilterExpression() != null) { + indexOptions.partialFilterExpression(Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexOptions.collation(toCollation(Document.parse(index.collation()))); + } + if (index.bits() != null) { + indexOptions.bits(index.bits()); + } + if (index.min() != null) { + indexOptions.min(index.min()); + } + if (index.max() != null) { + indexOptions.max(index.max()); + } + if (index.defaultLanguage() != null) { + indexOptions.defaultLanguage(index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexOptions.languageOverride(index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexOptions.textVersion(index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexOptions.sphereVersion(index.sphereVersion()); + } + if (index.wildcardProjection() != null) { + indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); + } + if (index.storageEngine() != null) { + indexOptions.storageEngine(Document.parse(index.storageEngine())); + } + return indexOptions; + } + + static Document toCreateIndexesCommandDocument(String collection, MongoResolvedIndex index) { + Document command = new Document("createIndexes", collection) + .append("indexes", List.of(toIndexCommandDocument(index))); + if (index.comment() != null) { + command.append("comment", index.comment()); + } + if (index.commitQuorum() != null) { + command.append("commitQuorum", toCommitQuorumValue(index.commitQuorum())); + } + return command; + } + + static Document toIndexCommandDocument(MongoResolvedIndex index) { + Document indexDocument = new Document("key", index.keysDocument()); + if (index.name() != null) { + indexDocument.append("name", index.name()); + } + if (index.unique()) { + indexDocument.append("unique", true); + } + if (index.sparse()) { + indexDocument.append("sparse", true); + } + if (index.hidden()) { + indexDocument.append("hidden", true); + } + if (index.expireAfterSeconds() != null) { + indexDocument.append("expireAfterSeconds", index.expireAfterSeconds()); + } + if (index.partialFilterExpression() != null) { + indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); + } + if (index.collation() != null) { + indexDocument.append("collation", Document.parse(index.collation())); + } + if (index.bits() != null) { + indexDocument.append("bits", index.bits()); + } + if (index.min() != null) { + indexDocument.append("min", index.min()); + } + if (index.max() != null) { + indexDocument.append("max", index.max()); + } + if (index.defaultLanguage() != null) { + indexDocument.append("default_language", index.defaultLanguage()); + } + if (index.languageOverride() != null) { + indexDocument.append("language_override", index.languageOverride()); + } + if (index.textIndexVersion() != null) { + indexDocument.append("textIndexVersion", index.textIndexVersion()); + } + if (index.sphereVersion() != null) { + indexDocument.append("2dsphereIndexVersion", index.sphereVersion()); + } + if (index.wildcardProjection() != null) { + indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); + } + if (index.storageEngine() != null) { + indexDocument.append("storageEngine", Document.parse(index.storageEngine())); + } + return indexDocument; + } + + static Object toCommitQuorumValue(String commitQuorum) { + try { + return Integer.parseInt(commitQuorum); + } catch (NumberFormatException ignored) { + return commitQuorum; + } + } + + static Collation toCollation(Document document) { + Collation.Builder builder = Collation.builder(); + String locale = document.getString("locale"); + if (locale != null) { + builder.locale(locale); + } + Integer strength = document.getInteger("strength"); + if (strength != null) { + builder.collationStrength(CollationStrength.fromInt(strength)); + } + Boolean caseLevel = document.getBoolean("caseLevel"); + if (caseLevel != null) { + builder.caseLevel(caseLevel); + } + return builder.build(); + } + static @Nullable String normalizeJsonValue(@Nullable Object value) { if (value == null) { return null; diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index d0481b6417e..01da955a92f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -18,11 +18,6 @@ import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.Collation; -import com.mongodb.client.model.CollationStrength; -import com.mongodb.client.model.ClusteredIndexOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.IndexOptions; import io.micronaut.configuration.mongo.core.AbstractMongoConfiguration; import io.micronaut.context.BeanLocator; import io.micronaut.context.annotation.Context; @@ -46,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; /** * MongoDB's collections creator. @@ -95,16 +89,7 @@ public void createCollection(MongoDatabase database, String collection, @Nullabl database.createCollection(collection); return; } - CreateCollectionOptions collectionOptions = new CreateCollectionOptions(); - ClusteredIndexOptions clusteredIndexOptions = new ClusteredIndexOptions(new Document("_id", 1), options.clusteredIndexUnique()); - if (options.clusteredIndexName() != null) { - clusteredIndexOptions.name(options.clusteredIndexName()); - } - collectionOptions.clusteredIndexOptions(clusteredIndexOptions); - if (options.expireAfterSeconds() != null) { - collectionOptions.expireAfter(options.expireAfterSeconds().longValue(), TimeUnit.SECONDS); - } - database.createCollection(collection, collectionOptions); + database.createCollection(collection, toCreateCollectionOptions(options)); } @Override @@ -113,19 +98,7 @@ public void createCollection(MongoDatabase database, String collection, @Nullabl if (!collection.equals(collectionDocument.getString("name"))) { continue; } - Document options = collectionDocument.get("options", Document.class); - if (options == null) { - return null; - } - Document clustered = options.get("clusteredIndex", Document.class); - if (clustered == null) { - return null; - } - return new MongoResolvedCollectionOptions( - clustered.getString("name"), - clustered.getBoolean("unique", true), - options.getInteger("expireAfterSeconds") - ); + return toResolvedCollectionOptions(collectionDocument); } return null; } @@ -135,61 +108,10 @@ public List listIndexes(MongoDatabase database, String colle MongoCollection mongoCollection = database.getCollection(collection); List indexes = new ArrayList<>(); for (Document indexDocument : mongoCollection.listIndexes()) { - Document keyDocument = indexDocument.get("key", Document.class); - if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { - continue; - } - List fields; - if ("text".equals(keyDocument.getString("_fts"))) { - Document weights = indexDocument.get("weights", Document.class); - if (weights != null && !weights.isEmpty()) { - fields = new ArrayList<>(weights.size()); - for (Map.Entry entry : weights.entrySet()) { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); - } - } else { - fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); - } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); - } - } - } - } else { - fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); - } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); - } - } + MongoResolvedIndex resolvedIndex = toResolvedIndex(indexDocument); + if (resolvedIndex != null) { + indexes.add(resolvedIndex); } - indexes.add(new MongoResolvedIndex( - indexDocument.getString("name"), - List.copyOf(fields), - indexDocument.getBoolean("unique", false), - indexDocument.getBoolean("sparse", false), - indexDocument.getBoolean("hidden", false), - indexDocument.getInteger("expireAfterSeconds"), - normalizeJsonValue(indexDocument.get("partialFilterExpression")), - normalizeJsonValue(indexDocument.get("collation")), - toInteger(indexDocument.get("bits")), - toDouble(indexDocument.get("min")), - toDouble(indexDocument.get("max")), - indexDocument.getString("default_language"), - indexDocument.getString("language_override"), - toInteger(indexDocument.get("textIndexVersion")), - toInteger(indexDocument.get("2dsphereIndexVersion")), - normalizeJsonValue(indexDocument.get("wildcardProjection")), - normalizeJsonValue(indexDocument.get("storageEngine")), - null, - null - )); } return indexes; } @@ -198,142 +120,12 @@ public List listIndexes(MongoDatabase database, String colle public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { MongoCollection mongoCollection = database.getCollection(collection); if (index.comment() != null || index.commitQuorum() != null) { - Document command = new Document("createIndexes", collection) - .append("indexes", List.of(toIndexCommandDocument(index))); - if (index.comment() != null) { - command.append("comment", index.comment()); - } - if (index.commitQuorum() != null) { - command.append("commitQuorum", toCommitQuorumValue(index.commitQuorum())); - } - database.runCommand(command); + database.runCommand(toCreateIndexesCommandDocument(collection, index)); return; } - IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); - if (index.hidden()) { - indexOptions.hidden(true); - } - if (index.name() != null) { - indexOptions.name(index.name()); - } - if (index.expireAfterSeconds() != null) { - indexOptions.expireAfter((long) index.expireAfterSeconds(), java.util.concurrent.TimeUnit.SECONDS); - } - if (index.partialFilterExpression() != null) { - indexOptions.partialFilterExpression(Document.parse(index.partialFilterExpression())); - } - if (index.collation() != null) { - indexOptions.collation(toCollation(Document.parse(index.collation()))); - } - if (index.bits() != null) { - indexOptions.bits(index.bits()); - } - if (index.min() != null) { - indexOptions.min(index.min()); - } - if (index.max() != null) { - indexOptions.max(index.max()); - } - if (index.defaultLanguage() != null) { - indexOptions.defaultLanguage(index.defaultLanguage()); - } - if (index.languageOverride() != null) { - indexOptions.languageOverride(index.languageOverride()); - } - if (index.textIndexVersion() != null) { - indexOptions.textVersion(index.textIndexVersion()); - } - if (index.sphereVersion() != null) { - indexOptions.sphereVersion(index.sphereVersion()); - } - if (index.wildcardProjection() != null) { - indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); - } - if (index.storageEngine() != null) { - indexOptions.storageEngine(Document.parse(index.storageEngine())); - } - mongoCollection.createIndex(index.keysDocument(), indexOptions); + mongoCollection.createIndex(index.keysDocument(), toIndexOptions(index)); } }; }, mongoCollectionNameProvider); } - - private Document toIndexCommandDocument(MongoResolvedIndex index) { - Document indexDocument = new Document("key", index.keysDocument()); - if (index.name() != null) { - indexDocument.append("name", index.name()); - } - if (index.unique()) { - indexDocument.append("unique", true); - } - if (index.sparse()) { - indexDocument.append("sparse", true); - } - if (index.hidden()) { - indexDocument.append("hidden", true); - } - if (index.expireAfterSeconds() != null) { - indexDocument.append("expireAfterSeconds", index.expireAfterSeconds()); - } - if (index.partialFilterExpression() != null) { - indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); - } - if (index.collation() != null) { - indexDocument.append("collation", Document.parse(index.collation())); - } - if (index.bits() != null) { - indexDocument.append("bits", index.bits()); - } - if (index.min() != null) { - indexDocument.append("min", index.min()); - } - if (index.max() != null) { - indexDocument.append("max", index.max()); - } - if (index.defaultLanguage() != null) { - indexDocument.append("default_language", index.defaultLanguage()); - } - if (index.languageOverride() != null) { - indexDocument.append("language_override", index.languageOverride()); - } - if (index.textIndexVersion() != null) { - indexDocument.append("textIndexVersion", index.textIndexVersion()); - } - if (index.sphereVersion() != null) { - indexDocument.append("2dsphereIndexVersion", index.sphereVersion()); - } - if (index.wildcardProjection() != null) { - indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); - } - if (index.storageEngine() != null) { - indexDocument.append("storageEngine", Document.parse(index.storageEngine())); - } - return indexDocument; - } - - private Object toCommitQuorumValue(String commitQuorum) { - try { - return Integer.parseInt(commitQuorum); - } catch (NumberFormatException ignored) { - return commitQuorum; - } - } - - private Collation toCollation(Document document) { - Collation.Builder builder = Collation.builder(); - String locale = document.getString("locale"); - if (locale != null) { - builder.locale(locale); - } - Integer strength = document.getInteger("strength"); - if (strength != null) { - builder.collationStrength(CollationStrength.fromInt(strength)); - } - Boolean caseLevel = document.getBoolean("caseLevel"); - if (caseLevel != null) { - builder.caseLevel(caseLevel); - } - return builder.build(); - } - } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index 9c30be723ce..5aba46cb73a 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -15,11 +15,6 @@ */ package io.micronaut.data.mongodb.init; -import com.mongodb.client.model.Collation; -import com.mongodb.client.model.CollationStrength; -import com.mongodb.client.model.ClusteredIndexOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; @@ -48,7 +43,6 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import java.util.concurrent.TimeUnit; /** * MongoDB's reactive collections creator. @@ -97,16 +91,7 @@ public void createCollection(MongoDatabase database, String collection, @Nullabl Mono.from(database.createCollection(collection)).block(); return; } - CreateCollectionOptions collectionOptions = new CreateCollectionOptions(); - ClusteredIndexOptions clusteredIndexOptions = new ClusteredIndexOptions(new Document("_id", 1), options.clusteredIndexUnique()); - if (options.clusteredIndexName() != null) { - clusteredIndexOptions.name(options.clusteredIndexName()); - } - collectionOptions.clusteredIndexOptions(clusteredIndexOptions); - if (options.expireAfterSeconds() != null) { - collectionOptions.expireAfter(options.expireAfterSeconds().longValue(), TimeUnit.SECONDS); - } - Mono.from(database.createCollection(collection, collectionOptions)).block(); + Mono.from(database.createCollection(collection, toCreateCollectionOptions(options))).block(); } @Override @@ -118,19 +103,7 @@ public void createCollection(MongoDatabase database, String collection, @Nullabl if (collectionDocument == null) { return null; } - Document options = collectionDocument.get("options", Document.class); - if (options == null) { - return null; - } - Document clustered = options.get("clusteredIndex", Document.class); - if (clustered == null) { - return null; - } - return new MongoResolvedCollectionOptions( - clustered.getString("name"), - clustered.getBoolean("unique", true), - options.getInteger("expireAfterSeconds") - ); + return toResolvedCollectionOptions(collectionDocument); } @Override @@ -141,61 +114,10 @@ public List listIndexes(MongoDatabase database, String colle .map(indexDocuments -> { List resolvedIndexes = new ArrayList<>(); for (Document indexDocument : indexDocuments) { - Document keyDocument = indexDocument.get("key", Document.class); - if (keyDocument == null || (keyDocument.size() == 1 && keyDocument.getInteger("_id", 0) == 1)) { - continue; - } - List fields; - if ("text".equals(keyDocument.getString("_fts"))) { - Document weights = indexDocument.get("weights", Document.class); - if (weights != null && !weights.isEmpty()) { - fields = new ArrayList<>(weights.size()); - for (Map.Entry entry : weights.entrySet()) { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); - } - } else { - fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); - } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); - } - } - } - } else { - fields = new ArrayList<>(keyDocument.size()); - for (Map.Entry entry : keyDocument.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number number) { - fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); - } else { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); - } - } + MongoResolvedIndex resolvedIndex = toResolvedIndex(indexDocument); + if (resolvedIndex != null) { + resolvedIndexes.add(resolvedIndex); } - resolvedIndexes.add(new MongoResolvedIndex( - indexDocument.getString("name"), - List.copyOf(fields), - indexDocument.getBoolean("unique", false), - indexDocument.getBoolean("sparse", false), - indexDocument.getBoolean("hidden", false), - indexDocument.getInteger("expireAfterSeconds"), - normalizeJsonValue(indexDocument.get("partialFilterExpression")), - normalizeJsonValue(indexDocument.get("collation")), - toInteger(indexDocument.get("bits")), - toDouble(indexDocument.get("min")), - toDouble(indexDocument.get("max")), - indexDocument.getString("default_language"), - indexDocument.getString("language_override"), - toInteger(indexDocument.get("textIndexVersion")), - toInteger(indexDocument.get("2dsphereIndexVersion")), - normalizeJsonValue(indexDocument.get("wildcardProjection")), - normalizeJsonValue(indexDocument.get("storageEngine")), - null, - null - )); } return resolvedIndexes; }) @@ -207,142 +129,12 @@ public List listIndexes(MongoDatabase database, String colle public void createIndex(MongoDatabase database, String collection, MongoResolvedIndex index) { MongoCollection mongoCollection = database.getCollection(collection); if (index.comment() != null || index.commitQuorum() != null) { - Document command = new Document("createIndexes", collection) - .append("indexes", List.of(toIndexCommandDocument(index))); - if (index.comment() != null) { - command.append("comment", index.comment()); - } - if (index.commitQuorum() != null) { - command.append("commitQuorum", toCommitQuorumValue(index.commitQuorum())); - } - Mono.from(database.runCommand(command)).block(); + Mono.from(database.runCommand(toCreateIndexesCommandDocument(collection, index))).block(); return; } - IndexOptions indexOptions = new IndexOptions().unique(index.unique()).sparse(index.sparse()); - if (index.hidden()) { - indexOptions.hidden(true); - } - if (index.name() != null) { - indexOptions.name(index.name()); - } - if (index.expireAfterSeconds() != null) { - indexOptions.expireAfter((long) index.expireAfterSeconds(), java.util.concurrent.TimeUnit.SECONDS); - } - if (index.partialFilterExpression() != null) { - indexOptions.partialFilterExpression(Document.parse(index.partialFilterExpression())); - } - if (index.collation() != null) { - indexOptions.collation(toCollation(Document.parse(index.collation()))); - } - if (index.bits() != null) { - indexOptions.bits(index.bits()); - } - if (index.min() != null) { - indexOptions.min(index.min()); - } - if (index.max() != null) { - indexOptions.max(index.max()); - } - if (index.defaultLanguage() != null) { - indexOptions.defaultLanguage(index.defaultLanguage()); - } - if (index.languageOverride() != null) { - indexOptions.languageOverride(index.languageOverride()); - } - if (index.textIndexVersion() != null) { - indexOptions.textVersion(index.textIndexVersion()); - } - if (index.sphereVersion() != null) { - indexOptions.sphereVersion(index.sphereVersion()); - } - if (index.wildcardProjection() != null) { - indexOptions.wildcardProjection(Document.parse(index.wildcardProjection())); - } - if (index.storageEngine() != null) { - indexOptions.storageEngine(Document.parse(index.storageEngine())); - } - Mono.from(mongoCollection.createIndex(index.keysDocument(), indexOptions)).block(); + Mono.from(mongoCollection.createIndex(index.keysDocument(), toIndexOptions(index))).block(); } }; }, mongoCollectionNameProvider); } - - private Document toIndexCommandDocument(MongoResolvedIndex index) { - Document indexDocument = new Document("key", index.keysDocument()); - if (index.name() != null) { - indexDocument.append("name", index.name()); - } - if (index.unique()) { - indexDocument.append("unique", true); - } - if (index.sparse()) { - indexDocument.append("sparse", true); - } - if (index.hidden()) { - indexDocument.append("hidden", true); - } - if (index.expireAfterSeconds() != null) { - indexDocument.append("expireAfterSeconds", index.expireAfterSeconds()); - } - if (index.partialFilterExpression() != null) { - indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); - } - if (index.collation() != null) { - indexDocument.append("collation", Document.parse(index.collation())); - } - if (index.bits() != null) { - indexDocument.append("bits", index.bits()); - } - if (index.min() != null) { - indexDocument.append("min", index.min()); - } - if (index.max() != null) { - indexDocument.append("max", index.max()); - } - if (index.defaultLanguage() != null) { - indexDocument.append("default_language", index.defaultLanguage()); - } - if (index.languageOverride() != null) { - indexDocument.append("language_override", index.languageOverride()); - } - if (index.textIndexVersion() != null) { - indexDocument.append("textIndexVersion", index.textIndexVersion()); - } - if (index.sphereVersion() != null) { - indexDocument.append("2dsphereIndexVersion", index.sphereVersion()); - } - if (index.wildcardProjection() != null) { - indexDocument.append("wildcardProjection", Document.parse(index.wildcardProjection())); - } - if (index.storageEngine() != null) { - indexDocument.append("storageEngine", Document.parse(index.storageEngine())); - } - return indexDocument; - } - - private Object toCommitQuorumValue(String commitQuorum) { - try { - return Integer.parseInt(commitQuorum); - } catch (NumberFormatException ignored) { - return commitQuorum; - } - } - - private Collation toCollation(Document document) { - Collation.Builder builder = Collation.builder(); - String locale = document.getString("locale"); - if (locale != null) { - builder.locale(locale); - } - Integer strength = document.getInteger("strength"); - if (strength != null) { - builder.collationStrength(CollationStrength.fromInt(strength)); - } - Boolean caseLevel = document.getBoolean("caseLevel"); - if (caseLevel != null) { - builder.caseLevel(caseLevel); - } - return builder.build(); - } - } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy new file mode 100644 index 00000000000..1f1f464e0e6 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy @@ -0,0 +1,86 @@ +package io.micronaut.data.document.mongodb.joincollection + +import com.mongodb.client.MongoClient +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Relation +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoJoinCollectionCreationSpec extends Specification implements MongoTestPropertyProvider { + @AutoCleanup + @Shared + ApplicationContext applicationContext + + @Shared + MongoClient mongoClient + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.joincollection'] + } + + def setupSpec() { + applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'false' + ]) + mongoClient = applicationContext.getBean(MongoClient) + } + + void 'creates join collection for many-to-many association at startup'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) + conditions.eventually { + def collectionNames = mongoClient.getDatabase('test').listCollectionNames().into([]) + assert collectionNames.contains('m2m_student') + assert collectionNames.contains('m2m_course') + assert collectionNames.contains('student_course') + } + } +} + +@MongoRepository +interface JoinCollectionStudentRepository extends CrudRepository { +} + +@MongoRepository +interface JoinCollectionCourseRepository extends CrudRepository { +} + +@EqualsAndHashCode(includes = 'id') +@MappedEntity('m2m_student') +class JoinCollectionStudent { + @Id + @GeneratedValue + String id + + String name + + @Relation(value = Relation.Kind.MANY_TO_MANY, cascade = Relation.Cascade.PERSIST) + List courses +} + +@EqualsAndHashCode(includes = 'id') +@MappedEntity('m2m_course') +class JoinCollectionCourse { + @Id + @GeneratedValue + String id + + String name + + @Relation(value = Relation.Kind.MANY_TO_MANY, mappedBy = 'courses') + List students +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy new file mode 100644 index 00000000000..c81cc1a68c0 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy @@ -0,0 +1,78 @@ +package io.micronaut.data.document.mongodb.simple.embedded + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Embeddable +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Relation +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoEmbeddedFieldIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.simple.embedded'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates field index declared inside embedded type'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'embedded_field_indexed_entities') + assert indexes*.name.contains('embedded_state_idx') + def index = indexes.find { it.name == 'embedded_state_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'location.state' + assert index.fields[0].order() == 1 + } + } +} + +@MongoRepository +interface EmbeddedFieldIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('embedded_field_indexed_entities') +class EmbeddedFieldIndexedEntity { + @Id + @GeneratedValue + String id + + @Relation(Relation.Kind.EMBEDDED) + EmbeddedLocation location +} + +@Embeddable +class EmbeddedLocation { + @MongoIndexed(name = 'embedded_state_idx') + String state + + String city +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..a7b386d0f81 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy @@ -0,0 +1,78 @@ +package io.micronaut.data.document.mongodb.text.embedded + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Embeddable +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Relation +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoEmbeddedTextIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.text.embedded'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates text index declared inside embedded type'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'embedded_text_indexed_entities') + assert indexes*.name.contains('embedded_text_idx') + def index = indexes.find { it.name == 'embedded_text_idx' } + assert index.fields.size() == 2 + assert index.fields*.path().contains('_fts') + assert index.fields*.path().contains('_ftsx') + } + } +} + +@MongoRepository +interface EmbeddedTextIndexedEntityRepository extends CrudRepository { +} + +@MappedEntity('embedded_text_indexed_entities') +class EmbeddedTextIndexedEntity { + @Id + @GeneratedValue + String id + + @Relation(Relation.Kind.EMBEDDED) + EmbeddedTextDetails details +} + +@Embeddable +class EmbeddedTextDetails { + String title + + @MongoTextIndexed(name = 'embedded_text_idx', weight = 3) + String city +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index 1a47308f4e7..c0020afc8e3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.data.document.mongodb.validation.options +import io.micronaut.data.annotation.Embeddable import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.annotation.Relation import io.micronaut.data.model.runtime.RuntimePersistentEntity import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField @@ -94,6 +96,29 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { e.message.contains('must use the same defaultLanguage option') } + void 'resolves embedded field simple index path'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(EmbeddedFieldIndexedEntity)).indexes + def index = indexes.find { it.name() == 'embedded_state_idx' } + + then: + index != null + index.fields().size() == 1 + index.fields()[0].path() == 'location.state' + index.fields()[0].order() == 1 + } + + void 'resolves embedded field text index path'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(EmbeddedTextIndexedEntity)).indexes + def index = indexes.find { it.name() == 'embedded_text_idx' } + + then: + index != null + index.fields()*.path().contains('details.city') + index.fields().find { it.path() == 'details.city' }.weight() == 3 + } + void 'resolves 2dsphere sphereVersion'() { when: def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(GeoSphereVersionEntity)).indexes @@ -203,3 +228,31 @@ class InvalidGeoSphereVersionOn2dEntity { @MongoGeoIndexed(name = 'invalid_geo_sphere_version_idx', type = MongoGeoIndexType.GEO_2D, sphereVersion = 3) Map location } + +@MappedEntity('embedded_field_indexed_entity') +class EmbeddedFieldIndexedEntity { + @Relation(Relation.Kind.EMBEDDED) + EmbeddedFieldIndexedLocation location +} + +@Embeddable +class EmbeddedFieldIndexedLocation { + @MongoIndexed(name = 'embedded_state_idx') + String state + + String city +} + +@MappedEntity('embedded_text_indexed_entity') +class EmbeddedTextIndexedEntity { + @Relation(Relation.Kind.EMBEDDED) + EmbeddedTextIndexedDetails details +} + +@Embeddable +class EmbeddedTextIndexedDetails { + String title + + @MongoTextIndexed(name = 'embedded_text_idx', weight = 3) + String city +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy new file mode 100644 index 00000000000..a8d6c9e993a --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy @@ -0,0 +1,157 @@ +package io.micronaut.data.mongodb.init + +import com.mongodb.client.model.CollationStrength +import org.bson.Document +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +class AbstractMongoCollectionsCreatorSpec extends Specification { + + void 'resolves clustered collection options from collection document'() { + given: + def collectionDocument = new Document('name', 'events') + .append('options', new Document('clusteredIndex', new Document('name', 'clustered_idx').append('unique', true)) + .append('expireAfterSeconds', 300)) + + when: + def options = AbstractMongoCollectionsCreator.toResolvedCollectionOptions(collectionDocument) + + then: + options != null + options.clusteredIndexName() == 'clustered_idx' + options.clusteredIndexUnique() + options.expireAfterSeconds() == 300 + } + + void 'skips id index when resolving index document'() { + expect: + AbstractMongoCollectionsCreator.toResolvedIndex(new Document('name', '_id_').append('key', new Document('_id', 1))) == null + } + + void 'resolves text index using weights'() { + given: + def indexDocument = new Document('name', 'embedded_text_idx') + .append('key', new Document('_fts', 'text').append('_ftsx', 1)) + .append('weights', new Document('details.city', 3)) + .append('default_language', 'english') + .append('textIndexVersion', 3) + + when: + def index = AbstractMongoCollectionsCreator.toResolvedIndex(indexDocument) + + then: + index != null + index.name() == 'embedded_text_idx' + index.fields().size() == 1 + index.fields()[0].path() == 'details.city' + index.fields()[0].weight() == 3 + index.fields()[0].kind() == 'text' + index.defaultLanguage() == 'english' + index.textIndexVersion() == 3 + } + + void 'builds create collection options from resolved options'() { + when: + def options = AbstractMongoCollectionsCreator.toCreateCollectionOptions( + new AbstractMongoCollectionsCreator.MongoResolvedCollectionOptions('clustered_idx', true, 120) + ) + + then: + options.clusteredIndexOptions != null + options.clusteredIndexOptions.name == 'clustered_idx' + options.getExpireAfter(TimeUnit.SECONDS) == 120L + } + + void 'builds index options from resolved index'() { + given: + def index = new AbstractMongoCollectionsCreator.MongoResolvedIndex( + 'field_idx', + [new AbstractMongoCollectionsCreator.MongoResolvedIndexField('location.state', 1, null, null, null, null)], + true, + true, + true, + 60, + '{ tenantId: 1 }', + '{ locale: "en", strength: 2 }', + 26, + -180d, + 180d, + 'english', + 'language', + 3, + 2, + '{ location: 0 }', + '{ wiredTiger: {} }', + null, + null + ) + + when: + def options = AbstractMongoCollectionsCreator.toIndexOptions(index) + + then: + options.name == 'field_idx' + options.unique + options.sparse + options.hidden + options.getExpireAfter(TimeUnit.SECONDS) == 60L + options.bits == 26 + options.min == -180d + options.max == 180d + options.defaultLanguage == 'english' + options.languageOverride == 'language' + options.textVersion == 3 + options.sphereVersion == 2 + } + + void 'builds create indexes command document with comment and commit quorum'() { + given: + def index = new AbstractMongoCollectionsCreator.MongoResolvedIndex( + 'field_idx', + [new AbstractMongoCollectionsCreator.MongoResolvedIndexField('location.state', 1, null, null, null, null)], + false, + false, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 'create embedded index', + 'majority' + ) + + when: + def command = AbstractMongoCollectionsCreator.toCreateIndexesCommandDocument('entities', index) + + then: + command.getString('createIndexes') == 'entities' + command.getString('comment') == 'create embedded index' + command.get('commitQuorum') == 'majority' + command.getList('indexes', Object).size() == 1 + } + + void 'converts numeric and symbolic commit quorum values'() { + expect: + AbstractMongoCollectionsCreator.toCommitQuorumValue('2') == 2 + AbstractMongoCollectionsCreator.toCommitQuorumValue('majority') == 'majority' + } + + void 'converts document to collation'() { + when: + def collation = AbstractMongoCollectionsCreator.toCollation(new Document('locale', 'en').append('strength', 2).append('caseLevel', true)) + + then: + collation.locale == 'en' + collation.strength == CollationStrength.SECONDARY + collation.caseLevel + } +} From eebec1213f8dd7ed655d9445efee85516dc3e8a2 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 30 Mar 2026 17:00:33 +0200 Subject: [PATCH 14/34] Minor improvement --- .../data/mongodb/init/AbstractMongoCollectionsCreator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index 968f20d1c5c..13576c1f4b4 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -244,8 +244,9 @@ private MongoResolvedCollectionOptions resolveCollectionOptions(PersistentEntity if (!unique) { throw new IllegalStateException("Mongo clustered index for entity [" + entity.getName() + "] must be unique=true"); } - Integer expireAfterSeconds = annotation.intValue("expireAfterSeconds").isPresent() && annotation.intValue("expireAfterSeconds").getAsInt() >= 0 - ? annotation.intValue("expireAfterSeconds").getAsInt() : null; + var expireAfterSecondsValue = annotation.intValue("expireAfterSeconds"); + Integer expireAfterSeconds = expireAfterSecondsValue.isPresent() && expireAfterSecondsValue.getAsInt() >= 0 + ? expireAfterSecondsValue.getAsInt() : null; if (expireAfterSeconds != null) { PersistentProperty identity = entity.hasIdentity() ? entity.getIdentity() : null; if (identity == null) { From dbc72849a091badb7a90554a0152ec93da936dea Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 31 Mar 2026 09:59:21 +0200 Subject: [PATCH 15/34] Inject converters instead of manually creating --- .../MongoGeoGeometryCollectionConverter.java | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java index 150e2b6a784..96770c3f627 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionContext; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.jspecify.annotations.Nullable; @@ -36,13 +37,27 @@ @Singleton public final class MongoGeoGeometryCollectionConverter implements AttributeConverter> { - private static final MongoGeoPointConverter POINT_CONVERTER = new MongoGeoPointConverter(); - private static final MongoGeoMultiPointConverter MULTI_POINT_CONVERTER = new MongoGeoMultiPointConverter(); - private static final MongoGeoLineStringConverter LINE_STRING_CONVERTER = new MongoGeoLineStringConverter(); - private static final MongoGeoMultiLineStringConverter MULTI_LINE_STRING_CONVERTER = new MongoGeoMultiLineStringConverter(); - private static final MongoGeoPolygonConverter POLYGON_CONVERTER = new MongoGeoPolygonConverter(); - private static final MongoGeoMultiPolygonConverter MULTI_POLYGON_CONVERTER = new MongoGeoMultiPolygonConverter(); - private static final MongoGeoGeometryCollectionConverter GEOMETRY_COLLECTION_CONVERTER = new MongoGeoGeometryCollectionConverter(); + private final MongoGeoPointConverter pointConverter; + private final MongoGeoMultiPointConverter multiPointConverter; + private final MongoGeoLineStringConverter lineStringConverter; + private final MongoGeoMultiLineStringConverter multiLineStringConverter; + private final MongoGeoPolygonConverter polygonConverter; + private final MongoGeoMultiPolygonConverter multiPolygonConverter; + + @Inject + public MongoGeoGeometryCollectionConverter(MongoGeoPointConverter pointConverter, + MongoGeoMultiPointConverter multiPointConverter, + MongoGeoLineStringConverter lineStringConverter, + MongoGeoMultiLineStringConverter multiLineStringConverter, + MongoGeoPolygonConverter polygonConverter, + MongoGeoMultiPolygonConverter multiPolygonConverter) { + this.pointConverter = pointConverter; + this.multiPointConverter = multiPointConverter; + this.lineStringConverter = lineStringConverter; + this.multiLineStringConverter = multiLineStringConverter; + this.polygonConverter = polygonConverter; + this.multiPolygonConverter = multiPolygonConverter; + } @Override public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, @@ -96,44 +111,44 @@ public static boolean supportsImplicitGeometryCollectionType(Class type) { return type == MongoGeoGeometryCollection.class || type.isAssignableFrom(MongoGeoGeometryCollection.class); } - private static Map convertGeometryToMap(MongoGeoGeometry geometry) { + private Map convertGeometryToMap(MongoGeoGeometry geometry) { if (geometry instanceof MongoGeoPoint point) { - return Objects.requireNonNull(POINT_CONVERTER.convertToPersistedValue(point, ConversionContext.DEFAULT)); + return Objects.requireNonNull(pointConverter.convertToPersistedValue(point, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoMultiPoint multiPoint) { - return Objects.requireNonNull(MULTI_POINT_CONVERTER.convertToPersistedValue(multiPoint, ConversionContext.DEFAULT)); + return Objects.requireNonNull(multiPointConverter.convertToPersistedValue(multiPoint, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoLineString lineString) { - return Objects.requireNonNull(LINE_STRING_CONVERTER.convertToPersistedValue(lineString, ConversionContext.DEFAULT)); + return Objects.requireNonNull(lineStringConverter.convertToPersistedValue(lineString, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoMultiLineString multiLineString) { - return Objects.requireNonNull(MULTI_LINE_STRING_CONVERTER.convertToPersistedValue(multiLineString, ConversionContext.DEFAULT)); + return Objects.requireNonNull(multiLineStringConverter.convertToPersistedValue(multiLineString, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoPolygon polygon) { - return Objects.requireNonNull(POLYGON_CONVERTER.convertToPersistedValue(polygon, ConversionContext.DEFAULT)); + return Objects.requireNonNull(polygonConverter.convertToPersistedValue(polygon, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoMultiPolygon multiPolygon) { - return Objects.requireNonNull(MULTI_POLYGON_CONVERTER.convertToPersistedValue(multiPolygon, ConversionContext.DEFAULT)); + return Objects.requireNonNull(multiPolygonConverter.convertToPersistedValue(multiPolygon, ConversionContext.DEFAULT)); } if (geometry instanceof MongoGeoGeometryCollection geometryCollection) { - return Objects.requireNonNull(GEOMETRY_COLLECTION_CONVERTER.convertToPersistedValue(geometryCollection, ConversionContext.DEFAULT)); + return Objects.requireNonNull(convertToPersistedValue(geometryCollection, ConversionContext.DEFAULT)); } throw new IllegalArgumentException("Unsupported geometry collection entry type: " + geometry.getClass().getName()); } - private static MongoGeoGeometry convertMapToGeometry(Map geometryMap) { + private MongoGeoGeometry convertMapToGeometry(Map geometryMap) { Object type = geometryMap.get("type"); if (!(type instanceof String geometryType)) { throw new IllegalArgumentException("Invalid GeoJSON geometry entry type: " + geometryMap); } return switch (geometryType) { - case "Point" -> (MongoGeoGeometry) Objects.requireNonNull(POINT_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiPoint" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_POINT_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "LineString" -> (MongoGeoGeometry) Objects.requireNonNull(LINE_STRING_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiLineString" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_LINE_STRING_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "Polygon" -> (MongoGeoGeometry) Objects.requireNonNull(POLYGON_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiPolygon" -> (MongoGeoGeometry) Objects.requireNonNull(MULTI_POLYGON_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "GeometryCollection" -> (MongoGeoGeometry) Objects.requireNonNull(GEOMETRY_COLLECTION_CONVERTER.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "Point" -> (MongoGeoGeometry) Objects.requireNonNull(pointConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiPoint" -> (MongoGeoGeometry) Objects.requireNonNull(multiPointConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "LineString" -> (MongoGeoGeometry) Objects.requireNonNull(lineStringConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiLineString" -> (MongoGeoGeometry) Objects.requireNonNull(multiLineStringConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "Polygon" -> (MongoGeoGeometry) Objects.requireNonNull(polygonConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "MultiPolygon" -> (MongoGeoGeometry) Objects.requireNonNull(multiPolygonConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); + case "GeometryCollection" -> (MongoGeoGeometry) Objects.requireNonNull(convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); default -> throw new IllegalArgumentException("Unsupported GeoJSON geometry collection entry type: " + geometryType); }; } From 70b7f89e02cb18bcc2e8a9a43ac95d79a765afd7 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 31 Mar 2026 12:15:06 +0200 Subject: [PATCH 16/34] Minor changes and improvements --- .../mongodb/common/MongoEntityIndexes.java | 145 +++++++++++------- .../init/AbstractMongoCollectionsCreator.java | 6 +- ...dMultipleDeclarationsValidationSpec.groovy | 12 +- ...dMultipleDeclarationsValidationSpec.groovy | 12 +- ...ltipleDeclarationsIndexCreationSpec.groovy | 27 +++- .../mongo/mongoMapping/mongoIndexes.adoc | 6 +- 6 files changed, 136 insertions(+), 72 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 0f0f085d526..00e28065b78 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -37,7 +37,9 @@ import org.bson.Document; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -82,9 +84,9 @@ private static MongoEntityIndexes resolve(RuntimePersistentEntity entity) { } private static List resolveTopLevelWildcardIndexes(RuntimePersistentEntity entity) { - List indexes = new ArrayList<>(); + Map> groupedIndexes = new LinkedHashMap<>(); for (var annotation : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoWildcardIndex.class)) { - indexes.add(new ResolvedIndex( + ResolvedIndex index = new ResolvedIndex( annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField("$**", 1, null, null, null, null)), false, @@ -104,70 +106,56 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist parseJsonOption(annotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), annotation.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) - )); + ); + groupedIndexes.computeIfAbsent(new WildcardIndexSignature(index), ignored -> new ArrayList<>()).add(index); } - if (indexes.size() <= 1) { - return indexes; + if (groupedIndexes.isEmpty()) { + return List.of(); } - ResolvedIndex first = indexes.getFirst(); - String mergedName = first.name(); - for (int i = 1; i < indexes.size(); i++) { - ResolvedIndex candidate = indexes.get(i); - if (!Objects.equals(first.fields(), candidate.fields()) - || first.unique() != candidate.unique() - || first.sparse() != candidate.sparse() - || first.hidden() != candidate.hidden() - || !Objects.equals(first.expireAfterSeconds(), candidate.expireAfterSeconds()) - || !Objects.equals(first.partialFilterExpression(), candidate.partialFilterExpression()) - || !Objects.equals(first.collation(), candidate.collation()) - || !Objects.equals(first.bits(), candidate.bits()) - || !Objects.equals(first.min(), candidate.min()) - || !Objects.equals(first.max(), candidate.max()) - || !Objects.equals(first.defaultLanguage(), candidate.defaultLanguage()) - || !Objects.equals(first.languageOverride(), candidate.languageOverride()) - || !Objects.equals(first.textIndexVersion(), candidate.textIndexVersion()) - || !Objects.equals(first.sphereVersion(), candidate.sphereVersion()) - || !Objects.equals(first.wildcardProjection(), candidate.wildcardProjection()) - || !Objects.equals(first.storageEngine(), candidate.storageEngine()) - || !Objects.equals(first.comment(), candidate.comment()) - || !Objects.equals(first.commitQuorum(), candidate.commitQuorum())) { - throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] declare conflicting options for key [$**]"); - } - if (mergedName == null) { - mergedName = candidate.name(); - } else if (candidate.name() != null && !mergedName.equals(candidate.name())) { - throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] must use the same index name when declaring equivalent key [$**]"); + List resolvedIndexes = new ArrayList<>(groupedIndexes.size()); + for (List indexes : groupedIndexes.values()) { + ResolvedIndex first = indexes.getFirst(); + String mergedName = first.name(); + for (int i = 1; i < indexes.size(); i++) { + ResolvedIndex candidate = indexes.get(i); + if (mergedName == null) { + mergedName = candidate.name(); + } else if (candidate.name() != null && !mergedName.equals(candidate.name())) { + throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] must use the same index name when declaring equivalent key [$**]"); + } } + resolvedIndexes.add(new ResolvedIndex( + mergedName, + first.fields(), + first.unique(), + first.sparse(), + first.hidden(), + first.expireAfterSeconds(), + first.partialFilterExpression(), + first.collation(), + first.bits(), + first.min(), + first.max(), + first.defaultLanguage(), + first.languageOverride(), + first.textIndexVersion(), + first.sphereVersion(), + first.wildcardProjection(), + first.storageEngine(), + first.comment(), + first.commitQuorum() + )); } - return List.of(new ResolvedIndex( - mergedName, - first.fields(), - first.unique(), - first.sparse(), - first.hidden(), - first.expireAfterSeconds(), - first.partialFilterExpression(), - first.collation(), - first.bits(), - first.min(), - first.max(), - first.defaultLanguage(), - first.languageOverride(), - first.textIndexVersion(), - first.sphereVersion(), - first.wildcardProjection(), - first.storageEngine(), - first.comment(), - first.commitQuorum() - )); + return List.copyOf(resolvedIndexes); } private static List resolveFieldIndexes(RuntimePersistentEntity entity) { List indexes = new ArrayList<>(); PersistentEntityUtils.traversePersistentProperties(entity, false, false, (associations, property) -> { - if (!(property instanceof RuntimePersistentProperty runtimeProperty) || containsNonEmbeddedAssociation(associations)) { + if (!isIndexableField(property, associations)) { return; } + RuntimePersistentProperty runtimeProperty = (RuntimePersistentProperty) property; String persistedPath = toPersistedPath(associations, property); var annotationMetadata = property.getAnnotationMetadata(); var annotation = annotationMetadata.getAnnotation(MongoIndexed.class); @@ -309,7 +297,7 @@ private static void validateGeoIndexedType(RuntimePersistentEntity entity, private static List resolveTextIndexes(RuntimePersistentEntity entity) { TextIndexState state = new TextIndexState(); PersistentEntityUtils.traversePersistentProperties(entity, false, false, (associations, property) -> { - if (!(property instanceof RuntimePersistentProperty) || containsNonEmbeddedAssociation(associations)) { + if (!isIndexableField(property, associations)) { return; } var textAnnotation = property.getAnnotationMetadata().getAnnotation(MongoTextIndexed.class); @@ -474,6 +462,10 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit return indexes; } + private static boolean isIndexableField(PersistentProperty property, List associations) { + return property instanceof RuntimePersistentProperty && !containsNonEmbeddedAssociation(associations); + } + private static boolean containsNonEmbeddedAssociation(List associations) { for (Association association : associations) { if (!(association instanceof io.micronaut.data.model.Embedded)) { @@ -526,6 +518,47 @@ private static final class TextIndexState { private @Nullable Integer textIndexVersion; } + private record WildcardIndexSignature(List fields, + boolean unique, + boolean sparse, + boolean hidden, + @Nullable Integer expireAfterSeconds, + @Nullable String partialFilterExpression, + @Nullable String collation, + @Nullable Integer bits, + @Nullable Double min, + @Nullable Double max, + @Nullable String defaultLanguage, + @Nullable String languageOverride, + @Nullable Integer textIndexVersion, + @Nullable Integer sphereVersion, + @Nullable String wildcardProjection, + @Nullable String storageEngine, + @Nullable String comment, + @Nullable String commitQuorum) { + + private WildcardIndexSignature(ResolvedIndex index) { + this(index.fields(), + index.unique(), + index.sparse(), + index.hidden(), + index.expireAfterSeconds(), + index.partialFilterExpression(), + index.collation(), + index.bits(), + index.min(), + index.max(), + index.defaultLanguage(), + index.languageOverride(), + index.textIndexVersion(), + index.sphereVersion(), + index.wildcardProjection(), + index.storageEngine(), + index.comment(), + index.commitQuorum()); + } + } + /** * Resolved Mongo index definition. * diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index 13576c1f4b4..e53fa245ba5 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -299,7 +299,7 @@ private void createIndexes(DatabaseOperations databaseOperations, @Nullable private MongoResolvedIndex findMatchingIndex(List existingIndexes, MongoResolvedIndex desiredIndex) { for (MongoResolvedIndex existingIndex : existingIndexes) { - if (existingIndex.hasSameKey(desiredIndex)) { + if (existingIndex.hasSameIdentity(desiredIndex)) { return existingIndex; } } @@ -795,8 +795,8 @@ record MongoResolvedIndex(@Nullable String name, @Nullable String comment, @Nullable String commitQuorum) { - boolean hasSameKey(MongoResolvedIndex other) { - return fields.equals(other.fields); + boolean hasSameIdentity(MongoResolvedIndex other) { + return fields.equals(other.fields) && Objects.equals(wildcardProjection, other.wildcardProjection); } boolean matchesManagedOptions(MongoResolvedIndex other) { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index c2292031e79..3d2cbc56a21 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -17,16 +17,18 @@ class MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec extends Sp ['io.micronaut.data.document.mongodb.validation.reactivewildcardtoplevelmultiple'] } - void 'fails fast when reactive multiple top-level wildcard declarations conflict on options'() { + void 'allows reactive multiple top-level wildcard declarations when wildcardProjection differs'() { when: - ApplicationContext.run(getProperties() + [ + def context = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', 'micronaut.data.mongodb.create-indexes' : 'true' ]) then: - def e = thrown(RuntimeException) - e.message.contains('declare conflicting options for key [$**]') + noExceptionThrown() + + cleanup: + context?.close() } } @@ -35,7 +37,7 @@ interface InvalidReactiveTopLevelWildcardMultipleEntityRepository extends CrudRe } @MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.secret": 0 }') -@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.internal": 0 }') +@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_other_idx', wildcardProjection = '{ "metadata.internal": 0 }') @MappedEntity('invalid_reactive_top_level_wildcard_multiple_entities') class InvalidReactiveTopLevelWildcardMultipleEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index 6256605815b..56ff2590c8b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -17,16 +17,18 @@ class MongoTopLevelWildcardMultipleDeclarationsValidationSpec extends Specificat ['io.micronaut.data.document.mongodb.validation.wildcardtoplevelmultiple'] } - void 'fails fast when multiple top-level wildcard declarations conflict on options'() { + void 'allows multiple top-level wildcard declarations when wildcardProjection differs'() { when: - ApplicationContext.run(getProperties() + [ + def context = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', 'micronaut.data.mongodb.create-indexes' : 'true' ]) then: - def e = thrown(RuntimeException) - e.message.contains('declare conflicting options for key [$**]') + noExceptionThrown() + + cleanup: + context?.close() } } @@ -35,7 +37,7 @@ interface InvalidTopLevelWildcardMultipleEntityRepository extends CrudRepository } @MongoWildcardIndex(name = 'invalid_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.secret": 0 }') -@MongoWildcardIndex(name = 'invalid_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.internal": 0 }') +@MongoWildcardIndex(name = 'invalid_top_level_wildcard_multiple_other_idx', wildcardProjection = '{ "metadata.internal": 0 }') @MappedEntity('invalid_top_level_wildcard_multiple_entities') class InvalidTopLevelWildcardMultipleEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy index 4ae441f7023..76de17cd6c7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy @@ -48,11 +48,34 @@ class MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends Specifi applicationContext.containsBean(expectedCollectionsCreatorBeanType()) conditions.eventually { def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_multiple_indexed_entities') - def wildcardIndexes = indexes.findAll { it.fields.size() == 1 && it.fields[0].path() == '$**' } + def wildcardIndexes = indexes.findAll { + it.fields.size() == 1 && it.fields[0].path() == '$**' && it.wildcardProjection == null + } assert wildcardIndexes.size() == 1 assert wildcardIndexes[0].name == 'top_level_wildcard_multiple_idx' } } + + void 'creates multiple top-level wildcard indexes when wildcardProjection differs'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'top_level_wildcard_multiple_indexed_entities') + def wildcardIndexes = indexes.findAll { it.fields.size() == 1 && it.fields[0].path() == '$**' } + assert wildcardIndexes.size() == 3 + assert wildcardIndexes*.name.contains('top_level_wildcard_multiple_idx') + assert wildcardIndexes*.name.contains('top_level_wildcard_projection_secret_idx') + assert wildcardIndexes*.name.contains('top_level_wildcard_projection_internal_idx') + + def secretIndex = wildcardIndexes.find { it.name == 'top_level_wildcard_projection_secret_idx' } + def internalIndex = wildcardIndexes.find { it.name == 'top_level_wildcard_projection_internal_idx' } + assert secretIndex.wildcardProjection.getInteger('metadata.secret') == 0 + assert internalIndex.wildcardProjection.getInteger('metadata.internal') == 0 + } + } } @MongoRepository @@ -61,6 +84,8 @@ interface TopLevelWildcardMultipleIndexedEntityRepository extends CrudRepository @MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') @MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') +@MongoWildcardIndex(name = 'top_level_wildcard_projection_secret_idx', wildcardProjection = '{ "metadata.secret": 0 }') +@MongoWildcardIndex(name = 'top_level_wildcard_projection_internal_idx', wildcardProjection = '{ "metadata.internal": 0 }') @MappedEntity('top_level_wildcard_multiple_indexed_entities') class TopLevelWildcardMultipleIndexedEntity { @Id diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index da6aaa5f0d8..6a0cda09969 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -167,7 +167,9 @@ Use ann:data.mongodb.annotation.index.MongoWildcardIndexed[] for field-level wil Use ann:data.mongodb.annotation.index.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. -ann:data.mongodb.annotation.index.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key are merged into a single managed index definition. +ann:data.mongodb.annotation.index.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key and `wildcardProjection` signature are merged into a single managed index definition. + +MongoDB 5.0+ allows multiple top-level `{ "$**": 1 }` indexes on the same collection when `wildcardProjection` differs. Micronaut Data preserves those as separate managed index definitions. Top-level wildcard declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. @@ -176,7 +178,7 @@ NOTE: `storageEngine` must be valid JSON. Invalid declarations are rejected duri NOTE: MongoDB allows `wildcardProjection` only for top-level wildcard indexes (`{"$**": 1}`). Using `wildcardProjection` on field-level ann:data.mongodb.annotation.index.MongoWildcardIndexed[] is rejected during startup validation (fail-fast), with an explicit error that points to ann:data.mongodb.annotation.index.MongoWildcardIndex[]. -NOTE: If multiple top-level wildcard declarations define conflicting options for `$**`, startup fails fast with an explicit conflict error. +NOTE: Equivalent repeated top-level wildcard declarations must still agree on managed options and explicit index name. Distinct `wildcardProjection` signatures are allowed to coexist. == TTL indexes From 31afb95279a8b524b9e854ad81cc8641a8ade780 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 31 Mar 2026 12:32:38 +0200 Subject: [PATCH 17/34] Minor changes and improvements --- .../init/AbstractMongoCollectionsCreator.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index e53fa245ba5..bda1029ff78 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -33,6 +33,11 @@ import io.micronaut.context.annotation.Context; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.data.annotation.JsonSubView; +import io.micronaut.data.annotation.JsonView; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; import io.micronaut.data.model.Association; @@ -51,7 +56,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -126,16 +134,18 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, for (AbstractMongoConfiguration mongoConfiguration : mongoConfigurations) { List packageNames = environment.getProperty("mongodb.package-names", List.class).orElseGet(List::of); - List> entityTypes; - if (!packageNames.isEmpty()) { - entityTypes = environment.scan(MappedEntity.class, packageNames.toArray(new String[0])).toList(); + Collection> introspections; + if (CollectionUtils.isNotEmpty(packageNames)) { + introspections = BeanIntrospector.SHARED.findIntrospections(MappedEntity.class, packageNames.toArray(new String[0])); } else { - entityTypes = environment.scan(MappedEntity.class).toList(); + introspections = BeanIntrospector.SHARED.findIntrospections(MappedEntity.class); } - PersistentEntity[] entities = entityTypes.stream() - .filter(type -> !type.getName().contains("$")) - .filter(type -> !type.isSynthetic()) - .map(runtimeEntityRegistry::getEntity) + PersistentEntity[] entities = introspections.stream() + .filter(i -> !i.getBeanType().getName().contains("$")) + .filter(i -> !Modifier.isAbstract(i.getBeanType().getModifiers())) + .filter(i -> !i.hasAnnotation(JsonSubView.class)) + .sorted(Comparator.comparing(i -> i.hasAnnotation(JsonView.class))) + .map(beanIntrospection -> runtimeEntityRegistry.getEntity(beanIntrospection.getBeanType())) .toArray(PersistentEntity[]::new); DatabaseOperations databaseOperations = databaseOperationsProvider.get(mongoConfiguration); From e9e77fac4d669732d108b8e3eccb4c5822093ee4 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Wed, 1 Apr 2026 16:15:20 +0200 Subject: [PATCH 18/34] Remove unneeded code changes, leave only index related stuff. --- .../query/builder/MongoQueryBuilder.java | 82 ------ .../PersistentEntityCriteriaBuilder.java | 214 --------------- .../impl/AbstractCriteriaBuilder.java | 54 ---- .../jpa/criteria/impl/CriteriaUtils.java | 51 ---- .../jpa/criteria/impl/PredicateVisitor.java | 40 --- .../predicate/GeoIntersectsPredicate.java | 57 ---- .../impl/predicate/GeoWithinPredicate.java | 57 ---- .../impl/predicate/NearPredicate.java | 101 ------- .../impl/predicate/NearSpherePredicate.java | 101 ------- .../impl/predicate/TextPredicate.java | 82 ------ .../model/jpa/criteria/impl/util/Joiner.java | 55 ---- .../sql/AbstractSqlLikeQueryBuilder.java | 30 --- .../query/impl/AdvancedPredicateVisitor.java | 30 --- .../criteria/impl/CriteriaUtilsSpec.groovy | 135 ---------- .../mongodb/common/MongoEntityIndexes.java | 27 +- .../data/mongodb/geo/MongoGeoConverters.java | 78 ------ .../data/mongodb/geo/MongoGeoGeometry.java | 25 -- .../geo/MongoGeoGeometryCollection.java | 28 -- .../MongoGeoGeometryCollectionConverter.java | 155 ----------- .../data/mongodb/geo/MongoGeoLineString.java | 28 -- .../geo/MongoGeoLineStringConverter.java | 107 -------- .../mongodb/geo/MongoGeoMultiLineString.java | 28 -- .../geo/MongoGeoMultiLineStringConverter.java | 104 -------- .../data/mongodb/geo/MongoGeoMultiPoint.java | 28 -- .../geo/MongoGeoMultiPointConverter.java | 93 ------- .../mongodb/geo/MongoGeoMultiPolygon.java | 28 -- .../geo/MongoGeoMultiPolygonConverter.java | 115 -------- .../data/mongodb/geo/MongoGeoPoint.java | 30 --- .../mongodb/geo/MongoGeoPointConverter.java | 219 --------------- .../data/mongodb/geo/MongoGeoPointLike.java | 35 --- .../data/mongodb/geo/MongoGeoPolygon.java | 28 -- .../mongodb/geo/MongoGeoPolygonConverter.java | 104 -------- .../operations/DefaultMongoStoredQuery.java | 30 +-- .../mongodb/serde/DataDecoderContext.java | 31 --- .../mongodb/serde/DataEncoderContext.java | 35 --- .../document/mongodb/MongoCriteriaSpec.groovy | 101 +------ .../geo/MongoGeoIndexCreationSpec.groovy | 3 +- ...MongoGeoPointValueIndexCreationSpec.groovy | 9 +- ...ustomGeoPointValueIndexCreationSpec.groovy | 98 ------- ...tryCollectionValueIndexCreationSpec.groovy | 43 +-- ...ustomGeoPointValueIndexCreationSpec.groovy | 95 ------- ...GeoLineStringValueIndexCreationSpec.groovy | 28 +- ...ltiLineStringValueIndexCreationSpec.groovy | 36 +-- ...GeoMultiPointValueIndexCreationSpec.groovy | 28 +- ...oMultiPolygonValueIndexCreationSpec.groovy | 58 ++-- ...ngoGeoPolygonValueIndexCreationSpec.groovy | 31 +-- ...ongoGeoRawQueryParameterBindingSpec.groovy | 103 -------- ...CriteriaQueryOperatorsExecutionSpec.groovy | 249 ------------------ ...ustomGeoPointValueIndexCreationSpec.groovy | 18 -- ...ustomGeoPointValueIndexCreationSpec.groovy | 18 -- ...CriteriaQueryOperatorsExecutionSpec.groovy | 13 - ...goExistingIndexAdvancedConflictSpec.groovy | 3 +- ...MongoExistingIndexCompatibilitySpec.groovy | 7 +- .../MongoGeoIndexValidationSpec.groovy | 4 +- .../geotype/MongoGeoTypeValidationSpec.groovy | 3 +- .../geotypevalid/MongoGeoTypeValidSpec.groovy | 49 ++++ ...oIndexAdvancedOptionsResolutionSpec.groovy | 3 +- .../data/document/mongodb/entities/Test.java | 8 +- .../rawquery/MongoGeoRawQueryEntity.java | 37 --- .../MongoGeoRawQueryEntityRepository.java | 33 --- .../rawquery/ShiftedGeoPointConverter.java | 32 --- .../mongo/mongoCriteriaSpecifications.adoc | 9 - .../mongoCriteriaExecuteQuery.adoc | 141 ---------- 63 files changed, 185 insertions(+), 3520 deletions(-) delete mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java delete mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java delete mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java delete mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java delete mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java delete mode 100644 data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java delete mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy delete mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java delete mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java delete mode 100644 data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java delete mode 100644 src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java index 1a70aff2376..d9a27b7cf8f 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java @@ -51,14 +51,9 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; import io.micronaut.data.model.naming.NamingStrategy; @@ -124,15 +119,6 @@ public final class MongoQueryBuilder implements QueryBuilder { private static final String REGEX = "$regex"; private static final String NOT = "$not"; private static final String OPTIONS = "$options"; - private static final String TEXT = "$text"; - private static final String SEARCH = "$search"; - private static final String GEO_WITHIN = "$geoWithin"; - private static final String GEO_INTERSECTS = "$geoIntersects"; - private static final String GEO_NEAR = "$near"; - private static final String GEO_NEAR_SPHERE = "$nearSphere"; - private static final String GEOMETRY = "$geometry"; - private static final String MIN_DISTANCE = "$minDistance"; - private static final String MAX_DISTANCE = "$maxDistance"; @Nullable @Override @@ -1071,12 +1057,6 @@ private void visitDisjunctionPredicate(Collection @Override public void visit(NegatedPredicate negate) { IExpression negated = negate.getNegated(); - if (negated instanceof TextPredicate) { - throw new UnsupportedOperationException("MongoDB does not support negating a $text predicate."); - } - if (negated instanceof NearPredicate || negated instanceof NearSpherePredicate) { - throw new UnsupportedOperationException("MongoDB does not support negating $near/$nearSphere predicates."); - } if (negated instanceof InPredicate p) { visitIn(p.getExpression(), p.getValues(), true); return; @@ -1159,68 +1139,6 @@ public void visit(LikePredicate likePredicate) { pattern, true); } - @Override - public void visit(TextPredicate textPredicate) { - LinkedHashMap textClause = new LinkedHashMap<>(4); - textClause.put(SEARCH, valueRepresentation(textPredicate.getSearch())); - if (textPredicate.getLanguage() != null) { - textClause.put("$language", valueRepresentation(textPredicate.getLanguage())); - } - if (textPredicate.getCaseSensitive() != null) { - textClause.put("$caseSensitive", valueRepresentation(textPredicate.getCaseSensitive())); - } - if (textPredicate.getDiacriticSensitive() != null) { - textClause.put("$diacriticSensitive", valueRepresentation(textPredicate.getDiacriticSensitive())); - } - query.put(TEXT, textClause); - } - - @Override - public void visit(GeoWithinPredicate geoWithinPredicate) { - PersistentPropertyPath propertyPath = requireProperty(geoWithinPredicate.getExpression()).getPropertyPath(); - query.put(getPropertyPersistName(propertyPath), - Map.of(GEO_WITHIN, - Map.of(GEOMETRY, valueRepresentation(queryState, propertyPath, geoWithinPredicate.getGeometry())))); - } - - @Override - public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { - PersistentPropertyPath propertyPath = requireProperty(geoIntersectsPredicate.getExpression()).getPropertyPath(); - query.put(getPropertyPersistName(propertyPath), - Map.of(GEO_INTERSECTS, - Map.of(GEOMETRY, valueRepresentation(queryState, propertyPath, geoIntersectsPredicate.getGeometry())))); - } - - @Override - public void visit(NearPredicate nearPredicate) { - PersistentPropertyPath propertyPath = requireProperty(nearPredicate.getExpression()).getPropertyPath(); - query.put(getPropertyPersistName(propertyPath), - Map.of(GEO_NEAR, buildNearClause(propertyPath, nearPredicate.getGeometry(), nearPredicate.getMinDistance(), nearPredicate.getMaxDistance()))); - } - - @Override - public void visit(NearSpherePredicate nearSpherePredicate) { - PersistentPropertyPath propertyPath = requireProperty(nearSpherePredicate.getExpression()).getPropertyPath(); - query.put(getPropertyPersistName(propertyPath), - Map.of(GEO_NEAR_SPHERE, - buildNearClause(propertyPath, nearSpherePredicate.getGeometry(), nearSpherePredicate.getMinDistance(), nearSpherePredicate.getMaxDistance()))); - } - - private Map buildNearClause(PersistentPropertyPath propertyPath, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance) { - LinkedHashMap clause = new LinkedHashMap<>(3); - clause.put(GEOMETRY, valueRepresentation(queryState, propertyPath, geometry)); - if (minDistance != null) { - clause.put(MIN_DISTANCE, valueRepresentation(queryState, propertyPath, minDistance)); - } - if (maxDistance != null) { - clause.put(MAX_DISTANCE, valueRepresentation(queryState, propertyPath, maxDistance)); - } - return clause; - } - @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { throw new UnsupportedOperationException("ExistsSubquery is not supported by this implementation."); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 17b529edecb..2efe64cd777 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -228,218 +228,4 @@ default Predicate ilike(Expression x, String pattern) { */ Predicate arrayContains(Expression x, Expression y); - /** - * Creates a MongoDB {@code $text} predicate. - * - * @param search The full-text search expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate text(Expression search); - - /** - * Creates a MongoDB {@code $text} predicate with optional text-search options. - * - * @param search The full-text search expression - * @param language The optional language override expression - * @param caseSensitive The optional case-sensitive flag expression - * @param diacriticSensitive The optional diacritic-sensitive flag expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate text(Expression search, - @Nullable Expression language, - @Nullable Expression caseSensitive, - @Nullable Expression diacriticSensitive); - - /** - * Creates a MongoDB {@code $text} predicate. - * - * @param search The full-text search term - * @return a new predicate - * @since 5.0.0 - */ - default Predicate text(String search) { - return text(literal(search)); - } - - /** - * Creates a MongoDB {@code $text} predicate with optional text-search options. - * - * @param search The full-text search term - * @param language The optional language override - * @param caseSensitive The optional case-sensitive flag - * @param diacriticSensitive The optional diacritic-sensitive flag - * @return a new predicate - * @since 5.0.0 - */ - default Predicate text(String search, - @Nullable String language, - @Nullable Boolean caseSensitive, - @Nullable Boolean diacriticSensitive) { - return text( - literal(search), - language == null ? null : literal(language), - caseSensitive == null ? null : literal(caseSensitive), - diacriticSensitive == null ? null : literal(diacriticSensitive) - ); - } - - /** - * Creates a MongoDB {@code $geoWithin} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate geoWithin(Expression expression, Expression geometry); - - /** - * Creates a MongoDB {@code $geoWithin} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @return a new predicate - * @since 5.0.0 - */ - default Predicate geoWithin(Expression expression, Object geometry) { - return geoWithin(expression, literal(geometry)); - } - - /** - * Creates a MongoDB {@code $geoIntersects} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate geoIntersects(Expression expression, Expression geometry); - - /** - * Creates a MongoDB {@code $geoIntersects} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @return a new predicate - * @since 5.0.0 - */ - default Predicate geoIntersects(Expression expression, Object geometry) { - return geoIntersects(expression, literal(geometry)); - } - - /** - * Creates a MongoDB {@code $near} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate near(Expression expression, Expression geometry); - - /** - * Creates a MongoDB {@code $near} predicate with optional distance bounds. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @param minDistance The optional minimum distance expression - * @param maxDistance The optional maximum distance expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate near(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance); - - /** - * Creates a MongoDB {@code $near} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @return a new predicate - * @since 5.0.0 - */ - default Predicate near(Expression expression, Object geometry) { - return near(expression, literal(geometry)); - } - - /** - * Creates a MongoDB {@code $near} predicate with optional distance bounds. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @param minDistance The optional minimum distance - * @param maxDistance The optional maximum distance - * @return a new predicate - * @since 5.0.0 - */ - default Predicate near(Expression expression, - Object geometry, - @Nullable Number minDistance, - @Nullable Number maxDistance) { - return near(expression, - literal(geometry), - minDistance == null ? null : literal(minDistance), - maxDistance == null ? null : literal(maxDistance)); - } - - /** - * Creates a MongoDB {@code $nearSphere} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate nearSphere(Expression expression, Expression geometry); - - /** - * Creates a MongoDB {@code $nearSphere} predicate with optional distance bounds. - * - * @param expression The geospatial property expression - * @param geometry The geometry expression - * @param minDistance The optional minimum distance expression - * @param maxDistance The optional maximum distance expression - * @return a new predicate - * @since 5.0.0 - */ - Predicate nearSphere(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance); - - /** - * Creates a MongoDB {@code $nearSphere} predicate. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @return a new predicate - * @since 5.0.0 - */ - default Predicate nearSphere(Expression expression, Object geometry) { - return nearSphere(expression, literal(geometry)); - } - - /** - * Creates a MongoDB {@code $nearSphere} predicate with optional distance bounds. - * - * @param expression The geospatial property expression - * @param geometry The geometry value - * @param minDistance The optional minimum distance - * @param maxDistance The optional maximum distance - * @return a new predicate - * @since 5.0.0 - */ - default Predicate nearSphere(Expression expression, - Object geometry, - @Nullable Number minDistance, - @Nullable Number maxDistance) { - return nearSphere(expression, - literal(geometry), - minDistance == null ? null : literal(minDistance), - maxDistance == null ? null : literal(maxDistance)); - } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java index 1ae95b17f33..2ec1ede8ed9 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractCriteriaBuilder.java @@ -27,11 +27,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; @@ -39,7 +35,6 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateUnaryOp; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpressionType; import jakarta.persistence.Tuple; @@ -1477,55 +1472,6 @@ public Predicate arrayContains(Expression x, Expression y) { return predicate(x, y, PredicateBinaryOp.ARRAY_CONTAINS); } - @Override - public Predicate text(Expression search) { - return text(search, null, null, null); - } - - @Override - public Predicate text(Expression search, - @Nullable Expression language, - @Nullable Expression caseSensitive, - @Nullable Expression diacriticSensitive) { - return new TextPredicate(search, language, caseSensitive, diacriticSensitive); - } - - @Override - public Predicate geoWithin(Expression expression, Expression geometry) { - return new GeoWithinPredicate(expression, geometry); - } - - @Override - public Predicate geoIntersects(Expression expression, Expression geometry) { - return new GeoIntersectsPredicate(expression, geometry); - } - - @Override - public Predicate near(Expression expression, Expression geometry) { - return near(expression, geometry, (Expression) null, (Expression) null); - } - - @Override - public Predicate near(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance) { - return new NearPredicate(expression, geometry, minDistance, maxDistance); - } - - @Override - public Predicate nearSphere(Expression expression, Expression geometry) { - return nearSphere(expression, geometry, (Expression) null, (Expression) null); - } - - @Override - public Predicate nearSphere(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance) { - return new NearSpherePredicate(expression, geometry, minDistance, maxDistance); - } - @Override public Expression localDate() { throw notSupportedOperation(); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java index 902fe7ddebc..6eb0d7f084d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtils.java @@ -26,12 +26,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Subquery; @@ -205,52 +200,6 @@ private static void extractPredicateParameters(Expression predicate, Set { - if (textPredicate.getSearch() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (textPredicate.getLanguage() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (textPredicate.getCaseSensitive() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (textPredicate.getDiacriticSensitive() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - } - case GeoWithinPredicate geoWithinPredicate -> { - if (geoWithinPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - } - case GeoIntersectsPredicate geoIntersectsPredicate -> { - if (geoIntersectsPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - } - case NearPredicate nearPredicate -> { - if (nearPredicate.getGeometry() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (nearPredicate.getMinDistance() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (nearPredicate.getMaxDistance() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - } - case NearSpherePredicate nearSpherePredicate -> { - if (nearSpherePredicate.getGeometry() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (nearSpherePredicate.getMinDistance() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - if (nearSpherePredicate.getMaxDistance() instanceof ParameterExpression parameterExpression) { - parameters.add(parameterExpression); - } - } case ConjunctionPredicate conjunctionPredicate -> { for (IExpression pred : conjunctionPredicate.getPredicates()) { extractPredicateParameters(pred, parameters); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java index ddb85c2c0ca..d65cf6fe5b0 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java @@ -19,16 +19,11 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; /** @@ -96,41 +91,6 @@ public interface PredicateVisitor { */ void visit(LikePredicate likePredicate); - /** - * Visit {@link TextPredicate}. - * - * @param textPredicate The text predicate - */ - void visit(TextPredicate textPredicate); - - /** - * Visit {@link GeoWithinPredicate}. - * - * @param geoWithinPredicate The geo-within predicate - */ - void visit(GeoWithinPredicate geoWithinPredicate); - - /** - * Visit {@link GeoIntersectsPredicate}. - * - * @param geoIntersectsPredicate The geo-intersects predicate - */ - void visit(GeoIntersectsPredicate geoIntersectsPredicate); - - /** - * Visit {@link NearPredicate}. - * - * @param nearPredicate The near predicate - */ - void visit(NearPredicate nearPredicate); - - /** - * Visit {@link NearSpherePredicate}. - * - * @param nearSpherePredicate The near-sphere predicate - */ - void visit(NearSpherePredicate nearSpherePredicate); - /** * Visit {@link ExistsSubqueryPredicate}. * diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java deleted file mode 100644 index a8b9a6237f1..00000000000 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoIntersectsPredicate.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; -import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; -import jakarta.persistence.criteria.Expression; - -/** - * MongoDB geospatial predicate for {@code $geoIntersects} queries. - * - * @since 5.0.0 - */ -@Internal -public final class GeoIntersectsPredicate extends AbstractPredicate { - - private final Expression expression; - private final Expression geometry; - - public GeoIntersectsPredicate(Expression expression, Expression geometry) { - this.expression = CriteriaUtils.requireProperty(expression); - this.geometry = CriteriaUtils.requireIExpression(geometry); - } - - /** - * @return The geospatial property expression. - */ - public Expression getExpression() { - return expression; - } - - /** - * @return The geometry expression. - */ - public Expression getGeometry() { - return geometry; - } - - @Override - public void visitPredicate(PredicateVisitor predicateVisitor) { - predicateVisitor.visit(this); - } -} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java deleted file mode 100644 index 2afd3e44fee..00000000000 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/GeoWithinPredicate.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; -import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; -import jakarta.persistence.criteria.Expression; - -/** - * MongoDB geospatial predicate for {@code $geoWithin} queries. - * - * @since 5.0.0 - */ -@Internal -public final class GeoWithinPredicate extends AbstractPredicate { - - private final Expression expression; - private final Expression geometry; - - public GeoWithinPredicate(Expression expression, Expression geometry) { - this.expression = CriteriaUtils.requireProperty(expression); - this.geometry = CriteriaUtils.requireIExpression(geometry); - } - - /** - * @return The geospatial property expression. - */ - public Expression getExpression() { - return expression; - } - - /** - * @return The geometry expression. - */ - public Expression getGeometry() { - return geometry; - } - - @Override - public void visitPredicate(PredicateVisitor predicateVisitor) { - predicateVisitor.visit(this); - } -} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java deleted file mode 100644 index 07559c02481..00000000000 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearPredicate.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; -import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; -import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression; -import jakarta.persistence.criteria.Expression; -import org.jspecify.annotations.Nullable; - -/** - * MongoDB geospatial predicate for {@code $near} queries. - * - * @since 5.0.0 - */ -@Internal -public final class NearPredicate extends AbstractPredicate { - - private final Expression expression; - private final Expression geometry; - @Nullable - private final Expression minDistance; - @Nullable - private final Expression maxDistance; - - public NearPredicate(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance) { - this.expression = CriteriaUtils.requireProperty(expression); - this.geometry = CriteriaUtils.requireIExpression(geometry); - this.minDistance = minDistance; - if (minDistance != null) { - CriteriaUtils.requireNumericExpression(minDistance); - } - this.maxDistance = maxDistance; - if (maxDistance != null) { - CriteriaUtils.requireNumericExpression(maxDistance); - } - validateDistanceRange(minDistance, maxDistance); - } - - private static void validateDistanceRange(@Nullable Expression minDistance, - @Nullable Expression maxDistance) { - if (minDistance instanceof LiteralExpression minLiteral - && maxDistance instanceof LiteralExpression maxLiteral) { - Number minValue = minLiteral.getValue(); - Number maxValue = maxLiteral.getValue(); - if (minValue != null && maxValue != null && Double.compare(maxValue.doubleValue(), minValue.doubleValue()) < 0) { - throw new IllegalArgumentException("The maximum distance must be greater than or equal to the minimum distance"); - } - } - } - - /** - * @return The geospatial property expression. - */ - public Expression getExpression() { - return expression; - } - - /** - * @return The geometry expression. - */ - public Expression getGeometry() { - return geometry; - } - - /** - * @return The optional minimum distance expression. - */ - public @Nullable Expression getMinDistance() { - return minDistance; - } - - /** - * @return The optional maximum distance expression. - */ - public @Nullable Expression getMaxDistance() { - return maxDistance; - } - - @Override - public void visitPredicate(PredicateVisitor predicateVisitor) { - predicateVisitor.visit(this); - } -} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java deleted file mode 100644 index df753e7db31..00000000000 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/NearSpherePredicate.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; -import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; -import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression; -import jakarta.persistence.criteria.Expression; -import org.jspecify.annotations.Nullable; - -/** - * MongoDB geospatial predicate for {@code $nearSphere} queries. - * - * @since 5.0.0 - */ -@Internal -public final class NearSpherePredicate extends AbstractPredicate { - - private final Expression expression; - private final Expression geometry; - @Nullable - private final Expression minDistance; - @Nullable - private final Expression maxDistance; - - public NearSpherePredicate(Expression expression, - Expression geometry, - @Nullable Expression minDistance, - @Nullable Expression maxDistance) { - this.expression = CriteriaUtils.requireProperty(expression); - this.geometry = CriteriaUtils.requireIExpression(geometry); - this.minDistance = minDistance; - if (minDistance != null) { - CriteriaUtils.requireNumericExpression(minDistance); - } - this.maxDistance = maxDistance; - if (maxDistance != null) { - CriteriaUtils.requireNumericExpression(maxDistance); - } - validateDistanceRange(minDistance, maxDistance); - } - - private static void validateDistanceRange(@Nullable Expression minDistance, - @Nullable Expression maxDistance) { - if (minDistance instanceof LiteralExpression minLiteral - && maxDistance instanceof LiteralExpression maxLiteral) { - Number minValue = minLiteral.getValue(); - Number maxValue = maxLiteral.getValue(); - if (minValue != null && maxValue != null && Double.compare(maxValue.doubleValue(), minValue.doubleValue()) < 0) { - throw new IllegalArgumentException("The maximum distance must be greater than or equal to the minimum distance"); - } - } - } - - /** - * @return The geospatial property expression. - */ - public Expression getExpression() { - return expression; - } - - /** - * @return The geometry expression. - */ - public Expression getGeometry() { - return geometry; - } - - /** - * @return The optional minimum distance expression. - */ - public @Nullable Expression getMinDistance() { - return minDistance; - } - - /** - * @return The optional maximum distance expression. - */ - public @Nullable Expression getMaxDistance() { - return maxDistance; - } - - @Override - public void visitPredicate(PredicateVisitor predicateVisitor) { - predicateVisitor.visit(this); - } -} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java deleted file mode 100644 index 8684d6245df..00000000000 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/TextPredicate.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl.predicate; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; -import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; -import jakarta.persistence.criteria.Expression; -import org.jspecify.annotations.Nullable; - -/** - * MongoDB full-text search predicate. - * - * @since 5.0.0 - */ -@Internal -public final class TextPredicate extends AbstractPredicate { - - private final Expression search; - @Nullable - private final Expression language; - @Nullable - private final Expression caseSensitive; - @Nullable - private final Expression diacriticSensitive; - - public TextPredicate(Expression search, - @Nullable Expression language, - @Nullable Expression caseSensitive, - @Nullable Expression diacriticSensitive) { - this.search = CriteriaUtils.requireStringExpression(search); - this.language = language == null ? null : CriteriaUtils.requireStringExpression(language); - this.caseSensitive = caseSensitive == null ? null : CriteriaUtils.requireBoolExpression(caseSensitive); - this.diacriticSensitive = diacriticSensitive == null ? null : CriteriaUtils.requireBoolExpression(diacriticSensitive); - } - - /** - * @return The search expression. - */ - public Expression getSearch() { - return search; - } - - /** - * @return The optional language expression. - */ - public @Nullable Expression getLanguage() { - return language; - } - - /** - * @return The optional case-sensitive flag expression. - */ - public @Nullable Expression getCaseSensitive() { - return caseSensitive; - } - - /** - * @return The optional diacritic-sensitive flag expression. - */ - public @Nullable Expression getDiacriticSensitive() { - return diacriticSensitive; - } - - @Override - public void visitPredicate(PredicateVisitor predicateVisitor) { - predicateVisitor.visit(this); - } -} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java index ae822eeae70..334ad3440ef 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java @@ -40,14 +40,9 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; @@ -278,56 +273,6 @@ public void visit(LikePredicate likePredicate) { visitPredicateExpression(likePredicate.getExpression()); } - @Override - public void visit(TextPredicate textPredicate) { - visitPredicateExpression(textPredicate.getSearch()); - if (textPredicate.getLanguage() != null) { - visitPredicateExpression(textPredicate.getLanguage()); - } - if (textPredicate.getCaseSensitive() != null) { - visitPredicateExpression(textPredicate.getCaseSensitive()); - } - if (textPredicate.getDiacriticSensitive() != null) { - visitPredicateExpression(textPredicate.getDiacriticSensitive()); - } - } - - @Override - public void visit(GeoWithinPredicate geoWithinPredicate) { - visitPredicateExpression(geoWithinPredicate.getExpression()); - visitPredicateExpression(geoWithinPredicate.getGeometry()); - } - - @Override - public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { - visitPredicateExpression(geoIntersectsPredicate.getExpression()); - visitPredicateExpression(geoIntersectsPredicate.getGeometry()); - } - - @Override - public void visit(NearPredicate nearPredicate) { - visitPredicateExpression(nearPredicate.getExpression()); - visitPredicateExpression(nearPredicate.getGeometry()); - if (nearPredicate.getMinDistance() != null) { - visitPredicateExpression(nearPredicate.getMinDistance()); - } - if (nearPredicate.getMaxDistance() != null) { - visitPredicateExpression(nearPredicate.getMaxDistance()); - } - } - - @Override - public void visit(NearSpherePredicate nearSpherePredicate) { - visitPredicateExpression(nearSpherePredicate.getExpression()); - visitPredicateExpression(nearSpherePredicate.getGeometry()); - if (nearSpherePredicate.getMinDistance() != null) { - visitPredicateExpression(nearSpherePredicate.getMinDistance()); - } - if (nearSpherePredicate.getMaxDistance() != null) { - visitPredicateExpression(nearSpherePredicate.getMaxDistance()); - } - } - @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java index f9922f3f54b..444de52ad53 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java @@ -68,14 +68,9 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; @@ -2120,31 +2115,6 @@ public void visit(LikePredicate likePredicate) { } } - @Override - public void visit(TextPredicate textPredicate) { - throw new UnsupportedOperationException("Text predicate is not supported by SQL query builder."); - } - - @Override - public void visit(GeoWithinPredicate geoWithinPredicate) { - throw new UnsupportedOperationException("GeoWithin predicate is not supported by SQL query builder."); - } - - @Override - public void visit(GeoIntersectsPredicate geoIntersectsPredicate) { - throw new UnsupportedOperationException("GeoIntersects predicate is not supported by SQL query builder."); - } - - @Override - public void visit(NearPredicate nearPredicate) { - throw new UnsupportedOperationException("Near predicate is not supported by SQL query builder."); - } - - @Override - public void visit(NearSpherePredicate nearSpherePredicate) { - throw new UnsupportedOperationException("NearSphere predicate is not supported by SQL query builder."); - } - @Override public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { query.append("EXISTS"); diff --git a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java index 886a826e750..b8eb429065a 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/impl/AdvancedPredicateVisitor.java @@ -21,12 +21,7 @@ import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate; -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.UnaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import jakarta.persistence.criteria.Expression; @@ -150,31 +145,6 @@ default void visit(InPredicate inPredicate) { visitIn(inPredicate.getExpression(), inPredicate.getValues(), false); } - @Override - default void visit(TextPredicate textPredicate) { - throw new UnsupportedOperationException("Text predicate is not supported by this implementation."); - } - - @Override - default void visit(GeoWithinPredicate geoWithinPredicate) { - throw new UnsupportedOperationException("GeoWithin predicate is not supported by this implementation."); - } - - @Override - default void visit(GeoIntersectsPredicate geoIntersectsPredicate) { - throw new UnsupportedOperationException("GeoIntersects predicate is not supported by this implementation."); - } - - @Override - default void visit(NearPredicate nearPredicate) { - throw new UnsupportedOperationException("Near predicate is not supported by this implementation."); - } - - @Override - default void visit(NearSpherePredicate nearSpherePredicate) { - throw new UnsupportedOperationException("NearSphere predicate is not supported by this implementation."); - } - void visitIn(Expression expression, Collection values, boolean negated); } diff --git a/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy deleted file mode 100644 index d2edcdc9cbb..00000000000 --- a/data-model/src/test/groovy/io/micronaut/data/model/jpa/criteria/impl/CriteriaUtilsSpec.groovy +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.model.jpa.criteria.impl - -import io.micronaut.data.model.PersistentProperty -import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression -import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoIntersectsPredicate -import io.micronaut.data.model.jpa.criteria.impl.predicate.GeoWithinPredicate -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearPredicate -import io.micronaut.data.model.jpa.criteria.impl.predicate.NearSpherePredicate -import io.micronaut.data.model.jpa.criteria.impl.predicate.TextPredicate -import spock.lang.Specification - -class CriteriaUtilsSpec extends Specification { - - void 'extracts parameters from text predicate in declared order'() { - given: - def search = parameter(String, 'search') - def language = parameter(String, 'language') - def caseSensitive = parameter(Boolean, 'caseSensitive') - def diacriticSensitive = parameter(Boolean, 'diacriticSensitive') - - when: - def parameters = CriteriaUtils.extractPredicateParameters(new TextPredicate(search, language, caseSensitive, diacriticSensitive)) - - then: - parameters as List == [search, language, caseSensitive, diacriticSensitive] - } - - void 'extracts only parameter expressions from mongo-specific predicates and nested conjunctions'() { - given: - def property = Stub(DefaultPersistentPropertyPath) { - getProperty() >> Stub(PersistentProperty) - } - def textSearch = parameter(String, 'textSearch') - def geoWithinGeometry = parameter(Map, 'geoWithinGeometry') - def geoIntersectsGeometry = parameter(Map, 'geoIntersectsGeometry') - def nearGeometry = parameter(Map, 'nearGeometry') - def nearMin = parameter(Double, 'nearMin') - def nearSphereGeometry = parameter(Map, 'nearSphereGeometry') - def nearSphereMax = parameter(Double, 'nearSphereMax') - - def predicate = new ConjunctionPredicate([ - new TextPredicate(textSearch, new LiteralExpression<>('en'), null, null), - new GeoWithinPredicate(property, geoWithinGeometry), - new GeoIntersectsPredicate(property, geoIntersectsGeometry), - new NearPredicate(property, nearGeometry, nearMin, new LiteralExpression<>(2000d)), - new NearSpherePredicate(property, nearSphereGeometry, new LiteralExpression<>(0d), nearSphereMax) - ]) - - when: - def parameters = CriteriaUtils.extractPredicateParameters(predicate) - - then: - parameters as List == [textSearch, geoWithinGeometry, geoIntersectsGeometry, nearGeometry, nearMin, nearSphereGeometry, nearSphereMax] - } - - void 'rejects near predicate when max distance is less than min distance'() { - given: - def property = Stub(DefaultPersistentPropertyPath) { - getProperty() >> Stub(PersistentProperty) - } - def geometry = parameter(Map, 'geometry') - - when: - new NearPredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(5d)) - - then: - def e = thrown(IllegalArgumentException) - e.message == 'The maximum distance must be greater than or equal to the minimum distance' - } - - void 'allows near predicate when max distance equals min distance'() { - given: - def property = Stub(DefaultPersistentPropertyPath) { - getProperty() >> Stub(PersistentProperty) - } - def geometry = parameter(Map, 'geometry') - - when: - def predicate = new NearPredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(10d)) - - then: - predicate.minDistance.value == 10d - predicate.maxDistance.value == 10d - } - - void 'rejects near sphere predicate when max distance is less than min distance'() { - given: - def property = Stub(DefaultPersistentPropertyPath) { - getProperty() >> Stub(PersistentProperty) - } - def geometry = parameter(Map, 'geometry') - - when: - new NearSpherePredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(5d)) - - then: - def e = thrown(IllegalArgumentException) - e.message == 'The maximum distance must be greater than or equal to the minimum distance' - } - - void 'allows near sphere predicate when max distance equals min distance'() { - given: - def property = Stub(DefaultPersistentPropertyPath) { - getProperty() >> Stub(PersistentProperty) - } - def geometry = parameter(Map, 'geometry') - - when: - def predicate = new NearSpherePredicate(property, geometry, new LiteralExpression<>(10d), new LiteralExpression<>(10d)) - - then: - predicate.minDistance.value == 10d - predicate.maxDistance.value == 10d - } - - private static IParameterExpression parameter(Class type, String name) { - new DefaultParameterExpression<>(type, name, null) - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 00e28065b78..1e8d49cb8e1 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -15,6 +15,14 @@ */ package io.micronaut.data.mongodb.common; +import com.mongodb.client.model.geojson.Geometry; +import com.mongodb.client.model.geojson.GeometryCollection; +import com.mongodb.client.model.geojson.LineString; +import com.mongodb.client.model.geojson.MultiLineString; +import com.mongodb.client.model.geojson.MultiPoint; +import com.mongodb.client.model.geojson.MultiPolygon; +import com.mongodb.client.model.geojson.Point; +import com.mongodb.client.model.geojson.Polygon; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex; @@ -27,7 +35,6 @@ import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed; import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex; import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed; -import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.model.Association; import io.micronaut.data.model.PersistentEntityUtils; import io.micronaut.data.model.PersistentProperty; @@ -283,15 +290,29 @@ private static List resolveFieldIndexes(RuntimePersistentEntity entity, RuntimePersistentProperty property) { + MongoGeoIndexType indexType = property.getAnnotationMetadata().enumValue(MongoGeoIndexed.class, "type", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); Class propertyType = property.getType(); - if (MongoGeoConverters.supportsGeoIndexedPropertyType(propertyType)) { + if ((indexType == MongoGeoIndexType.GEO_2D && Map.class.isAssignableFrom(propertyType)) + || isSupportedGeoIndexedType(propertyType)) { return; } throw new IllegalStateException("Mongo geospatial index on entity [" + entity.getName() + "] property [" + property.getName() - + "] requires a supported type (MongoGeoPoint, MongoGeoPointLike, point-like bean shape, MongoGeoMultiPoint, MongoGeoLineString, MongoGeoMultiLineString, MongoGeoPolygon, MongoGeoMultiPolygon, or MongoGeoGeometryCollection)"); + + "] requires a supported MongoDB GeoJSON type (Geometry, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, or GeometryCollection)" + + (indexType == MongoGeoIndexType.GEO_2D ? " or a Map-backed legacy 2d coordinate value" : "")); + } + + private static boolean isSupportedGeoIndexedType(Class propertyType) { + return Geometry.class.isAssignableFrom(propertyType) + || Point.class.isAssignableFrom(propertyType) + || MultiPoint.class.isAssignableFrom(propertyType) + || LineString.class.isAssignableFrom(propertyType) + || MultiLineString.class.isAssignableFrom(propertyType) + || Polygon.class.isAssignableFrom(propertyType) + || MultiPolygon.class.isAssignableFrom(propertyType) + || GeometryCollection.class.isAssignableFrom(propertyType); } private static List resolveTextIndexes(RuntimePersistentEntity entity) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java deleted file mode 100644 index 1250daf7f34..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoConverters.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.Map; - -/** - * Utility methods for implicit Mongo geospatial converter resolution. - * - * @author radovanradic - * @since 5.0.0 - */ -public final class MongoGeoConverters { - - private MongoGeoConverters() { - } - - /** - * @param type The property type - * @return Whether this type has implicit geospatial converter support - */ - public static boolean supportsImplicitGeoType(Class type) { - return MongoGeoPointConverter.supportsImplicitPointType(type) - || MongoGeoLineStringConverter.supportsImplicitLineStringType(type) - || MongoGeoMultiLineStringConverter.supportsImplicitMultiLineStringType(type) - || MongoGeoMultiPointConverter.supportsImplicitMultiPointType(type) - || MongoGeoGeometryCollectionConverter.supportsImplicitGeometryCollectionType(type) - || MongoGeoMultiPolygonConverter.supportsImplicitMultiPolygonType(type) - || MongoGeoPolygonConverter.supportsImplicitPolygonType(type); - } - - /** - * @param type The property type - * @return The implicit converter class for this geospatial type - */ - public static Class resolveImplicitGeoConverterClass(Class type) { - if (MongoGeoMultiPointConverter.supportsImplicitMultiPointType(type)) { - return MongoGeoMultiPointConverter.class; - } - if (MongoGeoGeometryCollectionConverter.supportsImplicitGeometryCollectionType(type)) { - return MongoGeoGeometryCollectionConverter.class; - } - if (MongoGeoMultiLineStringConverter.supportsImplicitMultiLineStringType(type)) { - return MongoGeoMultiLineStringConverter.class; - } - if (MongoGeoMultiPolygonConverter.supportsImplicitMultiPolygonType(type)) { - return MongoGeoMultiPolygonConverter.class; - } - if (MongoGeoLineStringConverter.supportsImplicitLineStringType(type)) { - return MongoGeoLineStringConverter.class; - } - if (MongoGeoPolygonConverter.supportsImplicitPolygonType(type)) { - return MongoGeoPolygonConverter.class; - } - return MongoGeoPointConverter.class; - } - - /** - * @param type The property type - * @return Whether this type is supported for Mongo geospatial indexing - */ - public static boolean supportsGeoIndexedPropertyType(Class type) { - return Map.class.isAssignableFrom(type) || supportsImplicitGeoType(type); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java deleted file mode 100644 index 129491b9d0f..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometry.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -/** - * Marker interface for modeled Mongo GeoJSON geometry values. - * - * @author radovanradic - * @since 5.0.0 - */ -public interface MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java deleted file mode 100644 index fa060b50ca8..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollection.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON geometry-collection value for MongoDB geospatial fields. - * - * @param geometries Ordered list of modeled GeoJSON geometries. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoGeometryCollection(List geometries) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java deleted file mode 100644 index 96770c3f627..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoGeometryCollectionConverter.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Converts {@link MongoGeoGeometryCollection} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoGeometryCollectionConverter implements AttributeConverter> { - - private final MongoGeoPointConverter pointConverter; - private final MongoGeoMultiPointConverter multiPointConverter; - private final MongoGeoLineStringConverter lineStringConverter; - private final MongoGeoMultiLineStringConverter multiLineStringConverter; - private final MongoGeoPolygonConverter polygonConverter; - private final MongoGeoMultiPolygonConverter multiPolygonConverter; - - @Inject - public MongoGeoGeometryCollectionConverter(MongoGeoPointConverter pointConverter, - MongoGeoMultiPointConverter multiPointConverter, - MongoGeoLineStringConverter lineStringConverter, - MongoGeoMultiLineStringConverter multiLineStringConverter, - MongoGeoPolygonConverter polygonConverter, - MongoGeoMultiPolygonConverter multiPolygonConverter) { - this.pointConverter = pointConverter; - this.multiPointConverter = multiPointConverter; - this.lineStringConverter = lineStringConverter; - this.multiLineStringConverter = multiLineStringConverter; - this.polygonConverter = polygonConverter; - this.multiPolygonConverter = multiPolygonConverter; - } - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoGeometryCollection geometryCollection)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial geometry-collection value type: " + entityValue.getClass().getName()); - } - List> geometries = new ArrayList<>(geometryCollection.geometries().size()); - for (MongoGeoGeometry geometry : geometryCollection.geometries()) { - geometries.add(convertGeometryToMap(geometry)); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "GeometryCollection"); - geoJson.put("geometries", geometries); - return geoJson; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"GeometryCollection".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON geometry-collection type: " + type); - } - Object geometries = persistedValue.get("geometries"); - if (!(geometries instanceof List geometryList)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoGeometryCollection value: " + persistedValue); - } - List geometryValues = new ArrayList<>(geometryList.size()); - for (Object geometry : geometryList) { - if (!(geometry instanceof Map geometryMapRaw)) { - throw new IllegalArgumentException("Invalid GeoJSON geometry entry: " + geometry); - } - Map geometryMap = (Map) geometryMapRaw; - geometryValues.add(convertMapToGeometry(geometryMap)); - } - return new MongoGeoGeometryCollection(List.copyOf(geometryValues)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit geometry-collection conversion - */ - public static boolean supportsImplicitGeometryCollectionType(Class type) { - return type == MongoGeoGeometryCollection.class || type.isAssignableFrom(MongoGeoGeometryCollection.class); - } - - private Map convertGeometryToMap(MongoGeoGeometry geometry) { - if (geometry instanceof MongoGeoPoint point) { - return Objects.requireNonNull(pointConverter.convertToPersistedValue(point, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoMultiPoint multiPoint) { - return Objects.requireNonNull(multiPointConverter.convertToPersistedValue(multiPoint, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoLineString lineString) { - return Objects.requireNonNull(lineStringConverter.convertToPersistedValue(lineString, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoMultiLineString multiLineString) { - return Objects.requireNonNull(multiLineStringConverter.convertToPersistedValue(multiLineString, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoPolygon polygon) { - return Objects.requireNonNull(polygonConverter.convertToPersistedValue(polygon, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoMultiPolygon multiPolygon) { - return Objects.requireNonNull(multiPolygonConverter.convertToPersistedValue(multiPolygon, ConversionContext.DEFAULT)); - } - if (geometry instanceof MongoGeoGeometryCollection geometryCollection) { - return Objects.requireNonNull(convertToPersistedValue(geometryCollection, ConversionContext.DEFAULT)); - } - throw new IllegalArgumentException("Unsupported geometry collection entry type: " + geometry.getClass().getName()); - } - - private MongoGeoGeometry convertMapToGeometry(Map geometryMap) { - Object type = geometryMap.get("type"); - if (!(type instanceof String geometryType)) { - throw new IllegalArgumentException("Invalid GeoJSON geometry entry type: " + geometryMap); - } - return switch (geometryType) { - case "Point" -> (MongoGeoGeometry) Objects.requireNonNull(pointConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiPoint" -> (MongoGeoGeometry) Objects.requireNonNull(multiPointConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "LineString" -> (MongoGeoGeometry) Objects.requireNonNull(lineStringConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiLineString" -> (MongoGeoGeometry) Objects.requireNonNull(multiLineStringConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "Polygon" -> (MongoGeoGeometry) Objects.requireNonNull(polygonConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "MultiPolygon" -> (MongoGeoGeometry) Objects.requireNonNull(multiPolygonConverter.convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - case "GeometryCollection" -> (MongoGeoGeometry) Objects.requireNonNull(convertToEntityValue(geometryMap, ConversionContext.DEFAULT)); - default -> throw new IllegalArgumentException("Unsupported GeoJSON geometry collection entry type: " + geometryType); - }; - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java deleted file mode 100644 index 0ce742d8b66..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineString.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON line string value for MongoDB geospatial fields. - * - * @param coordinates Ordered list of points in this line string. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoLineString(List coordinates) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java deleted file mode 100644 index 9b9ad7fb015..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoLineStringConverter.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoLineString} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoLineStringConverter implements AttributeConverter> { - - /** - * Converts a line string modeled value to a persisted GeoJSON map. - * - * @param entityValue The modeled geospatial value - * @param context The conversion context - * @return The persisted GeoJSON map or {@code null} - */ - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoLineString lineString)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial line string value type: " + entityValue.getClass().getName()); - } - List> coordinates = new ArrayList<>(lineString.coordinates().size()); - for (MongoGeoPoint point : lineString.coordinates()) { - coordinates.add(List.of(point.x(), point.y())); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "LineString"); - geoJson.put("coordinates", coordinates); - return geoJson; - } - - /** - * Converts a persisted GeoJSON map to a line string modeled value. - * - * @param persistedValue The persisted GeoJSON map - * @param context The conversion context - * @return The line string modeled value or {@code null} - */ - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"LineString".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON line string type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (!(coordinates instanceof List coordinatePairs)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoLineString value: " + persistedValue); - } - List lineCoordinates = new ArrayList<>(coordinatePairs.size()); - for (Object pointValue : coordinatePairs) { - if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { - throw new IllegalArgumentException("Invalid GeoJSON line string coordinate pair: " + pointValue); - } - Object x = coordinatePair.get(0); - Object y = coordinatePair.get(1); - if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { - throw new IllegalArgumentException("Invalid GeoJSON line string numeric coordinates: " + pointValue); - } - lineCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); - } - return new MongoGeoLineString(List.copyOf(lineCoordinates)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit line string conversion - */ - public static boolean supportsImplicitLineStringType(Class type) { - return type == MongoGeoLineString.class || type.isAssignableFrom(MongoGeoLineString.class); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java deleted file mode 100644 index 48b1b824fcb..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineString.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON multi-line-string value for MongoDB geospatial fields. - * - * @param coordinates Line string list, each line string as ordered points. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoMultiLineString(List> coordinates) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java deleted file mode 100644 index a3a885d3735..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiLineStringConverter.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoMultiLineString} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoMultiLineStringConverter implements AttributeConverter> { - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoMultiLineString multiLineString)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial multi-line-string value type: " + entityValue.getClass().getName()); - } - List>> coordinates = new ArrayList<>(multiLineString.coordinates().size()); - for (List lineString : multiLineString.coordinates()) { - List> persistedLineString = new ArrayList<>(lineString.size()); - for (MongoGeoPoint point : lineString) { - persistedLineString.add(List.of(point.x(), point.y())); - } - coordinates.add(persistedLineString); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "MultiLineString"); - geoJson.put("coordinates", coordinates); - return geoJson; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"MultiLineString".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-line-string type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (!(coordinates instanceof List lineStrings)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoMultiLineString value: " + persistedValue); - } - List> multiLineStringCoordinates = new ArrayList<>(lineStrings.size()); - for (Object lineStringValue : lineStrings) { - if (!(lineStringValue instanceof List lineString)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-line-string line-string: " + lineStringValue); - } - List lineCoordinates = new ArrayList<>(lineString.size()); - for (Object pointValue : lineString) { - if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { - throw new IllegalArgumentException("Invalid GeoJSON multi-line-string coordinate pair: " + pointValue); - } - Object x = coordinatePair.get(0); - Object y = coordinatePair.get(1); - if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-line-string numeric coordinates: " + pointValue); - } - lineCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); - } - multiLineStringCoordinates.add(lineCoordinates); - } - return new MongoGeoMultiLineString(List.copyOf(multiLineStringCoordinates)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit multi-line-string conversion - */ - public static boolean supportsImplicitMultiLineStringType(Class type) { - return type == MongoGeoMultiLineString.class || type.isAssignableFrom(MongoGeoMultiLineString.class); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java deleted file mode 100644 index 80a9910569c..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPoint.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON multi-point value for MongoDB geospatial fields. - * - * @param coordinates Point list in this multi-point geometry. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoMultiPoint(List coordinates) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java deleted file mode 100644 index f26c77404c6..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPointConverter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoMultiPoint} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoMultiPointConverter implements AttributeConverter> { - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoMultiPoint multiPoint)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial multi-point value type: " + entityValue.getClass().getName()); - } - List> coordinates = new ArrayList<>(multiPoint.coordinates().size()); - for (MongoGeoPoint point : multiPoint.coordinates()) { - coordinates.add(List.of(point.x(), point.y())); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "MultiPoint"); - geoJson.put("coordinates", coordinates); - return geoJson; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"MultiPoint".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-point type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (!(coordinates instanceof List points)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoMultiPoint value: " + persistedValue); - } - List multiPointCoordinates = new ArrayList<>(points.size()); - for (Object pointValue : points) { - if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { - throw new IllegalArgumentException("Invalid GeoJSON multi-point coordinate pair: " + pointValue); - } - Object x = coordinatePair.get(0); - Object y = coordinatePair.get(1); - if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-point numeric coordinates: " + pointValue); - } - multiPointCoordinates.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); - } - return new MongoGeoMultiPoint(List.copyOf(multiPointCoordinates)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit multi-point conversion - */ - public static boolean supportsImplicitMultiPointType(Class type) { - return type == MongoGeoMultiPoint.class || type.isAssignableFrom(MongoGeoMultiPoint.class); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java deleted file mode 100644 index 59758293570..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygon.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON multi-polygon value for MongoDB geospatial fields. - * - * @param coordinates Polygon list, each polygon as list of rings, each ring as ordered points. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoMultiPolygon(List>> coordinates) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java deleted file mode 100644 index beb1d01854a..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoMultiPolygonConverter.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoMultiPolygon} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoMultiPolygonConverter implements AttributeConverter> { - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoMultiPolygon multiPolygon)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial multi-polygon value type: " + entityValue.getClass().getName()); - } - List>>> coordinates = new ArrayList<>(multiPolygon.coordinates().size()); - for (List> polygon : multiPolygon.coordinates()) { - List>> persistedPolygon = new ArrayList<>(polygon.size()); - for (List ring : polygon) { - List> persistedRing = new ArrayList<>(ring.size()); - for (MongoGeoPoint point : ring) { - persistedRing.add(List.of(point.x(), point.y())); - } - persistedPolygon.add(persistedRing); - } - coordinates.add(persistedPolygon); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "MultiPolygon"); - geoJson.put("coordinates", coordinates); - return geoJson; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"MultiPolygon".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-polygon type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (!(coordinates instanceof List polygons)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoMultiPolygon value: " + persistedValue); - } - List>> multiPolygonCoordinates = new ArrayList<>(polygons.size()); - for (Object polygonValue : polygons) { - if (!(polygonValue instanceof List rings)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-polygon polygon: " + polygonValue); - } - List> polygonRings = new ArrayList<>(rings.size()); - for (Object ringValue : rings) { - if (!(ringValue instanceof List ring)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-polygon ring: " + ringValue); - } - List polygonRing = new ArrayList<>(ring.size()); - for (Object pointValue : ring) { - if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { - throw new IllegalArgumentException("Invalid GeoJSON multi-polygon coordinate pair: " + pointValue); - } - Object x = coordinatePair.get(0); - Object y = coordinatePair.get(1); - if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { - throw new IllegalArgumentException("Invalid GeoJSON multi-polygon numeric coordinates: " + pointValue); - } - polygonRing.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); - } - polygonRings.add(polygonRing); - } - multiPolygonCoordinates.add(polygonRings); - } - return new MongoGeoMultiPolygon(List.copyOf(multiPolygonCoordinates)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit multi-polygon conversion - */ - public static boolean supportsImplicitMultiPolygonType(Class type) { - return type == MongoGeoMultiPolygon.class || type.isAssignableFrom(MongoGeoMultiPolygon.class); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java deleted file mode 100644 index c7f25155a51..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPoint.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.serde.annotation.Serdeable; - -/** - * Minimal GeoJSON point-like value for MongoDB geospatial fields. - * - * @param x longitude / x coordinate - * @param y latitude / y coordinate - * @author radovanradic - * @since 5.0.0 - */ -@Serdeable -public record MongoGeoPoint(double x, double y) implements MongoGeoPointLike, MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java deleted file mode 100644 index b6c62002184..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointConverter.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.beans.BeanProperty; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.annotation.NonNull; -import org.jspecify.annotations.Nullable; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoPoint} to and from a GeoJSON-like persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoPointConverter implements AttributeConverter> { - - private static final String[] LONGITUDE_NAMES = {"x", "longitude", "lng", "lon"}; - private static final String[] LATITUDE_NAMES = {"y", "latitude", "lat"}; - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - MongoGeoPoint point; - if (entityValue instanceof MongoGeoPointLike pointLike) { - point = new MongoGeoPoint(pointLike.x(), pointLike.y()); - } else { - point = toPoint(entityValue); - } - Map geoJsonPoint = new LinkedHashMap<>(); - geoJsonPoint.put("type", "Point"); - geoJsonPoint.put("coordinates", List.of(point.x(), point.y())); - return geoJsonPoint; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - MongoGeoPoint point = toPoint(persistedValue); - Class targetType = MongoGeoPoint.class; - if (context instanceof ArgumentConversionContext argumentConversionContext) { - targetType = argumentConversionContext.getArgument().getType(); - } - if (targetType == Object.class - || targetType == MongoGeoPoint.class - || targetType == MongoGeoPointLike.class - || targetType.isAssignableFrom(MongoGeoPoint.class)) { - return point; - } - return toTargetType(point, targetType); - } - - private MongoGeoPoint toPoint(Object value) { - if (value instanceof MongoGeoPointLike pointLike) { - return new MongoGeoPoint(pointLike.x(), pointLike.y()); - } - if (value instanceof Map map) { - return pointFromMap(map); - } - BeanIntrospection introspection = BeanIntrospection.getIntrospection(value.getClass()); - double longitude = readCoordinate(introspection, value, LONGITUDE_NAMES); - double latitude = readCoordinate(introspection, value, LATITUDE_NAMES); - return new MongoGeoPoint(longitude, latitude); - } - - private MongoGeoPoint pointFromMap(Map persistedValue) { - Object type = persistedValue.get("type"); - if (type != null && !"Point".equals(type)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoPoint type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (coordinates instanceof List list && list.size() == 2) { - Object x = list.get(0); - Object y = list.get(1); - if (x instanceof Number xNumber && y instanceof Number yNumber) { - return new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue()); - } - } - throw new IllegalArgumentException("Invalid persisted MongoGeoPoint value: " + persistedValue); - } - - private double readCoordinate(BeanIntrospection introspection, Object value, String[] coordinateNames) { - for (String coordinateName : coordinateNames) { - @SuppressWarnings("unchecked") - BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); - if (beanProperty == null) { - continue; - } - Object coordinateValue = beanProperty.get(value); - if (coordinateValue instanceof Number number) { - return number.doubleValue(); - } - } - throw new IllegalArgumentException("Cannot extract Mongo geospatial coordinates from type [" - + introspection.getBeanType().getName() - + "]; expected numeric properties named one of " - + List.of(coordinateNames)); - } - - private Object toTargetType(MongoGeoPoint point, Class targetType) { - BeanIntrospection introspection = BeanIntrospection.getIntrospection(targetType); - Object instance; - try { - instance = introspection.instantiate(); - } catch (Exception e) { - throw new IllegalArgumentException("Cannot instantiate Mongo geospatial type [" - + targetType.getName() - + "] from coordinates. Provide a no-args constructor or use MongoGeoPointLike.", e); - } - writeCoordinate(introspection, instance, point.x(), LONGITUDE_NAMES); - writeCoordinate(introspection, instance, point.y(), LATITUDE_NAMES); - return instance; - } - - private void writeCoordinate(BeanIntrospection introspection, - Object instance, - double value, - String[] coordinateNames) { - for (String coordinateName : coordinateNames) { - @SuppressWarnings("unchecked") - BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); - if (beanProperty == null || beanProperty.isReadOnly()) { - continue; - } - Class propertyType = beanProperty.getType(); - if (propertyType == double.class || propertyType == Double.class) { - beanProperty.set(instance, value); - return; - } - if (propertyType == float.class || propertyType == Float.class) { - beanProperty.set(instance, (float) value); - return; - } - if (propertyType == int.class || propertyType == Integer.class) { - beanProperty.set(instance, (int) value); - return; - } - if (propertyType == long.class || propertyType == Long.class) { - beanProperty.set(instance, (long) value); - return; - } - } - throw new IllegalArgumentException("Cannot write Mongo geospatial coordinate into type [" - + introspection.getBeanType().getName() - + "]; expected writable numeric property named one of " - + List.of(coordinateNames)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit point conversion - */ - public static boolean supportsImplicitPointType(Class type) { - if (type == MongoGeoPoint.class - || type == MongoGeoPointLike.class - || MongoGeoPointLike.class.isAssignableFrom(type) - || type.isAssignableFrom(MongoGeoPoint.class)) { - return true; - } - return hasPointLikeBeanShape(type); - } - - private static boolean hasPointLikeBeanShape(Class type) { - BeanIntrospection introspection; - try { - introspection = BeanIntrospection.getIntrospection(type); - } catch (Exception e) { - return false; - } - return hasNumericCoordinateProperty(introspection, LONGITUDE_NAMES) - && hasNumericCoordinateProperty(introspection, LATITUDE_NAMES); - } - - private static boolean hasNumericCoordinateProperty(BeanIntrospection introspection, String[] coordinateNames) { - for (String coordinateName : coordinateNames) { - @SuppressWarnings("unchecked") - BeanProperty beanProperty = (BeanProperty) introspection.getProperty(coordinateName).orElse(null); - if (beanProperty == null || beanProperty.isReadOnly()) { - continue; - } - Class propertyType = beanProperty.getType(); - if (propertyType == double.class || propertyType == Double.class - || propertyType == float.class || propertyType == Float.class - || propertyType == int.class || propertyType == Integer.class - || propertyType == long.class || propertyType == Long.class) { - return true; - } - } - return false; - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java deleted file mode 100644 index 758e512d5cf..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPointLike.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -/** - * Contract for point-like geospatial values that can be serialized to GeoJSON points. - * - * @author radovanradic - * @since 5.0.0 - */ -public interface MongoGeoPointLike { - - /** - * @return The x / longitude coordinate. - */ - double x(); - - /** - * @return The y / latitude coordinate. - */ - double y(); -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java deleted file mode 100644 index 9672b519ea6..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygon.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import java.util.List; - -/** - * Minimal GeoJSON polygon value for MongoDB geospatial fields. - * - * @param coordinates Polygon rings, each ring as ordered list of points. - * @author radovanradic - * @since 5.0.0 - */ -public record MongoGeoPolygon(List> coordinates) implements MongoGeoGeometry { -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java deleted file mode 100644 index cb2ca8074c4..00000000000 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/geo/MongoGeoPolygonConverter.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2017-2026 original authors - * - * 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 - * - * https://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 io.micronaut.data.mongodb.geo; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import jakarta.inject.Singleton; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts {@link MongoGeoPolygon} to and from a GeoJSON persisted map. - * - * @author radovanradic - * @since 5.0.0 - */ -@Singleton -public final class MongoGeoPolygonConverter implements AttributeConverter> { - - @Override - public @Nullable Map convertToPersistedValue(@Nullable Object entityValue, - @NonNull ConversionContext context) { - if (entityValue == null) { - return null; - } - if (!(entityValue instanceof MongoGeoPolygon polygon)) { - throw new IllegalArgumentException("Unsupported Mongo geospatial polygon value type: " + entityValue.getClass().getName()); - } - List>> coordinates = new ArrayList<>(polygon.coordinates().size()); - for (List ring : polygon.coordinates()) { - List> persistedRing = new ArrayList<>(ring.size()); - for (MongoGeoPoint point : ring) { - persistedRing.add(List.of(point.x(), point.y())); - } - coordinates.add(persistedRing); - } - Map geoJson = new LinkedHashMap<>(); - geoJson.put("type", "Polygon"); - geoJson.put("coordinates", coordinates); - return geoJson; - } - - @Override - public @Nullable Object convertToEntityValue(@Nullable Map persistedValue, - @NonNull ConversionContext context) { - if (persistedValue == null) { - return null; - } - Object type = persistedValue.get("type"); - if (type != null && !"Polygon".equals(type)) { - throw new IllegalArgumentException("Invalid GeoJSON polygon type: " + type); - } - Object coordinates = persistedValue.get("coordinates"); - if (!(coordinates instanceof List rings)) { - throw new IllegalArgumentException("Invalid persisted MongoGeoPolygon value: " + persistedValue); - } - List> polygonRings = new ArrayList<>(rings.size()); - for (Object ringValue : rings) { - if (!(ringValue instanceof List ring)) { - throw new IllegalArgumentException("Invalid GeoJSON polygon ring: " + ringValue); - } - List polygonRing = new ArrayList<>(ring.size()); - for (Object pointValue : ring) { - if (!(pointValue instanceof List coordinatePair) || coordinatePair.size() != 2) { - throw new IllegalArgumentException("Invalid GeoJSON polygon coordinate pair: " + pointValue); - } - Object x = coordinatePair.get(0); - Object y = coordinatePair.get(1); - if (!(x instanceof Number xNumber) || !(y instanceof Number yNumber)) { - throw new IllegalArgumentException("Invalid GeoJSON polygon numeric coordinates: " + pointValue); - } - polygonRing.add(new MongoGeoPoint(xNumber.doubleValue(), yNumber.doubleValue())); - } - polygonRings.add(polygonRing); - } - return new MongoGeoPolygon(List.copyOf(polygonRings)); - } - - /** - * @param type The property type - * @return Whether this type should use implicit polygon conversion - */ - public static boolean supportsImplicitPolygonType(Class type) { - return type == MongoGeoPolygon.class || type.isAssignableFrom(MongoGeoPolygon.class); - } -} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java index 30fb9bd9bea..74320503aad 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java @@ -28,7 +28,6 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.Query; -import io.micronaut.data.annotation.MappedProperty; import io.micronaut.data.document.model.query.builder.MongoQueryBuilder; import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.intercept.annotation.DataMethod; @@ -42,8 +41,6 @@ import io.micronaut.data.model.runtime.StoredQuery; import io.micronaut.data.model.runtime.convert.AttributeConverter; import io.micronaut.data.mongodb.annotation.MongoCollation; -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; -import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.data.mongodb.annotation.MongoProjection; import io.micronaut.data.mongodb.annotation.MongoSort; import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions; @@ -397,10 +394,6 @@ public Object convert(@Nullable Object value, @Nullable RuntimePersistentPropert if (converter != null) { return converter.convertToPersistedValue(value, createTypeConversionContext(property, property.getArgument())); } - if (shouldUseImplicitGeoConverter(property)) { - AttributeConverter implicitConverter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(property)); - return implicitConverter.convertToPersistedValue(value, createTypeConversionContext(property, property.getArgument())); - } } return value; } @@ -409,16 +402,7 @@ public Object convert(@Nullable Object value, @Nullable RuntimePersistentPropert @Override public Object convert(@Nullable Class converterClass, @Nullable Object value, @Nullable Argument argument) { if (converterClass == null || converterClass == Object.class) { - if (value == null) { - return value; - } - Class geoType = argument != null ? argument.getType() : value.getClass(); - if (!MongoGeoConverters.supportsImplicitGeoType(geoType)) { - return value; - } - AttributeConverter implicitConverter = attributeConverterRegistry.getConverter(MongoGeoConverters.resolveImplicitGeoConverterClass(geoType)); - ConversionContext conversionContext = argument != null ? ConversionContext.of(argument) : ConversionContext.of(Argument.of(geoType)); - return implicitConverter.convertToPersistedValue(value, conversionContext); + return value; } AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); ConversionContext conversionContext = createTypeConversionContext(null, argument); @@ -449,18 +433,6 @@ public void bindMany(QueryParameterBinding binding, Collection values) { return (Map.Entry) holder[0]; } - private boolean shouldUseImplicitGeoConverter(RuntimePersistentProperty property) { - if (!property.getAnnotationMetadata().isAnnotationPresent(MongoGeoIndexed.class)) { - return false; - } - Class converterClass = property.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); - return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(property.getType()); - } - - private Class resolveImplicitGeoConverterClass(RuntimePersistentProperty property) { - return MongoGeoConverters.resolveImplicitGeoConverterClass(property.getType()); - } - private BsonValue replaceQueryParametersInBsonValue(BsonValue value, @Nullable InvocationContext invocationContext, @Nullable E entity) { if (value instanceof BsonDocument bsonDocument) { BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER, null); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java index f3722dd3710..8166b32dd93 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java @@ -27,9 +27,7 @@ import io.micronaut.data.document.serde.OneRelationDeserializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; -import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Decoder; import io.micronaut.serde.Deserializer; import io.micronaut.serde.LimitingStream; @@ -47,7 +45,6 @@ import java.io.IOException; import java.util.Collection; -import java.util.Map; /** * The Micronaut Data's Serde's {@link Deserializer.DecoderContext}. @@ -180,22 +177,6 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume @Override public Deserializer findDeserializer(Argument type) throws SerdeException { - if (shouldUseImplicitGeoConverter(type)) { - Argument mapArgument = Argument.of(Map.class); - Deserializer deserializer = findDeserializer(mapArgument).createSpecific(this, mapArgument); - AttributeConverter converter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(type)); - return (Deserializer) new Deserializer<>() { - @Override - @Nullable - public Object deserialize(Decoder decoder, DecoderContext context, Argument argument) throws IOException { - if (decoder.decodeNull()) { - return null; - } - Object deserialized = ((Deserializer) (Deserializer) deserializer).deserialize(decoder, context, Argument.OBJECT_ARGUMENT); - return converter.convertToEntityValue(deserialized, ConversionContext.of(type)); - } - }; - } Codec codec = codecRegistry.get(type.getType(), codecRegistry); if (codec instanceof MappedCodec mappedCodec) { return mappedCodec.deserializer; @@ -206,18 +187,6 @@ public Object deserialize(Decoder decoder, DecoderContext context, Argument type) { - if (!type.isAnnotationPresent(MongoGeoIndexed.class)) { - return false; - } - Class converterClass = type.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); - return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(type.getType()); - } - - private Class resolveImplicitGeoConverterClass(Argument type) { - return MongoGeoConverters.resolveImplicitGeoConverterClass(type.getType()); - } - @Override public D findNamingStrategy(Class namingStrategyClass) throws SerdeException { if (namingStrategyClass == IdPropertyNamingStrategy.class) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java index a05d63b2fc5..47bbb7da4be 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java @@ -26,9 +26,7 @@ import io.micronaut.data.document.serde.IdSerializer; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.convert.AttributeConverter; -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; import io.micronaut.data.mongodb.conf.MongoDataConfiguration; -import io.micronaut.data.mongodb.geo.MongoGeoConverters; import io.micronaut.serde.Encoder; import io.micronaut.serde.Serializer; import io.micronaut.serde.bson.custom.CodecBsonDecoder; @@ -41,7 +39,6 @@ import org.bson.types.ObjectId; import java.io.IOException; -import java.util.Map; /** * The Micronaut Data's Serde's {@link Serializer.EncoderContext}. @@ -161,26 +158,6 @@ public void serialize(Encoder encoder, EncoderContext context, Argument type, @Override public Serializer findSerializer(Argument type) throws SerdeException { - if (shouldUseImplicitGeoConverter(type)) { - Argument mapArgument = Argument.of(Map.class); - Serializer serializer = findSerializer(mapArgument).createSpecific(this, mapArgument); - AttributeConverter converter = attributeConverterRegistry.getConverter(resolveImplicitGeoConverterClass(type)); - return (Serializer) new Serializer<>() { - @Override - public void serialize(Encoder encoder, EncoderContext context, Argument argument, Object value) throws IOException { - if (value == null) { - encoder.encodeNull(); - return; - } - Object converted = converter.convertToPersistedValue(value, ConversionContext.of(type)); - if (converted == null) { - encoder.encodeNull(); - return; - } - serializer.serialize(encoder, context, mapArgument, (Map) converted); - } - }; - } Codec codec = codecRegistry.get(type.getType(), codecRegistry); if (codec instanceof MappedCodec mappedCodec) { return mappedCodec.serializer; @@ -191,18 +168,6 @@ public void serialize(Encoder encoder, EncoderContext context, Argument argum return parent.findSerializer(type); } - private boolean shouldUseImplicitGeoConverter(Argument type) { - if (!type.isAnnotationPresent(MongoGeoIndexed.class)) { - return false; - } - Class converterClass = type.getAnnotationMetadata().classValue(MappedProperty.class, "converter").orElse(null); - return (converterClass == null || converterClass == Object.class) && MongoGeoConverters.supportsImplicitGeoType(type.getType()); - } - - private Class resolveImplicitGeoConverterClass(Argument type) { - return MongoGeoConverters.resolveImplicitGeoConverterClass(type.getType()); - } - @Override public D findNamingStrategy(Class namingStrategyClass) throws SerdeException { if (namingStrategyClass == IdPropertyNamingStrategy.class) { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy index 55a320905f5..20f79795439 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy @@ -22,8 +22,6 @@ import io.micronaut.data.document.model.query.builder.MongoQueryBuilder import io.micronaut.data.document.mongodb.entities.Test import io.micronaut.data.document.tck.entities.Settlement import io.micronaut.data.document.tck.entities.SettlementPk -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.mongodb.geo.MongoGeoPolygon import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery @@ -143,53 +141,7 @@ class MongoCriteriaSpec extends Specification { def colors = Arrays.asList("red", "white") def parameter = cb.literal(colors) ((PersistentEntityCriteriaBuilder)cb).arrayContains(root.get("colors"), parameter) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).text("coffee shop") - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).text( - cb.parameter(String), - cb.literal("en"), - cb.literal(true), - cb.literal(false) - ) - } as Specification, - { root, query, cb -> - def polygon = new MongoGeoPolygon([[ - new MongoGeoPoint(-74.0d, 40.0d), - new MongoGeoPoint(-74.0d, 41.0d), - new MongoGeoPoint(-73.0d, 41.0d), - new MongoGeoPoint(-73.0d, 40.0d), - new MongoGeoPoint(-74.0d, 40.0d) - ]]) - ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get("locations"), cb.literal(polygon)) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get("locations"), cb.literal(new MongoGeoPoint(-73.99d, 40.75d))) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).near( - root.get("locations"), - cb.literal(new MongoGeoPoint(-73.98d, 40.74d)), - cb.literal(10d), - cb.literal(100d) - ) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).near(root.get("locations"), cb.literal(new MongoGeoPoint(-73.97d, 40.73d))) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).nearSphere(root.get("locations"), cb.parameter(MongoGeoPoint)) - } as Specification, - { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).nearSphere( - root.get("locations"), - cb.literal(new MongoGeoPoint(-73.96d, 40.72d)), - cb.literal(5d), - cb.literal(50d) - ) - } as Specification, + } as Specification ] expectedWhereQuery << [ '{enabled:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', @@ -201,15 +153,7 @@ class MongoCriteriaSpec extends Specification { '''{name:{$in:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', '''{name:{$in:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', '''{name:{$nin:[{$mn_qp:0},{$mn_qp:1},{$mn_qp:2}]}}''', - '{colors:{$all:[{$mn_qp:0}]}}', - '{$text:{$search:{$mn_qp:0}}}', - '{$text:{$search:{$mn_qp:0},$language:{$mn_qp:1},$caseSensitive:{$mn_qp:2},$diacriticSensitive:{$mn_qp:3}}}', - '{locations:{$geoWithin:{$geometry:{$mn_qp:0}}}}', - '{locations:{$geoIntersects:{$geometry:{$mn_qp:0}}}}', - '{locations:{$near:{$geometry:{$mn_qp:0},$minDistance:{$mn_qp:1},$maxDistance:{$mn_qp:2}}}}', - '{locations:{$near:{$geometry:{$mn_qp:0}}}}', - '{locations:{$nearSphere:{$geometry:{$mn_qp:0}}}}', - '{locations:{$nearSphere:{$geometry:{$mn_qp:0},$minDistance:{$mn_qp:1},$maxDistance:{$mn_qp:2}}}}' + '{colors:{$all:[{$mn_qp:0}]}}' ] } @@ -258,47 +202,6 @@ class MongoCriteriaSpec extends Specification { ] } - void "test negated text predicate is unsupported"() { - given: - PersistentEntityRoot entityRoot = createRoot(criteriaQuery) - criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder).text("coffee").not()) - - when: - getQuery(criteriaQuery) - - then: - def e = thrown(UnsupportedOperationException) - e.message.contains('$text') - } - - void "test negated near predicate is unsupported"() { - given: - PersistentEntityRoot entityRoot = createRoot(criteriaQuery) - criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder) - .near(entityRoot.get("locations"), new MongoGeoPoint(-73.98d, 40.74d)).not()) - - when: - getQuery(criteriaQuery) - - then: - def e = thrown(UnsupportedOperationException) - e.message.contains('$near/$nearSphere') - } - - void "test negated nearSphere predicate is unsupported"() { - given: - PersistentEntityRoot entityRoot = createRoot(criteriaQuery) - criteriaQuery.where(((PersistentEntityCriteriaBuilder) criteriaBuilder) - .nearSphere(entityRoot.get("locations"), new MongoGeoPoint(-73.98d, 40.74d)).not()) - - when: - getQuery(criteriaQuery) - - then: - def e = thrown(UnsupportedOperationException) - e.message.contains('$near/$nearSphere') - } - @Unroll void "test projection #projection"() { given: diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy index 7c3ed88050f..5e1a253df15 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.data.document.mongodb.geo import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -70,5 +71,5 @@ class GeoIndexedEntity { String id @MongoGeoIndexed(name = 'geo_location_idx', sphereVersion = 3) - Map location + Point location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy index 90d5ecd3ece..c99a1243538 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -1,19 +1,17 @@ package io.micronaut.data.document.mongodb.geovalue import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.MappedProperty import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.mongodb.geo.MongoGeoPointConverter import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -45,7 +43,7 @@ class MongoGeoPointValueIndexCreationSpec extends Specification implements Mongo mongoClient = applicationContext.getBean(MongoClient) } - void 'creates geospatial index on a MongoGeoPoint modeled value'() { + void 'creates geospatial index on a MongoDB Point value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -73,7 +71,6 @@ class GeoPointValueIndexedEntity { String id @TypeDef(type = DataType.OBJECT) - @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) @MongoGeoIndexed(name = 'geo_point_location_idx') - MongoGeoPoint location + Point location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy deleted file mode 100644 index b7e90d50323..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/custom/MongoCustomGeoPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.custom - -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.MappedProperty -import io.micronaut.data.annotation.TypeDef -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPointConverter -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoCustomGeoPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient - - @Shared - CustomGeoPointValueIndexedEntityRepository repository - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.custom'] - } - - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoCollectionsCreator - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) - repository = applicationContext.getBean(CustomGeoPointValueIndexedEntityRepository) - } - - void 'creates geospatial index on a custom point-like modeled value'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(expectedCollectionsCreatorBeanType()) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'custom_geo_point_value_indexed_entities') - assert indexes*.name.contains('custom_geo_point_location_idx') - def index = indexes.find { it.name == 'custom_geo_point_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2dsphere' - } - } - - void 'persists and reads arbitrary custom geospatial modeled value'() { - when: - def saved = repository.save(new CustomGeoPointValueIndexedEntity(location: new CustomGeoPoint(longitude: 12.5d, latitude: 45.8d))) - def loaded = repository.findById(saved.id).orElseThrow() - - then: - loaded.location.longitude == 12.5d - loaded.location.latitude == 45.8d - } -} - -@MongoRepository -interface CustomGeoPointValueIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('custom_geo_point_value_indexed_entities') -class CustomGeoPointValueIndexedEntity { - @Id - @GeneratedValue - String id - - @TypeDef(type = DataType.OBJECT) - @MappedProperty(converter = MongoGeoPointConverter, converterPersistedType = java.util.Map) - @MongoGeoIndexed(name = 'custom_geo_point_location_idx') - CustomGeoPoint location -} - -@MappedEntity -class CustomGeoPoint { - double longitude - double latitude -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy index a8f834cf63c..5c42d2f8f14 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.data.document.mongodb.geovalue.geometrycollection import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.GeometryCollection import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -10,12 +11,6 @@ import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoGeometryCollection -import io.micronaut.data.mongodb.geo.MongoGeoLineString -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -29,9 +24,6 @@ class MongoGeoGeometryCollectionValueIndexCreationSpec extends Specification imp @Shared MongoClient mongoClient - @Shared - GeoGeometryCollectionValueIndexedEntityRepository repository - @Override List getPackageNames() { ['io.micronaut.data.document.mongodb.geovalue.geometrycollection'] @@ -47,10 +39,9 @@ class MongoGeoGeometryCollectionValueIndexCreationSpec extends Specification imp 'micronaut.data.mongodb.create-indexes' : 'true' ]) mongoClient = applicationContext.getBean(MongoClient) - repository = applicationContext.getBean(GeoGeometryCollectionValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoGeometryCollection modeled value'() { + void 'creates geospatial index on a MongoDB GeometryCollection value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -65,36 +56,8 @@ class MongoGeoGeometryCollectionValueIndexCreationSpec extends Specification imp assert index.fields[0].kind() == '2dsphere' } } - - void 'persists and reads MongoGeoGeometryCollection modeled value'() { - given: - def geometryCollection = new MongoGeoGeometryCollection([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) - ]), - new MongoGeoLineString([ - new MongoGeoPoint(-74.01d, 40.72d), - new MongoGeoPoint(-74.00d, 40.71d) - ]) - ]) - - when: - def saved = repository.save(new GeoGeometryCollectionValueIndexedEntity(geometry: geometryCollection)) - def loaded = repository.findById(saved.id).orElseThrow() - - then: - loaded.geometry.geometries().size() == 3 - loaded.geometry.geometries()[0] instanceof MongoGeoPoint - loaded.geometry.geometries()[1] instanceof MongoGeoMultiPoint - loaded.geometry.geometries()[2] instanceof MongoGeoLineString - } } -@MongoRepository -interface GeoGeometryCollectionValueIndexedEntityRepository extends CrudRepository { -} @MappedEntity('geo_geometry_collection_value_indexed_entities') class GeoGeometryCollectionValueIndexedEntity { @@ -104,5 +67,5 @@ class GeoGeometryCollectionValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_geometry_collection_location_idx') - MongoGeoGeometryCollection geometry + GeometryCollection geometry } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy deleted file mode 100644 index 449025b9527..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/implicit/MongoImplicitCustomGeoPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,95 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.implicit - -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoImplicitCustomGeoPointValueIndexCreationSpec extends Specification implements MongoTestPropertyProvider { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient - - @Shared - ImplicitCustomGeoPointValueIndexedEntityRepository repository - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.implicit'] - } - - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoCollectionsCreator - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) - repository = applicationContext.getBean(ImplicitCustomGeoPointValueIndexedEntityRepository) - } - - void 'creates geospatial index on custom modeled value without explicit converter'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(expectedCollectionsCreatorBeanType()) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'implicit_custom_geo_point_value_indexed_entities') - assert indexes*.name.contains('implicit_custom_geo_point_location_idx') - def index = indexes.find { it.name == 'implicit_custom_geo_point_location_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location' - assert index.fields[0].kind() == '2dsphere' - } - } - - void 'persists and reads custom geospatial modeled value without explicit converter'() { - when: - def saved = repository.save(new ImplicitCustomGeoPointValueIndexedEntity(location: new ImplicitCustomGeoPoint(longitude: 12.5d, latitude: 45.8d))) - def loaded = repository.findById(saved.id).orElseThrow() - - then: - loaded.location.longitude == 12.5d - loaded.location.latitude == 45.8d - } -} - -@MongoRepository -interface ImplicitCustomGeoPointValueIndexedEntityRepository extends CrudRepository { -} - -@MappedEntity('implicit_custom_geo_point_value_indexed_entities') -class ImplicitCustomGeoPointValueIndexedEntity { - @Id - @GeneratedValue - String id - - @TypeDef(type = DataType.OBJECT) - @MongoGeoIndexed(name = 'implicit_custom_geo_point_location_idx') - ImplicitCustomGeoPoint location -} - -@MappedEntity -class ImplicitCustomGeoPoint { - double longitude - double latitude -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy index bee28dec98e..c7fe989963f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.data.document.mongodb.geovalue.linestring import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.LineString +import com.mongodb.client.model.geojson.Position import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -11,8 +13,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoLineString -import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -48,7 +48,7 @@ class MongoGeoLineStringValueIndexCreationSpec extends Specification implements repository = applicationContext.getBean(GeoLineStringValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoLineString modeled value'() { + void 'creates geospatial index on a MongoDB LineString value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -64,12 +64,12 @@ class MongoGeoLineStringValueIndexCreationSpec extends Specification implements } } - void 'persists and reads MongoGeoLineString modeled value'() { + void 'persists and reads MongoDB LineString value'() { given: - def lineString = new MongoGeoLineString([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) + def lineString = new LineString([ + new Position(-73.99d, 40.75d), + new Position(-73.98d, 40.74d), + new Position(-73.97d, 40.73d) ]) when: @@ -77,11 +77,11 @@ class MongoGeoLineStringValueIndexCreationSpec extends Specification implements def loaded = repository.findById(saved.id).orElseThrow() then: - loaded.route.coordinates().size() == 3 - loaded.route.coordinates()[0].x() == -73.99d - loaded.route.coordinates()[0].y() == 40.75d - loaded.route.coordinates()[2].x() == -73.97d - loaded.route.coordinates()[2].y() == 40.73d + loaded.route.coordinates.size() == 3 + loaded.route.coordinates[0].values[0] == -73.99d + loaded.route.coordinates[0].values[1] == 40.75d + loaded.route.coordinates[2].values[0] == -73.97d + loaded.route.coordinates[2].values[1] == 40.73d } } @@ -97,5 +97,5 @@ class GeoLineStringValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_linestring_location_idx') - MongoGeoLineString route + LineString route } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy index 41b4c55a31c..8b568a919b4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.data.document.mongodb.geovalue.multilinestring import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.MultiLineString +import com.mongodb.client.model.geojson.Position import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -11,8 +13,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoMultiLineString -import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -48,7 +48,7 @@ class MongoGeoMultiLineStringValueIndexCreationSpec extends Specification implem repository = applicationContext.getBean(GeoMultiLineStringValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoMultiLineString modeled value'() { + void 'creates geospatial index on a MongoDB MultiLineString value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -64,18 +64,18 @@ class MongoGeoMultiLineStringValueIndexCreationSpec extends Specification implem } } - void 'persists and reads MongoGeoMultiLineString modeled value'() { + void 'persists and reads MongoDB MultiLineString value'() { given: - def multiLineString = new MongoGeoMultiLineString([ + def multiLineString = new MultiLineString([ [ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) + new Position(-73.99d, 40.75d), + new Position(-73.98d, 40.74d), + new Position(-73.97d, 40.73d) ], [ - new MongoGeoPoint(-74.01d, 40.73d), - new MongoGeoPoint(-74.00d, 40.72d), - new MongoGeoPoint(-73.99d, 40.71d) + new Position(-74.01d, 40.73d), + new Position(-74.00d, 40.72d), + new Position(-73.99d, 40.71d) ] ]) @@ -84,12 +84,12 @@ class MongoGeoMultiLineStringValueIndexCreationSpec extends Specification implem def loaded = repository.findById(saved.id).orElseThrow() then: - loaded.paths.coordinates().size() == 2 - loaded.paths.coordinates()[0].size() == 3 - loaded.paths.coordinates()[0][0].x() == -73.99d - loaded.paths.coordinates()[0][0].y() == 40.75d - loaded.paths.coordinates()[1][0].x() == -74.01d - loaded.paths.coordinates()[1][0].y() == 40.73d + loaded.paths.coordinates.size() == 2 + loaded.paths.coordinates[0].size() == 3 + loaded.paths.coordinates[0][0].values[0] == -73.99d + loaded.paths.coordinates[0][0].values[1] == 40.75d + loaded.paths.coordinates[1][0].values[0] == -74.01d + loaded.paths.coordinates[1][0].values[1] == 40.73d } } @@ -105,5 +105,5 @@ class GeoMultiLineStringValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multilinestring_location_idx') - MongoGeoMultiLineString paths + MultiLineString paths } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy index d213c1108f1..e42c2eeef87 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.data.document.mongodb.geovalue.multipoint import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.MultiPoint +import com.mongodb.client.model.geojson.Position import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -11,8 +13,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint -import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -48,7 +48,7 @@ class MongoGeoMultiPointValueIndexCreationSpec extends Specification implements repository = applicationContext.getBean(GeoMultiPointValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoMultiPoint modeled value'() { + void 'creates geospatial index on a MongoDB MultiPoint value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -64,12 +64,12 @@ class MongoGeoMultiPointValueIndexCreationSpec extends Specification implements } } - void 'persists and reads MongoGeoMultiPoint modeled value'() { + void 'persists and reads MongoDB MultiPoint value'() { given: - def multiPoint = new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) + def multiPoint = new MultiPoint([ + new Position(-73.99d, 40.75d), + new Position(-73.98d, 40.74d), + new Position(-73.97d, 40.73d) ]) when: @@ -77,11 +77,11 @@ class MongoGeoMultiPointValueIndexCreationSpec extends Specification implements def loaded = repository.findById(saved.id).orElseThrow() then: - loaded.locations.coordinates().size() == 3 - loaded.locations.coordinates()[0].x() == -73.99d - loaded.locations.coordinates()[0].y() == 40.75d - loaded.locations.coordinates()[2].x() == -73.97d - loaded.locations.coordinates()[2].y() == 40.73d + loaded.locations.coordinates.size() == 3 + loaded.locations.coordinates[0].values[0] == -73.99d + loaded.locations.coordinates[0].values[1] == 40.75d + loaded.locations.coordinates[2].values[0] == -73.97d + loaded.locations.coordinates[2].values[1] == 40.73d } } @@ -97,5 +97,5 @@ class GeoMultiPointValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multipoint_location_idx') - MongoGeoMultiPoint locations + MultiPoint locations } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy index 694a22cfc6f..3309e2e5f57 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy @@ -1,6 +1,9 @@ package io.micronaut.data.document.mongodb.geovalue.multipolygon import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.MultiPolygon +import com.mongodb.client.model.geojson.PolygonCoordinates +import com.mongodb.client.model.geojson.Position import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -11,8 +14,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoMultiPolygon -import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -48,7 +49,7 @@ class MongoGeoMultiPolygonValueIndexCreationSpec extends Specification implement repository = applicationContext.getBean(GeoMultiPolygonValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoMultiPolygon modeled value'() { + void 'creates geospatial index on a MongoDB MultiPolygon value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -64,27 +65,23 @@ class MongoGeoMultiPolygonValueIndexCreationSpec extends Specification implement } } - void 'persists and reads MongoGeoMultiPolygon modeled value'() { + void 'persists and reads MongoDB MultiPolygon value'() { given: - def multiPolygon = new MongoGeoMultiPolygon([ - [ - [ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.99d, 40.74d), - new MongoGeoPoint(-73.99d, 40.75d) - ] - ], - [ - [ - new MongoGeoPoint(-74.01d, 40.73d), - new MongoGeoPoint(-74.00d, 40.73d), - new MongoGeoPoint(-74.00d, 40.72d), - new MongoGeoPoint(-74.01d, 40.72d), - new MongoGeoPoint(-74.01d, 40.73d) - ] - ] + def multiPolygon = new MultiPolygon([ + new PolygonCoordinates([ + new Position(-73.99d, 40.75d), + new Position(-73.98d, 40.75d), + new Position(-73.98d, 40.74d), + new Position(-73.99d, 40.74d), + new Position(-73.99d, 40.75d) + ]), + new PolygonCoordinates([ + new Position(-74.01d, 40.73d), + new Position(-74.00d, 40.73d), + new Position(-74.00d, 40.72d), + new Position(-74.01d, 40.72d), + new Position(-74.01d, 40.73d) + ]) ]) when: @@ -92,13 +89,12 @@ class MongoGeoMultiPolygonValueIndexCreationSpec extends Specification implement def loaded = repository.findById(saved.id).orElseThrow() then: - loaded.areas.coordinates().size() == 2 - loaded.areas.coordinates()[0].size() == 1 - loaded.areas.coordinates()[0][0].size() == 5 - loaded.areas.coordinates()[0][0][0].x() == -73.99d - loaded.areas.coordinates()[0][0][0].y() == 40.75d - loaded.areas.coordinates()[1][0][0].x() == -74.01d - loaded.areas.coordinates()[1][0][0].y() == 40.73d + loaded.areas.coordinates.size() == 2 + loaded.areas.coordinates[0].exterior.size() == 5 + loaded.areas.coordinates[0].exterior[0].values[0] == -73.99d + loaded.areas.coordinates[0].exterior[0].values[1] == 40.75d + loaded.areas.coordinates[1].exterior[0].values[0] == -74.01d + loaded.areas.coordinates[1].exterior[0].values[1] == 40.73d } } @@ -114,5 +110,5 @@ class GeoMultiPolygonValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multipolygon_location_idx') - MongoGeoMultiPolygon areas + MultiPolygon areas } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy index c882680111a..bf24dc89204 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.data.document.mongodb.geovalue.polygon import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.Polygon +import com.mongodb.client.model.geojson.Position import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -11,8 +13,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.mongodb.geo.MongoGeoPolygon import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared @@ -48,7 +48,7 @@ class MongoGeoPolygonValueIndexCreationSpec extends Specification implements Mon repository = applicationContext.getBean(GeoPolygonValueIndexedEntityRepository) } - void 'creates geospatial index on a MongoGeoPolygon modeled value'() { + void 'creates geospatial index on a MongoDB Polygon value'() { given: def conditions = new PollingConditions(timeout: 10, delay: 0.25) @@ -64,16 +64,14 @@ class MongoGeoPolygonValueIndexCreationSpec extends Specification implements Mon } } - void 'persists and reads MongoGeoPolygon modeled value'() { + void 'persists and reads MongoDB Polygon value'() { given: - def polygon = new MongoGeoPolygon([ - [ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.99d, 40.74d), - new MongoGeoPoint(-73.99d, 40.75d) - ] + def polygon = new Polygon([ + new Position(-73.99d, 40.75d), + new Position(-73.98d, 40.75d), + new Position(-73.98d, 40.74d), + new Position(-73.99d, 40.74d), + new Position(-73.99d, 40.75d) ]) when: @@ -81,10 +79,9 @@ class MongoGeoPolygonValueIndexCreationSpec extends Specification implements Mon def loaded = repository.findById(saved.id).orElseThrow() then: - loaded.area.coordinates().size() == 1 - loaded.area.coordinates()[0].size() == 5 - loaded.area.coordinates()[0][0].x() == -73.99d - loaded.area.coordinates()[0][0].y() == 40.75d + loaded.area.coordinates.exterior.size() == 5 + loaded.area.coordinates.exterior[0].values[0] == -73.99d + loaded.area.coordinates.exterior[0].values[1] == 40.75d } } @@ -100,5 +97,5 @@ class GeoPolygonValueIndexedEntity { @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_polygon_location_idx') - MongoGeoPolygon area + Polygon area } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy deleted file mode 100644 index 179fc8cda9e..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryParameterBindingSpec.groovy +++ /dev/null @@ -1,103 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.rawquery - -import io.micronaut.context.ApplicationContext -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -class MongoGeoRawQueryParameterBindingSpec extends Specification implements MongoTestPropertyProvider { - - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoGeoRawQueryEntityRepository repository - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.rawquery'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - repository = applicationContext.getBean(MongoGeoRawQueryEntityRepository) - } - - void setup() { - repository.deleteAll() - } - - void 'binds modeled geospatial parameter in @MongoFindQuery as GeoJSON'() { - given: - def multiPoint = new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) - ]) - - when: - def saved = repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) - def loaded = repository.findByIntersectsGeometry(new MongoGeoPoint(-73.99d, 40.75d)) - - then: - loaded.present - loaded.get().id == saved.id - } - - void 'binds modeled geospatial parameter typed as Object in @MongoFindQuery as GeoJSON'() { - given: - def multiPoint = new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) - ]) - - when: - def saved = repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) - def loaded = repository.findByIntersectsGeometryObject(new MongoGeoPoint(-73.99d, 40.75d)) - - then: - loaded.present - loaded.get().id == saved.id - } - - void 'keeps null raw-query geospatial parameter as null binding'() { - given: - def multiPoint = new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) - ]) - repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) - - when: - def loaded = repository.findByLocationsRawNullable(null) - - then: - !loaded.present - } - - void 'respects explicit parameter converter and does not apply implicit geospatial fallback'() { - given: - def multiPoint = new MongoGeoMultiPoint([ - new MongoGeoPoint(-73.99d, 40.75d), - new MongoGeoPoint(-73.98d, 40.74d), - new MongoGeoPoint(-73.97d, 40.73d) - ]) - repository.save(new MongoGeoRawQueryEntity(locations: multiPoint)) - - when: - repository.findByIntersectsGeometryWithExplicitConverter(new MongoGeoPoint(-73.99d, 40.75d)) - - then: - def e = thrown(Exception) - e.message.contains('Longitude/latitude is out of bounds') - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy deleted file mode 100644 index 0f65d070897..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/query/MongoCriteriaQueryOperatorsExecutionSpec.groovy +++ /dev/null @@ -1,249 +0,0 @@ -package io.micronaut.data.document.mongodb.query - -import io.micronaut.context.ApplicationContext -import io.micronaut.core.convert.ConversionContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed -import io.micronaut.data.mongodb.geo.MongoGeoPoint -import io.micronaut.data.mongodb.geo.MongoGeoPointConverter -import io.micronaut.data.mongodb.geo.MongoGeoPolygon -import io.micronaut.data.mongodb.geo.MongoGeoPolygonConverter -import io.micronaut.data.repository.CrudRepository -import io.micronaut.data.repository.jpa.JpaSpecificationExecutor -import io.micronaut.data.repository.jpa.criteria.PredicateSpecification -import io.micronaut.data.repository.jpa.criteria.QuerySpecification -import org.jspecify.annotations.NonNull -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification - -class MongoCriteriaQueryOperatorsExecutionSpec extends Specification implements MongoTestPropertyProvider { - - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.query'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - } - - def cleanup() { - repository.deleteAll() - } - - void 'criteria text predicate filters persisted entities'() { - given: - repository.saveAll([ - new QueryOperatorEntity(description: 'coffee shop downtown', location: new MongoGeoPoint(-73.9857d, 40.7484d)), - new QueryOperatorEntity(description: 'tea house uptown', location: new MongoGeoPoint(-74.1200d, 40.7200d)), - new QueryOperatorEntity(description: 'coffee roastery', location: new MongoGeoPoint(-73.9800d, 40.7500d)) - ]) - - QuerySpecification specification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).text('coffee') - } as QuerySpecification - - when: - def results = repository.findAll(specification) - - then: - results*.description as Set == ['coffee shop downtown', 'coffee roastery'] as Set - } - - void 'criteria geoWithin and geoIntersects filter persisted entities'() { - given: - def inside = new MongoGeoPoint(-73.9857d, 40.7484d) - def outside = new MongoGeoPoint(-74.3000d, 40.6000d) - repository.saveAll([ - new QueryOperatorEntity(description: 'inside', location: inside), - new QueryOperatorEntity(description: 'outside', location: outside) - ]) - - def polygon = [ - type : 'Polygon', - coordinates: [[ - [-74.0500d, 40.7000d], - [-74.0500d, 40.8000d], - [-73.9000d, 40.8000d], - [-73.9000d, 40.7000d], - [-74.0500d, 40.7000d] - ]] - ] - - def intersectPoint = [ - type : 'Point', - coordinates: [-73.9857d, 40.7484d] - ] - - QuerySpecification withinSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get('location'), cb.literal(polygon)) - } as QuerySpecification - - QuerySpecification intersectsSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get('location'), cb.literal(intersectPoint)) - } as QuerySpecification - - when: - def withinResults = repository.findAll(withinSpecification) - def intersectsResults = repository.findAll(intersectsSpecification) - - then: - withinResults*.description == ['inside'] - intersectsResults*.description == ['inside'] - } - - void 'criteria near and nearSphere filter by distance'() { - given: - def center = new MongoGeoPoint(-73.9857d, 40.7484d) - def centerGeometry = [ - type : 'Point', - coordinates: [-73.9857d, 40.7484d] - ] - repository.saveAll([ - new QueryOperatorEntity(description: 'near', location: center), - new QueryOperatorEntity(description: 'far', location: new MongoGeoPoint(-74.3000d, 40.6000d)) - ]) - - QuerySpecification nearSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).near( - root.get('location'), - cb.literal(centerGeometry), - cb.literal(0d), - cb.literal(2_000d) - ) - } as QuerySpecification - - QuerySpecification nearSphereSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).nearSphere( - root.get('location'), - cb.literal(centerGeometry), - cb.literal(0d), - cb.literal(2_000d) - ) - } as QuerySpecification - - when: - def nearResults = repository.findAll(nearSpecification) - def nearSphereResults = repository.findAll(nearSphereSpecification) - - then: - nearResults*.description == ['near'] - nearSphereResults*.description == ['near'] - } - - void 'predicate specification supports text and near operators'() { - given: - def center = new MongoGeoPoint(-73.9857d, 40.7484d) - def centerGeometry = new MongoGeoPointConverter().convertToPersistedValue(center, ConversionContext.DEFAULT) - repository.saveAll([ - new QueryOperatorEntity(description: 'coffee near center', location: center), - new QueryOperatorEntity(description: 'coffee far away', location: new MongoGeoPoint(-74.3000d, 40.6000d)), - new QueryOperatorEntity(description: 'tea near center', location: new MongoGeoPoint(-73.9856d, 40.7485d)) - ]) - - PredicateSpecification textSpecification = { root, cb -> - ((PersistentEntityCriteriaBuilder) cb).text('coffee') - } as PredicateSpecification - - PredicateSpecification nearSpecification = { root, cb -> - ((PersistentEntityCriteriaBuilder) cb).near( - root.get('location'), - cb.literal(centerGeometry), - cb.literal(0d), - cb.literal(2_000d) - ) - } as PredicateSpecification - - when: - def textResults = repository.findAll(textSpecification) - def nearResults = repository.findAll(nearSpecification) - - then: - textResults*.description as Set == ['coffee near center', 'coffee far away'] as Set - nearResults*.description as Set == ['coffee near center', 'tea near center'] as Set - } - - void 'criteria execution supports modeled geospatial values via converters'() { - given: - def inside = new MongoGeoPoint(-73.9857d, 40.7484d) - def outside = new MongoGeoPoint(-74.3000d, 40.6000d) - repository.saveAll([ - new QueryOperatorEntity(description: 'inside-modeled', location: inside), - new QueryOperatorEntity(description: 'outside-modeled', location: outside) - ]) - - def modeledPolygon = new MongoGeoPolygon([[ - new MongoGeoPoint(-74.0500d, 40.7000d), - new MongoGeoPoint(-74.0500d, 40.8000d), - new MongoGeoPoint(-73.9000d, 40.8000d), - new MongoGeoPoint(-73.9000d, 40.7000d), - new MongoGeoPoint(-74.0500d, 40.7000d) - ]]) - def pointGeometry = new MongoGeoPointConverter().convertToPersistedValue(inside, ConversionContext.DEFAULT) - def polygonGeometry = new MongoGeoPolygonConverter().convertToPersistedValue(modeledPolygon, ConversionContext.DEFAULT) - - QuerySpecification withinSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).geoWithin(root.get('location'), cb.literal(polygonGeometry)) - } as QuerySpecification - - QuerySpecification intersectsSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).geoIntersects(root.get('location'), cb.literal(pointGeometry)) - } as QuerySpecification - - QuerySpecification nearSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).near(root.get('location'), cb.literal(pointGeometry), cb.literal(0d), cb.literal(2_000d)) - } as QuerySpecification - - QuerySpecification nearSphereSpecification = { root, query, cb -> - ((PersistentEntityCriteriaBuilder) cb).nearSphere(root.get('location'), cb.literal(pointGeometry), cb.literal(0d), cb.literal(2_000d)) - } as QuerySpecification - - when: - def withinResults = repository.findAll(withinSpecification) - def intersectsResults = repository.findAll(intersectsSpecification) - def nearResults = repository.findAll(nearSpecification) - def nearSphereResults = repository.findAll(nearSphereSpecification) - - then: - withinResults*.description == ['inside-modeled'] - intersectsResults*.description == ['inside-modeled'] - nearResults*.description == ['inside-modeled'] - nearSphereResults*.description == ['inside-modeled'] - } - - @NonNull - QueryOperatorRepository getRepository() { - applicationContext.getBean(QueryOperatorRepository) - } -} - -@MongoRepository -interface QueryOperatorRepository extends CrudRepository, JpaSpecificationExecutor { -} - -@MappedEntity('query_operator_entities') -class QueryOperatorEntity { - @Id - @GeneratedValue - String id - - @MongoTextIndexed(name = 'query_operator_description_text_idx') - String description - - @MongoGeoIndexed(name = 'query_operator_location_geo_idx') - MongoGeoPoint location -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy deleted file mode 100644 index e94d9cb5baf..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/custom/MongoReactiveCustomGeoPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.custom - -import io.micronaut.data.document.mongodb.geovalue.custom.MongoCustomGeoPointValueIndexCreationSpec - -class MongoReactiveCustomGeoPointValueIndexCreationSpec extends MongoCustomGeoPointValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy deleted file mode 100644 index 8df57694c1f..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/implicit/MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.implicit - -import io.micronaut.data.document.mongodb.geovalue.implicit.MongoImplicitCustomGeoPointValueIndexCreationSpec - -class MongoReactiveImplicitCustomGeoPointValueIndexCreationSpec extends MongoImplicitCustomGeoPointValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy deleted file mode 100644 index 84f34f2b492..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/query/MongoReactiveCriteriaQueryOperatorsExecutionSpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.query - -import io.micronaut.data.document.mongodb.query.MongoCriteriaQueryOperatorsExecutionSpec - -class MongoReactiveCriteriaQueryOperatorsExecutionSpec extends MongoCriteriaQueryOperatorsExecutionSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy index f85b1b44a2d..14d4b471027 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -301,7 +302,7 @@ class ExistingSphereConflictIndexEntity { String id @MongoGeoIndexed(name = 'existing_sphere_conflict_idx', sphereVersion = 3) - Map location + Point location } @MongoRepository diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy index de789e53ea2..8e8137a6684 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.data.document.mongodb.validation.existingindexcompatibility import com.mongodb.client.MongoClient +import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -106,10 +107,6 @@ class MongoExistingIndexCompatibilitySpec extends Specification implements Mongo } } -@MongoRepository -interface ExistingSimpleIndexEntityRepository extends CrudRepository { -} - @MappedEntity('existing_simple_index_entities') class ExistingSimpleIndexEntity { @Id @@ -154,5 +151,5 @@ class ExistingGeoIndexEntity { String id @MongoGeoIndexed(name = 'existing_geo_location_idx', sphereVersion = 3) - Map location + Point location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy index 5796839e07c..7e082d17289 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.data.document.mongodb.validation.georules +import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id @@ -8,7 +9,6 @@ import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.geo.MongoGeoPoint import io.micronaut.data.repository.CrudRepository import spock.lang.Specification @@ -43,5 +43,5 @@ class InvalidGeoIndexedEntity { String id @MongoGeoIndexed(name = 'invalid_geo_idx', type = MongoGeoIndexType.GEO_2DSPHERE, bits = 26) - MongoGeoPoint location + Point location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy index 11dd281fbf8..c54a502f925 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy @@ -26,8 +26,9 @@ class MongoGeoTypeValidationSpec extends Specification implements MongoTestPrope then: def e = thrown(RuntimeException) - e.message.contains('requires a supported type') + e.message.contains('requires a supported MongoDB GeoJSON type') } + } @MongoRepository diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy new file mode 100644 index 00000000000..79076bfdee0 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy @@ -0,0 +1,49 @@ +package io.micronaut.data.document.mongodb.validation.geotypevalid + +import com.mongodb.client.model.geojson.Point +import com.mongodb.client.model.geojson.Position +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoGeoTypeValidSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.document.mongodb.validation.geotypevalid'] + } + + void 'starts when MongoGeoIndexed uses supported MongoDB GeoJSON type'() { + when: + def context = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + context?.close() + } +} + +@MongoRepository +interface ValidGeoTypeEntityRepository extends CrudRepository { +} + +@MappedEntity('valid_geo_type_entities') +class ValidGeoTypeEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'valid_geo_type_idx') + Point location = new Point(new Position(-73.99d, 40.75d)) +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index c0020afc8e3..24609a662d4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.data.document.mongodb.validation.options +import com.mongodb.client.model.geojson.Point import io.micronaut.data.annotation.Embeddable import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.annotation.Relation @@ -220,7 +221,7 @@ class InvalidTextDefaultLanguageEntity { @MappedEntity('geo_sphere_version_entity') class GeoSphereVersionEntity { @MongoGeoIndexed(name = 'geo_sphere_version_idx', sphereVersion = 3) - Map location + Point location } @MappedEntity('invalid_geo_sphere_version_on_2d_entity') diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java index d135b4c1031..0d6b493bccf 100644 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/entities/Test.java @@ -1,10 +1,10 @@ package io.micronaut.data.document.mongodb.entities; +import com.mongodb.client.model.geojson.MultiPoint; import io.micronaut.data.annotation.GeneratedValue; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; import java.math.BigDecimal; import java.util.List; @@ -31,7 +31,7 @@ class Test { private OtherEntity manyToOneOther; private List colors; - private MongoGeoMultiPoint locations; + private MultiPoint locations; public Test(String name) { this.name = name; @@ -121,11 +121,11 @@ public void setColors(List colors) { this.colors = colors; } - public MongoGeoMultiPoint getLocations() { + public MultiPoint getLocations() { return locations; } - public void setLocations(MongoGeoMultiPoint locations) { + public void setLocations(MultiPoint locations) { this.locations = locations; } } diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java deleted file mode 100644 index 1fcdfac6842..00000000000 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.rawquery; - -import io.micronaut.data.annotation.GeneratedValue; -import io.micronaut.data.annotation.Id; -import io.micronaut.data.annotation.MappedEntity; -import io.micronaut.data.annotation.TypeDef; -import io.micronaut.data.model.DataType; -import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed; -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; - -@MappedEntity("geo_raw_query_entities") -public class MongoGeoRawQueryEntity { - - @Id - @GeneratedValue - private String id; - - @TypeDef(type = DataType.OBJECT) - @MongoGeoIndexed(name = "geo_raw_query_idx") - private MongoGeoMultiPoint locations; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public MongoGeoMultiPoint getLocations() { - return locations; - } - - public void setLocations(MongoGeoMultiPoint locations) { - this.locations = locations; - } -} diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java deleted file mode 100644 index f1ce5875949..00000000000 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/MongoGeoRawQueryEntityRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.rawquery; - -import io.micronaut.data.annotation.TypeDef; -import io.micronaut.data.model.DataType; -import io.micronaut.data.mongodb.annotation.MongoFindQuery; -import io.micronaut.data.mongodb.annotation.MongoRepository; -import io.micronaut.data.mongodb.geo.MongoGeoMultiPoint; -import io.micronaut.data.mongodb.geo.MongoGeoPoint; -import io.micronaut.data.repository.CrudRepository; -import org.jspecify.annotations.Nullable; - -import java.util.Optional; - -@MongoRepository -public interface MongoGeoRawQueryEntityRepository extends CrudRepository { - - @MongoFindQuery(filter = "{'locations': {'$eq': :locations}}") - Optional findByLocationsRaw(MongoGeoMultiPoint locations); - - @MongoFindQuery(filter = "{'locations': {'$eq': :locations}}") - Optional findByLocationsRawNullable(@Nullable MongoGeoMultiPoint locations); - - @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") - Optional findByIntersectsGeometry(MongoGeoPoint geometry); - - @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") - Optional findByIntersectsGeometryObject(Object geometry); - - @MongoFindQuery(filter = "{'locations': {'$geoIntersects': {'$geometry': :geometry}}}") - Optional findByIntersectsGeometryWithExplicitConverter( - @TypeDef(type = DataType.OBJECT, converter = ShiftedGeoPointConverter.class) MongoGeoPoint geometry - ); -} diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java deleted file mode 100644 index 6032069d55e..00000000000 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/geovalue/rawquery/ShiftedGeoPointConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.micronaut.data.document.mongodb.geovalue.rawquery; - -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.data.model.runtime.convert.AttributeConverter; -import io.micronaut.data.mongodb.geo.MongoGeoPoint; -import jakarta.inject.Singleton; - -import java.util.List; -import java.util.Map; - -@Singleton -final class ShiftedGeoPointConverter implements AttributeConverter> { - - @Override - public Map convertToPersistedValue(MongoGeoPoint entityValue, ConversionContext context) { - return Map.of( - "type", "Point", - "coordinates", List.of(entityValue.x() + 100, entityValue.y() + 100) - ); - } - - @Override - public MongoGeoPoint convertToEntityValue(Map persistedValue, ConversionContext context) { - Object coordinates = persistedValue.get("coordinates"); - if (coordinates instanceof List list && list.size() >= 2) { - Number x = (Number) list.get(0); - Number y = (Number) list.get(1); - return new MongoGeoPoint(x.doubleValue() - 100, y.doubleValue() - 100); - } - throw new IllegalArgumentException("Invalid persisted geo point: " + persistedValue); - } -} diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc index 83d9b77020c..58442c04afa 100644 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications.adoc @@ -10,15 +10,6 @@ Each method expects a "specification" which is a functional interface with a set Micronaut Criteria API currently implements only a subset of the API. Most of it is internally used to create queries with predicates and projections. -For MongoDB-specific criteria queries, Micronaut Data also supports: - -- text search predicates (`$text`) -- geospatial predicates (`$geoWithin`, `$geoIntersects`, `$near`, `$nearSphere`) - -See query usage details and examples in xref:mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc[Mongo criteria execute query]. - -NOTE: Negated `$text`, `$near`, and `$nearSphere` predicates are not supported. - Currently, not supported JPA Criteria API features: - Joins with custom `ON` expressions and typed join methods like `joinSet` etc diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc deleted file mode 100644 index 57fd64e594c..00000000000 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc +++ /dev/null @@ -1,141 +0,0 @@ -To find an entity or multiple entities you can use one of the following methods from api:data.jpa.repository.JpaSpecificationExecutor[] interface: - -snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="find",indent="0"] - -As you can see, there are two variations of `findOne`/`findAll` methods. - -First method is expecting api:data.repository.criteria.PredicateSpecification[] which is a simple specification interface that can be implemented to return a predicate: - -[source,java] ----- -import static jakarta.persistence.criteria.*; - -public interface PredicateSpecification { - - // <1> - @Nullable - Predicate toPredicate(@NonNull Root root, // <2> - @NonNull CriteriaBuilder criteriaBuilder // <3> - ); - -} ----- - -<1> The specification is producing a query limiting predicate -<2> The entity root -<3> The criteria builder - -This interface can also be used for update and delete methods, and it provides `or` and `and` methods for combining multiple predicates. - -The second interface is intended only for query criteria because it includes `jakarta.persistence.criteria.CriteriaQuery` as a parameter. - -[source,java] ----- -import static jakarta.persistence.criteria.*; - -public interface QuerySpecification { - - // <1> - @Nullable - Predicate toPredicate(@NonNull Root root, // <2> - @NonNull CriteriaQuery query, // <3> - @NonNull CriteriaBuilder criteriaBuilder // <4> - ); - -} ----- - -<1> The specification is producing a query limiting predicate -<2> The entity root -<3> The criteria query instance -<4> The criteria builder - -For implementing counting queries following methods can be used: - -snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="count",indent="0"] - -You can define criteria specification methods that will help you to create a query: - -snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="specifications",indent="0"] - -Then you can combine them for `find` or `count` queries: - -snippet::example.PersonRepositorySpec[project-base="doc-examples/mongo-example",source="test" tags="find",indent="0"] - -NOTE: The examples use compile-known values, and in this case, it would be better to create custom repository methods which would come with compile-time generates queries and eliminate runtime overhead. -It's recommended to use criteria only for dynamic queries where the query structure is not known at the build-time. - -== MongoDB-specific query operators - -When using MongoDB criteria runtime queries, you can use MongoDB-specific operators through api:data.model.jpa.criteria.PersistentEntityCriteriaBuilder[]. - -=== Text search - -[source,java] ----- -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; - -PredicateSpecification textSearch(String term) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).text(term); -} - -PredicateSpecification textSearchAdvanced(String term) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).text( - cb.literal(term), - cb.literal("en"), - cb.literal(false), - cb.literal(false) - ); -} ----- - -NOTE: MongoDB text search requires a text index on the queried collection (for example ann:data.mongodb.annotation.index.MongoTextIndexed[]). - -=== Geospatial predicates - -[source,java] ----- -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; -import io.micronaut.data.mongodb.geo.MongoGeoPoint; -import io.micronaut.data.mongodb.geo.MongoGeoPolygon; - -PredicateSpecification inArea(MongoGeoPolygon polygon) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).geoWithin( - root.get("location"), - cb.literal(polygon) - ); -} - -PredicateSpecification intersects(MongoGeoPoint point) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).geoIntersects( - root.get("location"), - cb.literal(point) - ); -} - -PredicateSpecification near(MongoGeoPoint point, double minDistance, double maxDistance) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).near( - root.get("location"), - cb.literal(point), - cb.literal(minDistance), - cb.literal(maxDistance) - ); -} - -PredicateSpecification nearSphere(MongoGeoPoint point) { - return (root, cb) -> ((PersistentEntityCriteriaBuilder) cb).nearSphere( - root.get("location"), - cb.literal(point) - ); -} ----- - -NOTE: Geospatial operators require compatible geospatial indexes and data modeling (for example ann:data.mongodb.annotation.index.MongoGeoIndexed[]). - -=== Unsupported negated operators - -Negated MongoDB-specific predicates below are intentionally rejected at query build time: - -- negated `$text` -- negated `$near` -- negated `$nearSphere` From 28cb06349effdcdf4a58ee94692628b26214f206 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Wed, 1 Apr 2026 16:30:36 +0200 Subject: [PATCH 19/34] Remove unneeded code changes, leave only index related stuff. --- .../query/builder/MongoQueryBuilder.java | 12 ---- data-model/build.gradle | 1 - .../PersistentEntityCriteriaBuilder.java | 5 +- .../operations/DefaultMongoStoredQuery.java | 2 +- .../mongodb/serde/DataDecoderContext.java | 4 +- .../mongodb/serde/DataEncoderContext.java | 2 +- .../document/mongodb/MongoCriteriaSpec.groovy | 2 +- .../mongoCriteriaExecuteQuery.adoc | 62 +++++++++++++++++++ 8 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java index d9a27b7cf8f..d66118770c6 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder.java @@ -1356,18 +1356,6 @@ private void handleRegexExpression(Expression leftExpression, query.put(getPropertyPersistName(propertyPath), filterValue); } - @Nullable - private Object valueRepresentation(Expression value) { - if (value instanceof LiteralExpression literalExpression) { - return literalExpression.getValue(); - } - if (value instanceof BindingParameter bindingParameter) { - int index = queryState.pushParameter(bindingParameter, newBindingContext(null, null)); - return Map.of(QUERY_PARAMETER_PLACEHOLDER, index); - } - return value; - } - @Nullable private Object valueRepresentation(PropertyParameterCreator parameterCreator, Expression leftExpression, @Nullable Object value) { PersistentPropertyPath propertyPath = requireProperty(leftExpression).getPropertyPath(); diff --git a/data-model/build.gradle b/data-model/build.gradle index 1f84e7a99bb..bf3d178f18e 100644 --- a/data-model/build.gradle +++ b/data-model/build.gradle @@ -21,7 +21,6 @@ dependencies { compileOnly mnSerde.micronaut.serde.api testCompileOnly mnSerde.micronaut.serde.processor - testImplementation mnSql.jakarta.persistence.api testImplementation mn.micronaut.inject.java.test testImplementation mnSerde.micronaut.serde.jackson testImplementation mn.micronaut.jackson.databind diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 2efe64cd777..753efd0f11f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -16,7 +16,6 @@ package io.micronaut.data.model.jpa.criteria; import io.micronaut.core.annotation.Experimental; -import org.jspecify.annotations.Nullable; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; @@ -54,7 +53,7 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @return The insert criteria * @since 5.0 */ - + PersistentEntityCriteriaInsert createCriteriaInsert(Class targetEntity); /** @@ -65,7 +64,7 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @param ignoreCase If ignore case should be used * @return ascending ordering corresponding to the expression */ - + Order sort(Expression x, boolean ascending, boolean ignoreCase); /** diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java index 74320503aad..9b0b6871248 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoStoredQuery.java @@ -401,7 +401,7 @@ public Object convert(@Nullable Object value, @Nullable RuntimePersistentPropert @Nullable @Override public Object convert(@Nullable Class converterClass, @Nullable Object value, @Nullable Argument argument) { - if (converterClass == null || converterClass == Object.class) { + if (converterClass == null) { return value; } AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java index 8166b32dd93..9625bacd50c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataDecoderContext.java @@ -151,7 +151,7 @@ public Deserializer createSpecific(DecoderContext decoderContext, Argume .orElseThrow(IllegalStateException::new); Argument convertedType = Argument.of(converterPersistedType); AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); - Deserializer deserializer = findDeserializer(convertedType).createSpecific(decoderContext, convertedType); + Deserializer deserializer = findDeserializer(convertedType); return new Deserializer<>() { @Override @Nullable @@ -160,7 +160,7 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume return null; } Object deserialized = deserializer.deserialize(decoder, decoderContext, convertedType); - return converter.convertToEntityValue(deserialized, ConversionContext.of(type)); + return converter.convertToEntityValue(deserialized, ConversionContext.of(convertedType)); } }; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java index 47bbb7da4be..8309dafe41f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/serde/DataEncoderContext.java @@ -125,7 +125,7 @@ public Serializer createSpecific(EncoderContext encoderContext, Argument Class converterPersistedType = type.getAnnotationMetadata().classValue(MappedProperty.class, "converterPersistedType") .orElseThrow(IllegalStateException::new); Argument convertedType = Argument.of(converterPersistedType); - Serializer serializer = findSerializer(convertedType).createSpecific(encoderContext, convertedType); + Serializer serializer = findSerializer(convertedType); AttributeConverter converter = attributeConverterRegistry.getConverter(converterClass); return new Serializer<>() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy index 20f79795439..2540a96b107 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoCriteriaSpec.groovy @@ -141,7 +141,7 @@ class MongoCriteriaSpec extends Specification { def colors = Arrays.asList("red", "white") def parameter = cb.literal(colors) ((PersistentEntityCriteriaBuilder)cb).arrayContains(root.get("colors"), parameter) - } as Specification + } as Specification, ] expectedWhereQuery << [ '{enabled:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc new file mode 100644 index 00000000000..c9f891e99a1 --- /dev/null +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc @@ -0,0 +1,62 @@ +To find an entity or multiple entities you can use one of the following methods from api:data.jpa.repository.JpaSpecificationExecutor[] interface: + +snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="find",indent="0"] + +As you can see, there are two variations of `findOne`/`findAll` methods. + +First method is expecting api:data.repository.criteria.PredicateSpecification[] which is a simple specification interface that can be implemented to return a predicate: + +[source,java] +---- +import static jakarta.persistence.criteria.*; +public interface PredicateSpecification { + // <1> + @Nullable + Predicate toPredicate(@NonNull Root root, // <2> + @NonNull CriteriaBuilder criteriaBuilder // <3> + ); +} +---- + +<1> The specification is producing a query limiting predicate +<2> The entity root +<3> The criteria builder + +This interface can also be used for update and delete methods, and it provides `or` and `and` methods for combining multiple predicates. + +The second interface is intended only for query criteria because it includes `jakarta.persistence.criteria.CriteriaQuery` as a parameter. + +[source,java] +---- +import static jakarta.persistence.criteria.*; +public interface QuerySpecification { + // <1> + @Nullable + Predicate toPredicate(@NonNull Root root, // <2> + @NonNull CriteriaQuery query, // <3> + @NonNull CriteriaBuilder criteriaBuilder // <4> + ); +} +---- + +<1> The specification is producing a query limiting predicate +<2> The entity root +<3> The criteria query instance +<4> The criteria builder + +For implementing counting queries following methods can be used: + +snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="count",indent="0"] + +You can define criteria specification methods that will help you to create a query: + +snippet::example.PersonRepository[project-base="doc-examples/mongo-example",source="main" tags="specifications",indent="0"] + +Then you can combine them for `find` or `count` queries: + +snippet::example.PersonRepositorySpec[project-base="doc-examples/mongo-example",source="test" tags="find",indent="0"] + +NOTE: The examples use compile-known values, and in this case, it would be better to create custom repository methods which would come with compile-time generates queries and eliminate runtime overhead. +It's recommended to use criteria only for dynamic queries where the query structure is not known at the build-time. + + From 4bfb0500c888ffa4c3f50b0277a38e6d01b92672 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Wed, 1 Apr 2026 16:32:20 +0200 Subject: [PATCH 20/34] Remove unneeded code changes, leave only index related stuff. --- .../mongoCriteriaExecuteQuery.adoc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc index c9f891e99a1..abba9584cdc 100644 --- a/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc +++ b/src/main/docs/guide/mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc @@ -9,12 +9,15 @@ First method is expecting api:data.repository.criteria.PredicateSpecification[] [source,java] ---- import static jakarta.persistence.criteria.*; + public interface PredicateSpecification { + // <1> @Nullable Predicate toPredicate(@NonNull Root root, // <2> @NonNull CriteriaBuilder criteriaBuilder // <3> ); + } ---- @@ -29,13 +32,16 @@ The second interface is intended only for query criteria because it includes `ja [source,java] ---- import static jakarta.persistence.criteria.*; + public interface QuerySpecification { + // <1> @Nullable Predicate toPredicate(@NonNull Root root, // <2> @NonNull CriteriaQuery query, // <3> @NonNull CriteriaBuilder criteriaBuilder // <4> ); + } ---- @@ -59,4 +65,3 @@ snippet::example.PersonRepositorySpec[project-base="doc-examples/mongo-example", NOTE: The examples use compile-known values, and in this case, it would be better to create custom repository methods which would come with compile-time generates queries and eliminate runtime overhead. It's recommended to use criteria only for dynamic queries where the query structure is not known at the build-time. - From a57a1efecad7a3a0de815eb57c7de2a10976bd54 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Wed, 1 Apr 2026 22:17:44 +0200 Subject: [PATCH 21/34] Cleanup tests, remove some non needed extra tests. --- .../init/AbstractMongoCollectionsCreator.java | 8 +- .../mongodb/MongoEmbeddedSpec2.groovy | 2 +- .../mongodb/MongoTestPropertyProvider.groovy | 2 +- .../MongoJoinCollectionCreationSpec.groovy | 86 ------------------- ...goReactiveCompoundIndexCreationSpec.groovy | 18 ---- ...tiveClusteredCollectionCreationSpec.groovy | 69 --------------- ...oReactiveCollationIndexCreationSpec.groovy | 18 ---- ...ngoReactiveIndexCommentCreationSpec.groovy | 18 ---- ...ddedCollectionPathIndexCreationSpec.groovy | 73 ---------------- ...mpoundEmbeddedPathIndexCreationSpec.groovy | 82 ------------------ .../MongoReactiveGeoIndexCreationSpec.groovy | 18 ---- ...MongoReactiveGeo2dIndexCreationSpec.groovy | 18 ---- ...activeGeo2dOptionsIndexCreationSpec.groovy | 18 ---- ...eactiveCompoundGeoIndexCreationSpec.groovy | 18 ---- ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 18 ---- ...ctiveGeoPointValueIndexCreationSpec.groovy | 18 ---- ...tryCollectionValueIndexCreationSpec.groovy | 18 ---- ...GeoLineStringValueIndexCreationSpec.groovy | 18 ---- ...ltiLineStringValueIndexCreationSpec.groovy | 18 ---- ...GeoMultiPointValueIndexCreationSpec.groovy | 18 ---- ...oMultiPolygonValueIndexCreationSpec.groovy | 18 ---- ...iveGeoPolygonValueIndexCreationSpec.groovy | 18 ---- ...ongoReactiveHashedIndexCreationSpec.groovy | 18 ---- ...ongoReactiveHiddenIndexCreationSpec.groovy | 18 ---- ...goReactiveWildcardIndexCreationSpec.groovy | 18 ---- ...veTopLevelWildcardIndexCreationSpec.groovy | 18 ---- ...WildcardProjectionIndexCreationSpec.groovy | 18 ---- ...ltipleDeclarationsIndexCreationSpec.groovy | 18 ---- ...ctiveGeoSphereVersionValidationSpec.groovy | 18 ---- ...ongoClusteredCollectionCreationSpec.groovy | 10 +-- .../MongoCollationIndexCreationSpec.groovy | 10 +-- .../MongoIndexCommentCreationSpec.groovy | 10 +-- .../MongoCompoundIndexCreationSpec.groovy | 10 +-- ...ddedCollectionPathIndexCreationSpec.groovy | 10 +-- ...mpoundEmbeddedPathIndexCreationSpec.groovy | 10 +-- .../geo/MongoGeoIndexCreationSpec.groovy | 10 +-- .../geo2d/MongoGeo2dIndexCreationSpec.groovy | 10 +-- .../MongoGeo2dOptionsIndexCreationSpec.groovy | 10 +-- .../MongoCompoundGeoIndexCreationSpec.groovy | 10 +-- ...mpoundGeo2dOptionsIndexCreationSpec.groovy | 10 +-- ...MongoGeoPointValueIndexCreationSpec.groovy | 10 +-- ...tryCollectionValueIndexCreationSpec.groovy | 4 +- ...GeoLineStringValueIndexCreationSpec.groovy | 4 +- ...ltiLineStringValueIndexCreationSpec.groovy | 4 +- ...GeoMultiPointValueIndexCreationSpec.groovy | 4 +- ...oMultiPolygonValueIndexCreationSpec.groovy | 4 +- ...ngoGeoPolygonValueIndexCreationSpec.groovy | 4 +- .../MongoHashedIndexCreationSpec.groovy | 10 +-- .../MongoHiddenIndexCreationSpec.groovy | 14 +-- ...MongoPartialFilterIndexCreationSpec.groovy | 8 +- .../MongoReactiveIndexCreationSpec.groovy | 4 +- ...ctivePartialFilterIndexCreationSpec.groovy | 4 +- ...tiveAggregatedTextIndexCreationSpec.groovy | 4 +- .../MongoReactiveTextIndexCreationSpec.groovy | 4 +- .../MongoReactiveTtlIndexCreationSpec.groovy | 4 +- .../simple/MongoIndexCreationSpec.groovy | 10 +-- ...MongoEmbeddedFieldIndexCreationSpec.groovy | 10 +-- ...ongoAggregatedTextIndexCreationSpec.groovy | 10 +-- .../text/MongoTextIndexCreationSpec.groovy | 10 +-- .../MongoEmbeddedTextIndexCreationSpec.groovy | 10 +-- .../ttl/MongoTtlIndexCreationSpec.groovy | 10 +-- .../MongoClusteredTtlValidationSpec.groovy | 8 +- .../MongoClusteredUniqueValidationSpec.groovy | 10 +-- .../MongoCollationValidationSpec.groovy | 10 +-- ...ndIndexDuplicateFieldValidationSpec.groovy | 4 +- ...poundIndexEmptyFieldsValidationSpec.groovy | 4 +- ...lusteredCollectionCompatibilitySpec.groovy | 4 +- ...tingClusteredCollectionConflictSpec.groovy | 4 +- ...goExistingIndexAdvancedConflictSpec.groovy | 4 +- ...MongoExistingIndexCompatibilitySpec.groovy | 4 +- .../MongoExistingIndexConflictSpec.groovy | 4 +- ...ExistingIndexNameReconciliationSpec.groovy | 4 +- .../MongoCompoundGeoValidationSpec.groovy | 4 +- ...oCompoundGeo2dOptionsValidationSpec.groovy | 4 +- ...ngoCompoundGeoOptionsValidationSpec.groovy | 4 +- ...oCompoundGeo2dOptionsValidationSpec.groovy | 4 +- .../MongoGeoIndexValidationSpec.groovy | 4 +- ...MongoGeoSphereVersionValidationSpec.groovy | 4 +- .../geotype/MongoGeoTypeValidationSpec.groovy | 4 +- .../geotypevalid/MongoGeoTypeValidSpec.groovy | 4 +- ...ndexBootstrapWithoutCollectionsSpec.groovy | 4 +- ...oIndexFailurePolicyWarnContinueSpec.groovy | 4 +- .../MongoCompoundIndexValidationSpec.groovy | 4 +- ...oIndexAdvancedOptionsResolutionSpec.groovy | 2 +- ...undIndexPartialFilterValidationSpec.groovy | 4 +- ...lusteredCollectionCompatibilitySpec.groovy | 4 +- ...tingClusteredCollectionConflictSpec.groovy | 4 +- ...veExistingIndexAdvancedConflictSpec.groovy | 4 +- ...ctiveExistingIndexCompatibilitySpec.groovy | 4 +- ...goReactiveExistingIndexConflictSpec.groovy | 4 +- ...ExistingIndexNameReconciliationSpec.groovy | 4 +- ...ndexBootstrapWithoutCollectionsSpec.groovy | 4 +- ...eIndexFailurePolicyWarnContinueSpec.groovy | 4 +- ...ReactiveStorageEngineValidationSpec.groovy | 4 +- ...eactiveTextIndexVersionConflictSpec.groovy | 4 +- ...iveWildcardProjectionValidationSpec.groovy | 4 +- ...dMultipleDeclarationsValidationSpec.groovy | 4 +- .../MongoStorageEngineValidationSpec.groovy | 4 +- .../text/MongoTextIndexValidationSpec.groovy | 4 +- .../MongoTextIndexVersionConflictSpec.groovy | 4 +- ...MongoCompoundIndexTtlValidationSpec.groovy | 4 +- ...ngoWildcardProjectionValidationSpec.groovy | 4 +- ...dMultipleDeclarationsValidationSpec.groovy | 4 +- .../MongoWildcardIndexCreationSpec.groovy | 8 +- ...goTopLevelWildcardIndexCreationSpec.groovy | 10 +-- ...WildcardProjectionIndexCreationSpec.groovy | 10 +-- ...ltipleDeclarationsIndexCreationSpec.groovy | 10 +-- .../mongo/mongoMapping/mongoIndexes.adoc | 25 +----- 108 files changed, 164 insertions(+), 1055 deletions(-) delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/clustered/MongoClusteredCollectionCreationSpec.groovy (85%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/collation/MongoCollationIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/comment/MongoIndexCommentCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/compound/MongoCompoundIndexCreationSpec.groovy (88%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy (85%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geo/MongoGeoIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geo2d/MongoGeo2dIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geocompound/MongoCompoundGeoIndexCreationSpec.groovy (88%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy (88%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/MongoGeoPointValueIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy (94%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/hashed/MongoHashedIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/hidden/MongoHiddenIndexCreationSpec.groovy (88%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/partialfilter/MongoPartialFilterIndexCreationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/reactive/MongoReactiveIndexCreationSpec.groovy (75%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy (72%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy (74%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/reactive/text/MongoReactiveTextIndexCreationSpec.groovy (75%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy (75%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/simple/MongoIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/text/MongoAggregatedTextIndexCreationSpec.groovy (88%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/text/MongoTextIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/ttl/MongoTtlIndexCreationSpec.groovy (87%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy (83%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy (74%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/collation/MongoCollationValidationSpec.groovy (74%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy (94%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy (94%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy (98%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy (97%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geocompound/MongoCompoundGeoValidationSpec.groovy (93%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/georules/MongoGeoIndexValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geotype/MongoGeoTypeValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/geotypevalid/MongoGeoTypeValidSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy (96%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy (99%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy (56%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy (57%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy (56%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy (57%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy (57%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy (59%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy (69%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy (59%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy (58%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/storageengine/MongoStorageEngineValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/text/MongoTextIndexValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy (94%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy (92%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy (91%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/wildcard/MongoWildcardIndexCreationSpec.groovy (90%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy (85%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy (86%) rename data-mongodb/src/test/groovy/io/micronaut/data/{document/mongodb => mongodb/index}/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy (90%) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index bda1029ff78..371844eed24 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -151,11 +151,11 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, DatabaseOperations databaseOperations = databaseOperationsProvider.get(mongoConfiguration); int indexProcessedCount = 0; int indexFailureCount = 0; - String telemetryDatabaseName = ""; + String databaseName = ""; for (PersistentEntity entity : entities) { Dtbs database = databaseOperations.find(entity); - telemetryDatabaseName = databaseOperations.getDatabaseName(database); + databaseName = databaseOperations.getDatabaseName(database); Set collections = databaseOperations.listCollectionNames(database); String persistedName = mongoCollectionNameProvider.provide(entity); MongoResolvedCollectionOptions desiredCollectionOptions = resolveCollectionOptions(entity); @@ -206,13 +206,13 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, if (createIndexes) { if (indexFailureCount > 0 && indexCreationFailurePolicy == IndexCreationFailurePolicy.WARN_AND_CONTINUE) { LOG.warn("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", - telemetryDatabaseName, + databaseName, indexProcessedCount, indexFailureCount, indexCreationFailurePolicy); } else if (LOG.isInfoEnabled()) { LOG.info("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", - telemetryDatabaseName, + databaseName, indexProcessedCount, indexFailureCount, indexCreationFailurePolicy); diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec2.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec2.groovy index 3b09c18c101..c45e1e70d0d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec2.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec2.groovy @@ -38,7 +38,7 @@ class MongoEmbeddedSpec2 extends Specification implements MongoTestPropertyProvi @Override List getPackageNames() { - return ['example', 'io.micronaut.data'] + return ['io.micronaut.data.document.mongodb', 'io.micronaut.data.document.tck.entities', 'example'] } def cleanup() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoTestPropertyProvider.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoTestPropertyProvider.groovy index ba2214ad8f8..494264834d1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoTestPropertyProvider.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoTestPropertyProvider.groovy @@ -17,7 +17,7 @@ trait MongoTestPropertyProvider implements TestPropertyProvider { } List getPackageNames() { - ['io.micronaut.data'] + ['io.micronaut.data.document.mongodb', 'io.micronaut.data.document.tck.entities'] } } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy deleted file mode 100644 index 1f1f464e0e6..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/joincollection/MongoJoinCollectionCreationSpec.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package io.micronaut.data.document.mongodb.joincollection - -import com.mongodb.client.MongoClient -import groovy.transform.EqualsAndHashCode -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.Relation -import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoJoinCollectionCreationSpec extends Specification implements MongoTestPropertyProvider { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.joincollection'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'false' - ]) - mongoClient = applicationContext.getBean(MongoClient) - } - - void 'creates join collection for many-to-many association at startup'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoCollectionsCreator) - conditions.eventually { - def collectionNames = mongoClient.getDatabase('test').listCollectionNames().into([]) - assert collectionNames.contains('m2m_student') - assert collectionNames.contains('m2m_course') - assert collectionNames.contains('student_course') - } - } -} - -@MongoRepository -interface JoinCollectionStudentRepository extends CrudRepository { -} - -@MongoRepository -interface JoinCollectionCourseRepository extends CrudRepository { -} - -@EqualsAndHashCode(includes = 'id') -@MappedEntity('m2m_student') -class JoinCollectionStudent { - @Id - @GeneratedValue - String id - - String name - - @Relation(value = Relation.Kind.MANY_TO_MANY, cascade = Relation.Cascade.PERSIST) - List courses -} - -@EqualsAndHashCode(includes = 'id') -@MappedEntity('m2m_course') -class JoinCollectionCourse { - @Id - @GeneratedValue - String id - - String name - - @Relation(value = Relation.Kind.MANY_TO_MANY, mappedBy = 'courses') - List students -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy deleted file mode 100644 index 04307cbf3a9..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveCompoundIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive - -import io.micronaut.data.document.mongodb.compound.MongoCompoundIndexCreationSpec - -class MongoReactiveCompoundIndexCreationSpec extends MongoCompoundIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy deleted file mode 100644 index dc1813cf7a1..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/clustered/MongoReactiveClusteredCollectionCreationSpec.groovy +++ /dev/null @@ -1,69 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.clustered - -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import org.bson.Document -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoReactiveClusteredCollectionCreationSpec extends Specification implements MongoSelectReactiveDriver { - @AutoCleanup - @Shared - ApplicationContext applicationContext - - @Shared - MongoClient mongoClient - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.clustered'] - } - - def setupSpec() { - applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - mongoClient = applicationContext.getBean(MongoClient) - } - - void 'creates clustered collection with configured name in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def collection = mongoClient.getDatabase('test').listCollections().into([]).find { it.getString('name') == 'reactive_clustered_indexed_entities' } - assert collection != null - def options = collection.get('options', Document) - assert options != null - def clustered = options.get('clusteredIndex', Document) - assert clustered != null - assert clustered.get('key', Document).getInteger('_id') == 1 - assert clustered.getBoolean('unique') - assert clustered.getString('name') == 'reactive_clustered_idx' - } - } -} - -@MongoRepository -interface ReactiveClusteredIndexedEntityRepository extends CrudRepository { -} - -@MongoClusteredIndex(name = 'reactive_clustered_idx') -@MappedEntity('reactive_clustered_indexed_entities') -class ReactiveClusteredIndexedEntity { - @Id - java.time.Instant id - - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy deleted file mode 100644 index 4a495ce8ff9..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/collation/MongoReactiveCollationIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.collation - -import io.micronaut.data.document.mongodb.collation.MongoCollationIndexCreationSpec - -class MongoReactiveCollationIndexCreationSpec extends MongoCollationIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy deleted file mode 100644 index b53b556d1d2..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/comment/MongoReactiveIndexCommentCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.comment - -import io.micronaut.data.document.mongodb.comment.MongoIndexCommentCreationSpec - -class MongoReactiveIndexCommentCreationSpec extends MongoIndexCommentCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy deleted file mode 100644 index 4d096b73ebc..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/collectionpath/MongoReactiveCompoundEmbeddedCollectionPathIndexCreationSpec.groovy +++ /dev/null @@ -1,73 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.compound.collectionpath - -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoReactiveCompoundCollectionFieldIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - - @AutoCleanup - @Shared - ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - @Shared - MongoClient mongoClient = applicationContext.getBean(MongoClient) - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.compound.collectionpath'] - } - - void 'creates index for collection field (multikey) path in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_embedded_collection_path_entities') - assert indexes*.name.contains('reactive_tags_idx') - def index = indexes.find { it.name == 'reactive_tags_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'tags' - assert index.fields[0].order() == 1 - } - } -} - -@MongoRepository -interface ReactiveCompoundCollectionFieldIndexedEntityRepository extends CrudRepository { -} - -@MongoCompoundIndex( - name = 'reactive_tags_idx', - fields = [ - @MongoCompoundIndexField(value = 'tags', direction = MongoIndexDirection.ASC) - ] -) -@MappedEntity('reactive_compound_embedded_collection_path_entities') -class ReactiveCompoundCollectionFieldIndexedEntity { - @Id - @GeneratedValue - String id - - String name - - List tags -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy deleted file mode 100644 index 0a1cf4d6b83..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/compound/dotpath/MongoReactiveCompoundEmbeddedPathIndexCreationSpec.groovy +++ /dev/null @@ -1,82 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.compound.dotpath - -import com.mongodb.client.MongoClient -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.Embeddable -import io.micronaut.data.annotation.Relation -import io.micronaut.data.document.mongodb.MongoIndexInspector -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex -import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField -import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.repository.CrudRepository -import spock.lang.AutoCleanup -import spock.lang.Shared -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -class MongoReactiveCompoundEmbeddedPathIndexCreationSpec extends Specification implements MongoSelectReactiveDriver { - - @AutoCleanup - @Shared - ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - @Shared - MongoClient mongoClient = applicationContext.getBean(MongoClient) - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.reactive.compound.dotpath'] - } - - void 'creates index for valid embedded dot-notation path in reactive mode'() { - given: - def conditions = new PollingConditions(timeout: 10, delay: 0.25) - - expect: - applicationContext.containsBean(io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator) - conditions.eventually { - def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'reactive_compound_embedded_path_indexed_entities') - assert indexes*.name.contains('reactive_location_state_idx') - def index = indexes.find { it.name == 'reactive_location_state_idx' } - assert index.fields.size() == 1 - assert index.fields[0].path() == 'location.state' - assert index.fields[0].order() == 1 - } - } -} - -@MongoRepository -interface ReactiveCompoundEmbeddedPathIndexedEntityRepository extends CrudRepository { -} - -@MongoCompoundIndex( - name = 'reactive_location_state_idx', - fields = [ - @MongoCompoundIndexField(value = 'location.state', direction = MongoIndexDirection.ASC) - ] -) -@MappedEntity('reactive_compound_embedded_path_indexed_entities') -class ReactiveCompoundEmbeddedPathIndexedEntity { - @Id - @GeneratedValue - String id - - String name - - @Relation(Relation.Kind.EMBEDDED) - ReactiveEmbeddedLocation location -} - -@Embeddable -class ReactiveEmbeddedLocation { - String state - String city -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy deleted file mode 100644 index 1fe1045924a..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo/MongoReactiveGeoIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geo - -import io.micronaut.data.document.mongodb.geo.MongoGeoIndexCreationSpec - -class MongoReactiveGeoIndexCreationSpec extends MongoGeoIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy deleted file mode 100644 index dbfda5cd5e3..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/MongoReactiveGeo2dIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geo2d - -import io.micronaut.data.document.mongodb.geo2d.MongoGeo2dIndexCreationSpec - -class MongoReactiveGeo2dIndexCreationSpec extends MongoGeo2dIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy deleted file mode 100644 index 9660aae53eb..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geo2d/options/MongoReactiveGeo2dOptionsIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geo2d.options - -import io.micronaut.data.document.mongodb.geo2d.options.MongoGeo2dOptionsIndexCreationSpec - -class MongoReactiveGeo2dOptionsIndexCreationSpec extends MongoGeo2dOptionsIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy deleted file mode 100644 index 04644ed8193..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/MongoReactiveCompoundGeoIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geocompound - -import io.micronaut.data.document.mongodb.geocompound.MongoCompoundGeoIndexCreationSpec - -class MongoReactiveCompoundGeoIndexCreationSpec extends MongoCompoundGeoIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy deleted file mode 100644 index cfac781868d..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geocompound/options/MongoReactiveCompoundGeo2dOptionsIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geocompound.options - -import io.micronaut.data.document.mongodb.geocompound.options.MongoCompoundGeo2dOptionsIndexCreationSpec - -class MongoReactiveCompoundGeo2dOptionsIndexCreationSpec extends MongoCompoundGeo2dOptionsIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy deleted file mode 100644 index 5886f04dbac..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/MongoReactiveGeoPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue - -import io.micronaut.data.document.mongodb.geovalue.MongoGeoPointValueIndexCreationSpec - -class MongoReactiveGeoPointValueIndexCreationSpec extends MongoGeoPointValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy deleted file mode 100644 index 141a1666c50..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/geometrycollection/MongoReactiveGeoGeometryCollectionValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.geometrycollection - -import io.micronaut.data.document.mongodb.geovalue.geometrycollection.MongoGeoGeometryCollectionValueIndexCreationSpec - -class MongoReactiveGeoGeometryCollectionValueIndexCreationSpec extends MongoGeoGeometryCollectionValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy deleted file mode 100644 index f5944ec11a5..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/linestring/MongoReactiveGeoLineStringValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.linestring - -import io.micronaut.data.document.mongodb.geovalue.linestring.MongoGeoLineStringValueIndexCreationSpec - -class MongoReactiveGeoLineStringValueIndexCreationSpec extends MongoGeoLineStringValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy deleted file mode 100644 index 58e0256af62..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multilinestring/MongoReactiveGeoMultiLineStringValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.multilinestring - -import io.micronaut.data.document.mongodb.geovalue.multilinestring.MongoGeoMultiLineStringValueIndexCreationSpec - -class MongoReactiveGeoMultiLineStringValueIndexCreationSpec extends MongoGeoMultiLineStringValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy deleted file mode 100644 index cabd28705b7..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipoint/MongoReactiveGeoMultiPointValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.multipoint - -import io.micronaut.data.document.mongodb.geovalue.multipoint.MongoGeoMultiPointValueIndexCreationSpec - -class MongoReactiveGeoMultiPointValueIndexCreationSpec extends MongoGeoMultiPointValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy deleted file mode 100644 index 5c38ba5f9bd..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/multipolygon/MongoReactiveGeoMultiPolygonValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.multipolygon - -import io.micronaut.data.document.mongodb.geovalue.multipolygon.MongoGeoMultiPolygonValueIndexCreationSpec - -class MongoReactiveGeoMultiPolygonValueIndexCreationSpec extends MongoGeoMultiPolygonValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy deleted file mode 100644 index b6005c6c0ed..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/geovalue/polygon/MongoReactiveGeoPolygonValueIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.geovalue.polygon - -import io.micronaut.data.document.mongodb.geovalue.polygon.MongoGeoPolygonValueIndexCreationSpec - -class MongoReactiveGeoPolygonValueIndexCreationSpec extends MongoGeoPolygonValueIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy deleted file mode 100644 index 8327dde2c78..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hashed/MongoReactiveHashedIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.hashed - -import io.micronaut.data.document.mongodb.hashed.MongoHashedIndexCreationSpec - -class MongoReactiveHashedIndexCreationSpec extends MongoHashedIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy deleted file mode 100644 index 4444d48710a..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/hidden/MongoReactiveHiddenIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.hidden - -import io.micronaut.data.document.mongodb.hidden.MongoHiddenIndexCreationSpec - -class MongoReactiveHiddenIndexCreationSpec extends MongoHiddenIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy deleted file mode 100644 index 55e9065e6f4..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/MongoReactiveWildcardIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.wildcard - -import io.micronaut.data.document.mongodb.wildcard.MongoWildcardIndexCreationSpec - -class MongoReactiveWildcardIndexCreationSpec extends MongoWildcardIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy deleted file mode 100644 index 42e43871863..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel - -import io.micronaut.data.document.mongodb.wildcard.toplevel.MongoTopLevelWildcardIndexCreationSpec - -class MongoReactiveTopLevelWildcardIndexCreationSpec extends MongoTopLevelWildcardIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy deleted file mode 100644 index f5d24dc7aa1..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/MongoReactiveTopLevelWildcardProjectionIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel - -import io.micronaut.data.document.mongodb.wildcard.toplevel.MongoTopLevelWildcardProjectionIndexCreationSpec - -class MongoReactiveTopLevelWildcardProjectionIndexCreationSpec extends MongoTopLevelWildcardProjectionIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy deleted file mode 100644 index 370be655789..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/wildcard/toplevel/multiple/MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.reactive.wildcard.toplevel.multiple - -import io.micronaut.data.document.mongodb.wildcard.toplevel.multiple.MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec - -class MongoReactiveTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - Class expectedCollectionsCreatorBeanType() { - io.micronaut.data.mongodb.init.MongoReactiveCollectionsCreator - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy deleted file mode 100644 index c6e731ae475..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivegeosphereversion/MongoReactiveGeoSphereVersionValidationSpec.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.micronaut.data.document.mongodb.validation.reactivegeosphereversion - -import io.micronaut.data.document.mongodb.validation.geosphereversion.MongoGeoSphereVersionValidationSpec - -class MongoReactiveGeoSphereVersionValidationSpec extends MongoGeoSphereVersionValidationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } - - @Override - List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geosphereversion'] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/clustered/MongoClusteredCollectionCreationSpec.groovy similarity index 85% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/clustered/MongoClusteredCollectionCreationSpec.groovy index ec43cc76f6b..6c318e52da3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/clustered/MongoClusteredCollectionCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/clustered/MongoClusteredCollectionCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.clustered +package io.micronaut.data.mongodb.index.clustered import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -6,8 +6,6 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.AutoCleanup import spock.lang.Shared @@ -24,7 +22,7 @@ class MongoClusteredCollectionCreationSpec extends Specification implements Mong @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.clustered'] + ['io.micronaut.data.mongodb.index.clustered'] } def setupSpec() { @@ -55,10 +53,6 @@ class MongoClusteredCollectionCreationSpec extends Specification implements Mong } } -@MongoRepository -interface ClusteredIndexedEntityRepository extends CrudRepository { -} - @MongoClusteredIndex(name = 'clustered_idx') @MappedEntity('clustered_indexed_entities') class ClusteredIndexedEntity { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy index cf695e4ee5b..23b619ba996 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/collation/MongoCollationIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.collation +package io.micronaut.data.mongodb.index.collation import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -8,8 +8,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.AutoCleanup import spock.lang.Shared @@ -26,7 +24,7 @@ class MongoCollationIndexCreationSpec extends Specification implements MongoTest @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.collation'] + ['io.micronaut.data.mongodb.index.collation'] } Class expectedCollectionsCreatorBeanType() { @@ -60,10 +58,6 @@ class MongoCollationIndexCreationSpec extends Specification implements MongoTest } } -@MongoRepository -interface CollationIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('collation_indexed_entities') class CollationIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/comment/MongoIndexCommentCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/comment/MongoIndexCommentCreationSpec.groovy index 74d581522df..3f99c8fb1fa 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/comment/MongoIndexCommentCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/comment/MongoIndexCommentCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.comment +package io.micronaut.data.mongodb.index.comment import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -8,8 +8,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -26,7 +24,7 @@ class MongoIndexCommentCreationSpec extends Specification implements MongoTestPr @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.comment'] + ['io.micronaut.data.mongodb.index.comment'] } Class expectedCollectionsCreatorBeanType() { @@ -58,10 +56,6 @@ class MongoIndexCommentCreationSpec extends Specification implements MongoTestPr } } -@MongoRepository -interface CommentIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('comment_indexed_entities') class CommentIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/MongoCompoundIndexCreationSpec.groovy similarity index 88% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/MongoCompoundIndexCreationSpec.groovy index b2534f34287..18dd950ee68 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/MongoCompoundIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/MongoCompoundIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.compound +package io.micronaut.data.mongodb.index.compound import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -10,8 +10,6 @@ import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -21,7 +19,7 @@ class MongoCompoundIndexCreationSpec extends Specification implements MongoTestP @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.compound'] + ['io.micronaut.data.mongodb.index.compound'] } Class expectedCollectionsCreatorBeanType() { @@ -57,10 +55,6 @@ class MongoCompoundIndexCreationSpec extends Specification implements MongoTestP } } -@MongoRepository -interface CompoundIndexedEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'name_age_idx', unique = true, diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy similarity index 85% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy index c0cb2a0c594..821d88b3719 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/collectionpath/MongoCompoundEmbeddedCollectionPathIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.compound.collectionpath +package io.micronaut.data.mongodb.index.compound.collectionpath import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,11 +7,9 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -21,7 +19,7 @@ class MongoCompoundCollectionFieldIndexCreationSpec extends Specification implem @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.compound.collectionpath'] + ['io.micronaut.data.mongodb.index.compound.collectionpath'] } Class expectedCollectionsCreatorBeanType() { @@ -55,10 +53,6 @@ class MongoCompoundCollectionFieldIndexCreationSpec extends Specification implem } } -@MongoRepository -interface CompoundCollectionFieldIndexedEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'tags_idx', fields = [ diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy index 6f235197131..75750545718 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/dotpath/MongoCompoundEmbeddedPathIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.compound.dotpath +package io.micronaut.data.mongodb.index.compound.dotpath import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -9,11 +9,9 @@ import io.micronaut.data.annotation.Embeddable import io.micronaut.data.annotation.Relation import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -23,7 +21,7 @@ class MongoCompoundEmbeddedPathIndexCreationSpec extends Specification implement @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.compound.dotpath'] + ['io.micronaut.data.mongodb.index.compound.dotpath'] } Class expectedCollectionsCreatorBeanType() { @@ -57,10 +55,6 @@ class MongoCompoundEmbeddedPathIndexCreationSpec extends Specification implement } } -@MongoRepository -interface CompoundEmbeddedPathIndexedEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'location_state_idx', fields = [ diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo/MongoGeoIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo/MongoGeoIndexCreationSpec.groovy index 5e1a253df15..8f01c7225dd 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo/MongoGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo/MongoGeoIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geo +package io.micronaut.data.mongodb.index.geo import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.Point @@ -9,8 +9,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -26,7 +24,7 @@ class MongoGeoIndexCreationSpec extends Specification implements MongoTestProper @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geo'] + ['io.micronaut.data.mongodb.index.geo'] } Class expectedCollectionsCreatorBeanType() { @@ -60,10 +58,6 @@ class MongoGeoIndexCreationSpec extends Specification implements MongoTestProper } } -@MongoRepository -interface GeoIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('geo_indexed_entities') class GeoIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy index cd6b6463f4b..ac3ff3bdb54 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/MongoGeo2dIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geo2d +package io.micronaut.data.mongodb.index.geo2d import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -9,8 +9,6 @@ import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -26,7 +24,7 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geo2d'] + ['io.micronaut.data.mongodb.index.geo2d'] } Class expectedCollectionsCreatorBeanType() { @@ -59,10 +57,6 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp } } -@MongoRepository -interface Geo2dIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('geo2d_indexed_entities') class Geo2dIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy index 82cb87abf55..c2f65432fa0 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/options/MongoGeo2dOptionsIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geo2d.options +package io.micronaut.data.mongodb.index.geo2d.options import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -9,8 +9,6 @@ import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -26,7 +24,7 @@ class MongoGeo2dOptionsIndexCreationSpec extends Specification implements MongoT @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geo2d.options'] + ['io.micronaut.data.mongodb.index.geo2d.options'] } Class expectedCollectionsCreatorBeanType() { @@ -58,10 +56,6 @@ class MongoGeo2dOptionsIndexCreationSpec extends Specification implements MongoT } } -@MongoRepository -interface Geo2dOptionsIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('geo2d_options_indexed_entities') class Geo2dOptionsIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy similarity index 88% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy index 93276cd3a34..72c1a4b7a0c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/MongoCompoundGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geocompound +package io.micronaut.data.mongodb.index.geocompound import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -11,8 +11,6 @@ import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -28,7 +26,7 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geocompound'] + ['io.micronaut.data.mongodb.index.geocompound'] } Class expectedCollectionsCreatorBeanType() { @@ -62,10 +60,6 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe } } -@MongoRepository -interface GeoCompoundIndexedEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'geo_name_idx', fields = [ diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy similarity index 88% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy index a696c16527d..449275dda9d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geocompound.options +package io.micronaut.data.mongodb.index.geocompound.options import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -11,8 +11,6 @@ import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -28,7 +26,7 @@ class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implement @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geocompound.options'] + ['io.micronaut.data.document.mongodb.geocompound.index.options'] } Class expectedCollectionsCreatorBeanType() { @@ -64,10 +62,6 @@ class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implement } } -@MongoRepository -interface Geo2dCompoundOptionsEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'geo2d_name_idx', fields = [ diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy index c99a1243538..28287010abd 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/MongoGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue +package io.micronaut.data.mongodb.index.geovalue import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.Point @@ -11,8 +11,6 @@ import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.model.DataType -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -28,7 +26,7 @@ class MongoGeoPointValueIndexCreationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue'] + ['io.micronaut.data.mongodb.index.geovalue'] } Class expectedCollectionsCreatorBeanType() { @@ -60,10 +58,6 @@ class MongoGeoPointValueIndexCreationSpec extends Specification implements Mongo } } -@MongoRepository -interface GeoPointValueIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('geo_point_value_indexed_entities') class GeoPointValueIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy similarity index 94% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy index 5c42d2f8f14..261a73e8484 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.geometrycollection +package io.micronaut.data.mongodb.index.geovalue.geometrycollection import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.GeometryCollection @@ -26,7 +26,7 @@ class MongoGeoGeometryCollectionValueIndexCreationSpec extends Specification imp @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.geometrycollection'] + ['io.micronaut.data.mongodb.index.geovalue.geometrycollection'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy index c7fe989963f..39ea3bb9376 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.linestring +package io.micronaut.data.mongodb.index.geovalue.linestring import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.LineString @@ -32,7 +32,7 @@ class MongoGeoLineStringValueIndexCreationSpec extends Specification implements @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.linestring'] + ['io.micronaut.data.mongodb.index.geovalue.linestring'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy index 8b568a919b4..72389763958 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.multilinestring +package io.micronaut.data.mongodb.index.geovalue.multilinestring import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.MultiLineString @@ -32,7 +32,7 @@ class MongoGeoMultiLineStringValueIndexCreationSpec extends Specification implem @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.multilinestring'] + ['io.micronaut.data.mongodb.index.geovalue.multilinestring'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy index e42c2eeef87..f1e492cbcb8 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.multipoint +package io.micronaut.data.mongodb.index.geovalue.multipoint import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.MultiPoint @@ -32,7 +32,7 @@ class MongoGeoMultiPointValueIndexCreationSpec extends Specification implements @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.multipoint'] + ['io.micronaut.data.mongodb.index.geovalue.multipoint'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy index 3309e2e5f57..e124582727e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.multipolygon +package io.micronaut.data.mongodb.index.geovalue.multipolygon import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.MultiPolygon @@ -33,7 +33,7 @@ class MongoGeoMultiPolygonValueIndexCreationSpec extends Specification implement @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.multipolygon'] + ['io.micronaut.data.mongodb.index.geovalue.multipolygon'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy index bf24dc89204..c736c72d315 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.geovalue.polygon +package io.micronaut.data.mongodb.index.geovalue.polygon import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.Polygon @@ -32,7 +32,7 @@ class MongoGeoPolygonValueIndexCreationSpec extends Specification implements Mon @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geovalue.polygon'] + ['io.micronaut.data.mongodb.index.geovalue.polygon'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hashed/MongoHashedIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hashed/MongoHashedIndexCreationSpec.groovy index a12667cb88e..8d68bbc2b23 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hashed/MongoHashedIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hashed/MongoHashedIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.hashed +package io.micronaut.data.mongodb.index.hashed import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -8,8 +8,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoHashedIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoHashedIndexCreationSpec extends Specification implements MongoTestPro @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.hashed'] + ['io.micronaut.data.mongodb.index.hashed'] } Class expectedCollectionsCreatorBeanType() { @@ -58,10 +56,6 @@ class MongoHashedIndexCreationSpec extends Specification implements MongoTestPro } } -@MongoRepository -interface HashedIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('hashed_indexed_entities') class HashedIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hidden/MongoHiddenIndexCreationSpec.groovy similarity index 88% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hidden/MongoHiddenIndexCreationSpec.groovy index 15a1c4ccde4..f7a82dd466a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/hidden/MongoHiddenIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/hidden/MongoHiddenIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.hidden +package io.micronaut.data.mongodb.index.hidden import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -11,8 +11,6 @@ import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -29,7 +27,7 @@ class MongoHiddenIndexCreationSpec extends Specification implements MongoTestPro @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.hidden'] + ['io.micronaut.data.mongodb.index.hidden'] } Class expectedCollectionsCreatorBeanType() { @@ -81,10 +79,6 @@ class MongoHiddenIndexCreationSpec extends Specification implements MongoTestPro } } -@MongoRepository -interface HiddenIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('hidden_indexed_entities') class HiddenIndexedEntity { @Id @@ -95,10 +89,6 @@ class HiddenIndexedEntity { String name } -@MongoRepository -interface HiddenCompoundIndexedEntityRepository extends CrudRepository { -} - @MongoCompoundIndex( name = 'hidden_compound_idx', hidden = true, diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/partialfilter/MongoPartialFilterIndexCreationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/partialfilter/MongoPartialFilterIndexCreationSpec.groovy index e658ff56721..c5a33550edd 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/partialfilter/MongoPartialFilterIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/partialfilter/MongoPartialFilterIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.partialfilter +package io.micronaut.data.mongodb.index.partialfilter import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -25,7 +25,7 @@ class MongoPartialFilterIndexCreationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.partialfilter'] + ['io.micronaut.data.mongodb.index.partialfilter'] } Class expectedCollectionsCreatorBeanType() { @@ -59,10 +59,6 @@ class MongoPartialFilterIndexCreationSpec extends Specification implements Mongo } } -@MongoRepository -interface PartialFilterIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('partial_filter_indexed_entities') class PartialFilterIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/MongoReactiveIndexCreationSpec.groovy similarity index 75% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/MongoReactiveIndexCreationSpec.groovy index 7af93dac281..d2f8d05b41f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/MongoReactiveIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/MongoReactiveIndexCreationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.reactive +package io.micronaut.data.mongodb.index.reactive -import io.micronaut.data.document.mongodb.simple.MongoIndexCreationSpec +import io.micronaut.data.mongodb.index.simple.MongoIndexCreationSpec class MongoReactiveIndexCreationSpec extends MongoIndexCreationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy similarity index 72% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy index d94d050e110..e71eff734c1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/partialfilter/MongoReactivePartialFilterIndexCreationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.reactive.partialfilter +package io.micronaut.data.mongodb.index.reactive.partialfilter -import io.micronaut.data.document.mongodb.partialfilter.MongoPartialFilterIndexCreationSpec +import io.micronaut.data.mongodb.index.partialfilter.MongoPartialFilterIndexCreationSpec class MongoReactivePartialFilterIndexCreationSpec extends MongoPartialFilterIndexCreationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy similarity index 74% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy index 72690b9fabe..581c687ec2a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveAggregatedTextIndexCreationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.reactive.text +package io.micronaut.data.mongodb.index.reactive.text -import io.micronaut.data.document.mongodb.text.MongoAggregatedTextIndexCreationSpec +import io.micronaut.data.mongodb.index.text.MongoAggregatedTextIndexCreationSpec class MongoReactiveAggregatedTextIndexCreationSpec extends MongoAggregatedTextIndexCreationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveTextIndexCreationSpec.groovy similarity index 75% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveTextIndexCreationSpec.groovy index f90f02d1e60..fa4e62c2d58 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/text/MongoReactiveTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/text/MongoReactiveTextIndexCreationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.reactive.text +package io.micronaut.data.mongodb.index.reactive.text -import io.micronaut.data.document.mongodb.text.MongoTextIndexCreationSpec +import io.micronaut.data.mongodb.index.text.MongoTextIndexCreationSpec class MongoReactiveTextIndexCreationSpec extends MongoTextIndexCreationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy similarity index 75% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy index befb1fd6806..5ac455325cf 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/reactive/ttl/MongoReactiveTtlIndexCreationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.reactive.ttl +package io.micronaut.data.mongodb.index.reactive.ttl -import io.micronaut.data.document.mongodb.ttl.MongoTtlIndexCreationSpec +import io.micronaut.data.mongodb.index.ttl.MongoTtlIndexCreationSpec class MongoReactiveTtlIndexCreationSpec extends MongoTtlIndexCreationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/MongoIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/MongoIndexCreationSpec.groovy index 94881c804e4..6f786207d7f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/MongoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/MongoIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.simple +package io.micronaut.data.mongodb.index.simple import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -8,8 +8,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -19,7 +17,7 @@ class MongoIndexCreationSpec extends Specification implements MongoTestPropertyP @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.simple'] + ['io.micronaut.data.mongodb.index.simple'] } Class expectedCollectionsCreatorBeanType() { @@ -58,10 +56,6 @@ class MongoIndexCreationSpec extends Specification implements MongoTestPropertyP } } -@MongoRepository -interface IndexedEntityRepository extends CrudRepository { -} - @MappedEntity('indexed_entities') class IndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy index c81cc1a68c0..36021c1b597 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.simple.embedded +package io.micronaut.data.mongodb.index.simple.embedded import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -9,9 +9,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.annotation.Relation import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -21,7 +19,7 @@ class MongoEmbeddedFieldIndexCreationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.simple.embedded'] + ['io.micronaut.data.document.mongodb.simple.index.embedded'] } Class expectedCollectionsCreatorBeanType() { @@ -55,10 +53,6 @@ class MongoEmbeddedFieldIndexCreationSpec extends Specification implements Mongo } } -@MongoRepository -interface EmbeddedFieldIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('embedded_field_indexed_entities') class EmbeddedFieldIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy similarity index 88% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy index 8faa5cf2124..76a020b10ea 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.text +package io.micronaut.data.mongodb.index.text import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,9 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.text'] + ['io.micronaut.data.mongodb.index.text'] } Class expectedCollectionsCreatorBeanType() { @@ -60,10 +58,6 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong } } -@MongoRepository -interface AggregatedTextIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('aggregated_text_indexed_entities') class AggregatedTextIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoTextIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoTextIndexCreationSpec.groovy index 85935d7bd39..fca6c6a3e13 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/MongoTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoTextIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.text +package io.micronaut.data.mongodb.index.text import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,9 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoTextIndexCreationSpec extends Specification implements MongoTestPrope @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.text'] + ['io.micronaut.data.mongodb.index.text'] } Class expectedCollectionsCreatorBeanType() { @@ -57,10 +55,6 @@ class MongoTextIndexCreationSpec extends Specification implements MongoTestPrope } } -@MongoRepository -interface TextIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('text_indexed_entities') class TextIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy index a7b386d0f81..7d91f9f1421 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/embedded/MongoEmbeddedTextIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.text.embedded +package io.micronaut.data.mongodb.index.text.embedded import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -9,9 +9,7 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.annotation.Relation import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -21,7 +19,7 @@ class MongoEmbeddedTextIndexCreationSpec extends Specification implements MongoT @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.text.embedded'] + ['io.micronaut.data.mongodb.index.text.embedded'] } Class expectedCollectionsCreatorBeanType() { @@ -55,10 +53,6 @@ class MongoEmbeddedTextIndexCreationSpec extends Specification implements MongoT } } -@MongoRepository -interface EmbeddedTextIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('embedded_text_indexed_entities') class EmbeddedTextIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/ttl/MongoTtlIndexCreationSpec.groovy similarity index 87% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/ttl/MongoTtlIndexCreationSpec.groovy index e32b2f5a451..4eaea2c0a3e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/ttl/MongoTtlIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/ttl/MongoTtlIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.ttl +package io.micronaut.data.mongodb.index.ttl import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -8,8 +8,6 @@ import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoTtlIndexCreationSpec extends Specification implements MongoTestProper @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.ttl'] + ['io.micronaut.data.mongodb.index.ttl'] } Class expectedCollectionsCreatorBeanType() { @@ -58,10 +56,6 @@ class MongoTtlIndexCreationSpec extends Specification implements MongoTestProper } } -@MongoRepository -interface TtlIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('ttl_indexed_entities') class TtlIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy similarity index 83% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy index ee47680f517..d56d8a0bfad 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttl/MongoClusteredTtlValidationSpec.groovy @@ -1,4 +1,4 @@ -package isolated.mongodb.validation.clusteredttl +package io.micronaut.data.mongodb.index.validation.clusteredttl import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id @@ -13,7 +13,7 @@ class MongoClusteredTtlValidationSpec extends Specification implements MongoTest @Override List getPackageNames() { - ['isolated.mongodb.validation.clusteredttl'] + ['io.micronaut.data.mongodb.index.validation.clusteredttl'] } void 'fails fast when clustered TTL uses non date id type'() { @@ -29,10 +29,6 @@ class MongoClusteredTtlValidationSpec extends Specification implements MongoTest } } -@MongoRepository -interface InvalidClusteredTtlEntityRepository extends CrudRepository { -} - @MongoClusteredIndex(name = 'invalid_clustered_ttl_idx', expireAfterSeconds = 300) @MappedEntity('invalid_clustered_ttl_entities') class InvalidClusteredTtlEntity { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy similarity index 74% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy index 385af84a1f0..aa8f5bab454 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredunique/MongoClusteredUniqueValidationSpec.groovy @@ -1,19 +1,17 @@ -package isolated.mongodb.validation.clusteredunique +package io.micronaut.data.mongodb.index.validation.clusteredunique import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.Specification class MongoClusteredUniqueValidationSpec extends Specification implements MongoTestPropertyProvider { @Override List getPackageNames() { - ['isolated.mongodb.validation.clusteredunique'] + ['io.micronaut.data.mongodb.index.validation.clusteredunique'] } void 'fails fast when clustered unique is false'() { @@ -29,10 +27,6 @@ class MongoClusteredUniqueValidationSpec extends Specification implements MongoT } } -@MongoRepository -interface InvalidClusteredUniqueEntityRepository extends CrudRepository { -} - @MongoClusteredIndex(name = 'invalid_clustered_unique_idx', unique = false) @MappedEntity('invalid_clustered_unique_entities') class InvalidClusteredUniqueEntity { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy similarity index 74% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy index 3353b739ece..6d632858903 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/collation/MongoCollationValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.collation +package io.micronaut.data.mongodb.index.validation.collation import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -6,15 +6,13 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository import spock.lang.Specification class MongoCollationValidationSpec extends Specification implements MongoTestPropertyProvider { @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.collation'] + ['io.micronaut.data.mongodb.index.validation.collation'] } void 'fails fast for invalid collation JSON'() { @@ -30,10 +28,6 @@ class MongoCollationValidationSpec extends Specification implements MongoTestPro } } -@MongoRepository -interface InvalidCollationIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('invalid_collation_indexed_entities') class InvalidCollationIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy index 4635b695241..ca0fda55127 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/duplicatefield/MongoCompoundIndexDuplicateFieldValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.duplicatefield +package io.micronaut.data.mongodb.index.validation.duplicatefield import io.micronaut.context.ApplicationContext import io.micronaut.data.document.mongodb.MongoTestPropertyProvider @@ -16,7 +16,7 @@ class MongoCompoundIndexDuplicateFieldValidationSpec extends Specification imple @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.duplicatefield'] + ['io.micronaut.data.mongodb.index.validation.duplicatefield'] } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy index 875d3150faf..905fedcd603 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/emptyfields/MongoCompoundIndexEmptyFieldsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.emptyfields +package io.micronaut.data.mongodb.index.validation.emptyfields import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoCompoundIndexEmptyFieldsValidationSpec extends Specification implemen @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.emptyfields'] + ['io.micronaut.data.mongodb.index.validation.emptyfields'] } void 'fails fast for empty compound index field list'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy similarity index 94% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy index 1f82d1ce64a..fe72bd9bfdc 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredcompatibility/MongoExistingClusteredCollectionCompatibilitySpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility +package io.micronaut.data.mongodb.index.validation.existingclusteredcompatibility import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -15,7 +15,7 @@ class MongoExistingClusteredCollectionCompatibilitySpec extends Specification im @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility'] + ['io.micronaut.data.mongodb.index.validation.existingclusteredcompatibility'] } void 'starts successfully when matching clustered collection options already exist'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy similarity index 94% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy index c7a94ed503c..47789411606 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingclusteredconflict/MongoExistingClusteredCollectionConflictSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingclusteredconflict +package io.micronaut.data.mongodb.index.validation.existingclusteredconflict import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -15,7 +15,7 @@ class MongoExistingClusteredCollectionConflictSpec extends Specification impleme @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingclusteredconflict'] + ['io.micronaut.data.mongodb.index.validation.existingclusteredconflict'] } void 'fails fast when existing clustered collection options conflict'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy similarity index 98% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy index 14d4b471027..a7dba5a8db4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoExistingIndexAdvancedConflictSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict +package io.micronaut.data.mongodb.index.validation.existingindexadvancedconflict import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.Point @@ -34,7 +34,7 @@ class MongoExistingIndexAdvancedConflictSpec extends Specification implements Mo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict'] + ['io.micronaut.data.mongodb.index.validation.existingindexadvancedconflict'] } void 'fails fast when existing index has conflicting hidden option'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy index 8e8137a6684..3d137614bf7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingindexcompatibility +package io.micronaut.data.mongodb.index.validation.existingindexcompatibility import com.mongodb.client.MongoClient import com.mongodb.client.model.geojson.Point @@ -21,7 +21,7 @@ class MongoExistingIndexCompatibilitySpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingindexcompatibility'] + ['io.micronaut.data.mongodb.index.validation.existingindexcompatibility'] } void 'starts successfully when matching simple index already exists'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy similarity index 97% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy index ed777f4e819..525bdf2363d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoExistingIndexConflictSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingindexconflict +package io.micronaut.data.mongodb.index.validation.existingindexconflict import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -22,7 +22,7 @@ class MongoExistingIndexConflictSpec extends Specification implements MongoTestP @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingindexconflict'] + ['io.micronaut.data.mongodb.index.validation.existingindexconflict'] } void 'fails fast when existing index has conflicting unique option'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy index f8d154f9e59..9b92d615d43 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexname/MongoExistingIndexNameReconciliationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.existingindexname +package io.micronaut.data.mongodb.index.validation.existingindexname import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -21,7 +21,7 @@ class MongoExistingIndexNameReconciliationSpec extends Specification implements @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.existingindexname'] + ['io.micronaut.data.mongodb.index.validation.existingindexname'] } void 'fails fast when existing index name differs for same key'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompound/MongoCompoundGeoValidationSpec.groovy similarity index 93% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompound/MongoCompoundGeoValidationSpec.groovy index 190e1411002..ef6f45c2066 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompound/MongoCompoundGeoValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompound/MongoCompoundGeoValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geocompound +package io.micronaut.data.mongodb.index.validation.geocompound import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -17,7 +17,7 @@ class MongoCompoundGeoValidationSpec extends Specification implements MongoTestP @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geocompound'] + ['io.micronaut.data.mongodb.index.validation.geocompound'] } void 'fails fast when compound geospatial field also declares numeric direction'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy index f109124cf24..36395fefbff 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geocompoundopts +package io.micronaut.data.mongodb.index.validation.geocompoundopts import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoCompoundGeo2dOptionsValidationSpec extends Specification implements M @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geocompoundopts'] + ['io.micronaut.data.mongodb.index.validation.geocompoundopts'] } void 'fails fast when 2d-specific options are used without geo=true on a compound field'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy index bb8deb91a8d..fe35fa7d9c0 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geocompoundopts +package io.micronaut.data.mongodb.index.validation.geocompoundopts import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoCompoundGeoOptionsValidationSpec extends Specification implements Mon @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geocompoundopts'] + ['io.micronaut.data.mongodb.index.validation.geocompoundopts'] } void 'fails fast when 2d options are used on non-geospatial compound field'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy index 1f9c20133b8..e608fccb1f3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts2d/MongoCompoundGeo2dOptionsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geocompoundopts2d +package io.micronaut.data.mongodb.index.validation.geocompoundopts2d import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoCompoundGeo2dOptionsValidationSpec extends Specification implements M @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geocompoundopts2d'] + ['io.micronaut.data.mongodb.index.validation.geocompoundopts2d'] } void 'fails fast when 2d-specific options are used without geo=true on a compound field'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy index 7e082d17289..62e3110a299 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.georules +package io.micronaut.data.mongodb.index.validation.georules import com.mongodb.client.model.geojson.Point import io.micronaut.context.ApplicationContext @@ -16,7 +16,7 @@ class MongoGeoIndexValidationSpec extends Specification implements MongoTestProp @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.georules'] + ['io.micronaut.data.mongodb.index.validation.georules'] } void 'fails fast when 2d-specific options are used on a 2dsphere index'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy index 3f50454be5a..ae39070df34 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geosphereversion/MongoGeoSphereVersionValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geosphereversion +package io.micronaut.data.mongodb.index.validation.geosphereversion import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -15,7 +15,7 @@ class MongoGeoSphereVersionValidationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geosphereversion'] + ['io.micronaut.data.mongodb.index.validation.geosphereversion'] } void 'fails fast when sphereVersion is used on non-2dsphere geospatial index'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotype/MongoGeoTypeValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotype/MongoGeoTypeValidationSpec.groovy index c54a502f925..9159536ad34 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotype/MongoGeoTypeValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotype/MongoGeoTypeValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geotype +package io.micronaut.data.mongodb.index.validation.geotype import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoGeoTypeValidationSpec extends Specification implements MongoTestPrope @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geotype'] + ['io.micronaut.data.mongodb.index.validation.geotype'] } void 'fails fast when MongoGeoIndexed uses unsupported property type'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotypevalid/MongoGeoTypeValidSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotypevalid/MongoGeoTypeValidSpec.groovy index 79076bfdee0..0df8da0b5d6 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/geotypevalid/MongoGeoTypeValidSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geotypevalid/MongoGeoTypeValidSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.geotypevalid +package io.micronaut.data.mongodb.index.validation.geotypevalid import com.mongodb.client.model.geojson.Point import com.mongodb.client.model.geojson.Position @@ -16,7 +16,7 @@ class MongoGeoTypeValidSpec extends Specification implements MongoTestPropertyPr @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.geotypevalid'] + ['io.micronaut.data.mongodb.index.validation.geotypevalid'] } void 'starts when MongoGeoIndexed uses supported MongoDB GeoJSON type'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy similarity index 96% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy index 5b43857dc26..de8ef3cab27 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoIndexBootstrapWithoutCollectionsSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.indexbootstrap +package io.micronaut.data.mongodb.index.validation.indexbootstrap import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -19,7 +19,7 @@ class MongoIndexBootstrapWithoutCollectionsSpec extends Specification implements @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.indexbootstrap'] + ['io.micronaut.data.mongodb.index.validation.indexbootstrap'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy index c772c616407..5310acd4e74 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexfailurepolicy/MongoIndexFailurePolicyWarnContinueSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.indexfailurepolicy +package io.micronaut.data.mongodb.index.validation.indexfailurepolicy import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoIndexFailurePolicyWarnContinueSpec extends Specification implements M @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.indexfailurepolicy'] + ['io.micronaut.data.mongodb.index.validation.indexfailurepolicy'] } void 'starts when index initialization fails and policy is warn and continue'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy index 195977a8830..82ae3814309 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/invalidpath/MongoCompoundIndexValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.invalidpath +package io.micronaut.data.mongodb.index.validation.invalidpath import io.micronaut.context.ApplicationContext import io.micronaut.data.document.mongodb.MongoTestPropertyProvider @@ -16,7 +16,7 @@ class MongoCompoundIndexValidationSpec extends Specification implements MongoTes @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.invalidpath'] + ['io.micronaut.data.mongodb.index.validation.invalidpath'] } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy similarity index 99% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index 24609a662d4..76714c07858 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.options +package io.micronaut.data.mongodb.index.validation.options import com.mongodb.client.model.geojson.Point import io.micronaut.data.annotation.Embeddable diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy index 118d4a2a4b8..f1287967d5e 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/partialfilter/MongoCompoundIndexPartialFilterValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.partialfilter +package io.micronaut.data.mongodb.index.validation.partialfilter import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoCompoundIndexPartialFilterValidationSpec extends Specification implem @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.partialfilter'] + ['io.micronaut.data.mongodb.index.validation.partialfilter'] } void 'fails fast when sparse and partialFilterExpression are both defined on a compound index'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy similarity index 56% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy index 7db20f0678a..95b8324ce9a 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingclusteredcompatibility +package io.micronaut.data.mongodb.index.validation.reactiveexistingclusteredcompatibility -import io.micronaut.data.document.mongodb.validation.existingclusteredcompatibility.MongoExistingClusteredCollectionCompatibilitySpec +import io.micronaut.data.mongodb.index.validation.existingclusteredcompatibility.MongoExistingClusteredCollectionCompatibilitySpec class MongoReactiveExistingClusteredCollectionCompatibilitySpec extends MongoExistingClusteredCollectionCompatibilitySpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy similarity index 57% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy index 1c506b779c8..84a9d7e3c98 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingclusteredconflict +package io.micronaut.data.mongodb.index.validation.reactiveexistingclusteredconflict -import io.micronaut.data.document.mongodb.validation.existingclusteredconflict.MongoExistingClusteredCollectionConflictSpec +import io.micronaut.data.mongodb.index.validation.existingclusteredconflict.MongoExistingClusteredCollectionConflictSpec class MongoReactiveExistingClusteredCollectionConflictSpec extends MongoExistingClusteredCollectionConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy similarity index 56% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy index 39750feb493..c6efd33ee3f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingindexadvancedconflict +package io.micronaut.data.mongodb.index.validation.reactiveexistingindexadvancedconflict -import io.micronaut.data.document.mongodb.validation.existingindexadvancedconflict.MongoExistingIndexAdvancedConflictSpec +import io.micronaut.data.mongodb.index.validation.existingindexadvancedconflict.MongoExistingIndexAdvancedConflictSpec class MongoReactiveExistingIndexAdvancedConflictSpec extends MongoExistingIndexAdvancedConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy similarity index 57% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy index 786da52f0d0..3b143a59019 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingindexcompatibility +package io.micronaut.data.mongodb.index.validation.reactiveexistingindexcompatibility -import io.micronaut.data.document.mongodb.validation.existingindexcompatibility.MongoExistingIndexCompatibilitySpec +import io.micronaut.data.mongodb.index.validation.existingindexcompatibility.MongoExistingIndexCompatibilitySpec class MongoReactiveExistingIndexCompatibilitySpec extends MongoExistingIndexCompatibilitySpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy similarity index 57% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy index 70c51413e6c..11a38896bc3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingindexconflict +package io.micronaut.data.mongodb.index.validation.reactiveexistingindexconflict -import io.micronaut.data.document.mongodb.validation.existingindexconflict.MongoExistingIndexConflictSpec +import io.micronaut.data.mongodb.index.validation.existingindexconflict.MongoExistingIndexConflictSpec class MongoReactiveExistingIndexConflictSpec extends MongoExistingIndexConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy similarity index 59% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy index de758d71421..ce35a68b98b 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveexistingindexname +package io.micronaut.data.mongodb.index.validation.reactiveexistingindexname -import io.micronaut.data.document.mongodb.validation.existingindexname.MongoExistingIndexNameReconciliationSpec +import io.micronaut.data.mongodb.index.validation.existingindexname.MongoExistingIndexNameReconciliationSpec class MongoReactiveExistingIndexNameReconciliationSpec extends MongoExistingIndexNameReconciliationSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy similarity index 69% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy index c3da0aa344d..ca8b3c0b017 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveindexbootstrap +package io.micronaut.data.mongodb.index.validation.reactiveindexbootstrap -import io.micronaut.data.document.mongodb.validation.indexbootstrap.MongoIndexBootstrapWithoutCollectionsSpec +import io.micronaut.data.mongodb.index.validation.indexbootstrap.MongoIndexBootstrapWithoutCollectionsSpec class MongoReactiveIndexBootstrapWithoutCollectionsSpec extends MongoIndexBootstrapWithoutCollectionsSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy similarity index 59% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy index 98143dcce39..7053a4700fd 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactiveindexfailurepolicy +package io.micronaut.data.mongodb.index.validation.reactiveindexfailurepolicy -import io.micronaut.data.document.mongodb.validation.indexfailurepolicy.MongoIndexFailurePolicyWarnContinueSpec +import io.micronaut.data.mongodb.index.validation.indexfailurepolicy.MongoIndexFailurePolicyWarnContinueSpec class MongoReactiveIndexFailurePolicyWarnContinueSpec extends MongoIndexFailurePolicyWarnContinueSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy index 07fc25cc57f..ac44e935863 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.reactivestorageengine +package io.micronaut.data.mongodb.index.validation.reactivestorageengine import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoReactiveStorageEngineValidationSpec extends Specification implements @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.reactivestorageengine'] + ['io.micronaut.data.mongodb.index.validation.reactivestorageengine'] } void 'fails fast for invalid storageEngine JSON in reactive mode'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy similarity index 58% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy index 8d5ea801601..7b4c48205b9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy @@ -1,6 +1,6 @@ -package io.micronaut.data.document.mongodb.validation.reactivetextversionconflict +package io.micronaut.data.mongodb.index.validation.reactivetextversionconflict -import io.micronaut.data.document.mongodb.validation.textversionconflict.MongoTextIndexVersionConflictSpec +import io.micronaut.data.mongodb.index.validation.textversionconflict.MongoTextIndexVersionConflictSpec class MongoReactiveTextIndexVersionConflictSpec extends MongoTextIndexVersionConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy index 287f833a3fa..189a168c2e4 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.reactivewildcardprojection +package io.micronaut.data.mongodb.index.validation.reactivewildcardprojection import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoReactiveWildcardProjectionValidationSpec extends Specification implem @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.reactivewildcardprojection'] + ['io.micronaut.data.mongodb.index.validation.reactivewildcardprojection'] } void 'fails fast for field-level wildcard projection in reactive mode'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index 3d2cbc56a21..3a1a69335d9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.reactivewildcardtoplevelmultiple +package io.micronaut.data.mongodb.index.validation.reactivewildcardtoplevelmultiple import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec extends Sp @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.reactivewildcardtoplevelmultiple'] + ['io.micronaut.data.mongodb.index.validation.reactivewildcardtoplevelmultiple'] } void 'allows reactive multiple top-level wildcard declarations when wildcardProjection differs'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/storageengine/MongoStorageEngineValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/storageengine/MongoStorageEngineValidationSpec.groovy index 1376525fce2..6be5e4b4270 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/storageengine/MongoStorageEngineValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/storageengine/MongoStorageEngineValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.storageengine +package io.micronaut.data.mongodb.index.validation.storageengine import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoStorageEngineValidationSpec extends Specification implements MongoTes @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.storageengine'] + ['io.micronaut.data.mongodb.index.validation.storageengine'] } void 'fails fast for invalid storageEngine JSON'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy index 264a3e2f57d..1171aa24ba8 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/text/MongoTextIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.text +package io.micronaut.data.mongodb.index.validation.text import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoTextIndexValidationSpec extends Specification implements MongoTestPro @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.text'] + ['io.micronaut.data.mongodb.index.validation.text'] } void 'fails fast for invalid text weight'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy similarity index 94% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy index 332832bcf59..bd2e1a219f2 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoTextIndexVersionConflictSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.textversionconflict +package io.micronaut.data.mongodb.index.validation.textversionconflict import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -16,7 +16,7 @@ class MongoTextIndexVersionConflictSpec extends Specification implements MongoTe @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.textversionconflict'] + ['io.micronaut.data.mongodb.index.validation.textversionconflict'] } void 'fails fast when existing text index has conflicting textIndexVersion'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy similarity index 92% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy index 14df61927a1..d5dfa69f114 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/ttlcompound/MongoCompoundIndexTtlValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.ttlcompound +package io.micronaut.data.mongodb.index.validation.ttlcompound import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -16,7 +16,7 @@ class MongoCompoundIndexTtlValidationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.ttlcompound'] + ['io.micronaut.data.mongodb.index.validation.ttlcompound'] } void 'fails fast for TTL on compound index'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy index bd30cbbe29b..09594a90137 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardprojection/MongoWildcardProjectionValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.wildcardprojection +package io.micronaut.data.mongodb.index.validation.wildcardprojection import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoWildcardProjectionValidationSpec extends Specification implements Mon @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.wildcardprojection'] + ['io.micronaut.data.mongodb.index.validation.wildcardprojection'] } void 'fails fast for field-level wildcard projection'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy similarity index 91% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy index 56ff2590c8b..4c9ccf755e5 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/wildcardtoplevelmultiple/MongoTopLevelWildcardMultipleDeclarationsValidationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.validation.wildcardtoplevelmultiple +package io.micronaut.data.mongodb.index.validation.wildcardtoplevelmultiple import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue @@ -14,7 +14,7 @@ class MongoTopLevelWildcardMultipleDeclarationsValidationSpec extends Specificat @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.validation.wildcardtoplevelmultiple'] + ['io.micronaut.data.mongodb.index.validation.wildcardtoplevelmultiple'] } void 'allows multiple top-level wildcard declarations when wildcardProjection differs'() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/MongoWildcardIndexCreationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/MongoWildcardIndexCreationSpec.groovy index 6b925a30549..4217a047faf 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/MongoWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/MongoWildcardIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.wildcard +package io.micronaut.data.mongodb.index.wildcard import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -25,7 +25,7 @@ class MongoWildcardIndexCreationSpec extends Specification implements MongoTestP @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.wildcard'] + ['io.micronaut.data.mongodb.index.wildcard'] } Class expectedCollectionsCreatorBeanType() { @@ -57,10 +57,6 @@ class MongoWildcardIndexCreationSpec extends Specification implements MongoTestP } } -@MongoRepository -interface WildcardIndexedEntityRepository extends CrudRepository { -} - @MappedEntity('wildcard_indexed_entities') class WildcardIndexedEntity { @Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy similarity index 85% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy index 9160861053e..edc670a73bb 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.wildcard.toplevel +package io.micronaut.data.mongodb.index.wildcard.toplevel import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,9 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoTopLevelWildcardIndexCreationSpec extends Specification implements Mo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.wildcard.toplevel'] + ['io.micronaut.data.mongodb.index.wildcard.toplevel'] } Class expectedCollectionsCreatorBeanType() { @@ -57,10 +55,6 @@ class MongoTopLevelWildcardIndexCreationSpec extends Specification implements Mo } } -@MongoRepository -interface TopLevelWildcardIndexedEntityRepository extends CrudRepository { -} - @MongoWildcardIndex(name = 'top_level_wildcard_idx') @MappedEntity('top_level_wildcard_indexed_entities') class TopLevelWildcardIndexedEntity { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy similarity index 86% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy index da212af92df..eb625be07b2 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/MongoTopLevelWildcardProjectionIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.wildcard.toplevel +package io.micronaut.data.mongodb.index.wildcard.toplevel import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,9 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex -import io.micronaut.data.repository.CrudRepository import org.bson.Document import spock.lang.AutoCleanup import spock.lang.Shared @@ -26,7 +24,7 @@ class MongoTopLevelWildcardProjectionIndexCreationSpec extends Specification imp @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.wildcard.toplevel'] + ['io.micronaut.data.mongodb.index.wildcard.toplevel'] } Class expectedCollectionsCreatorBeanType() { @@ -60,10 +58,6 @@ class MongoTopLevelWildcardProjectionIndexCreationSpec extends Specification imp } } -@MongoRepository -interface TopLevelWildcardProjectionIndexedEntityRepository extends CrudRepository { -} - @MongoWildcardIndex(name = 'top_level_wildcard_projection_idx', wildcardProjection = '{ "metadata.secret": 0 }') @MappedEntity('top_level_wildcard_projection_indexed_entities') class TopLevelWildcardProjectionIndexedEntity { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy similarity index 90% rename from data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy index 76de17cd6c7..d116e0ac5c5 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/wildcard/toplevel/multiple/MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.document.mongodb.wildcard.toplevel.multiple +package io.micronaut.data.mongodb.index.wildcard.toplevel.multiple import com.mongodb.client.MongoClient import io.micronaut.context.ApplicationContext @@ -7,9 +7,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex -import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -25,7 +23,7 @@ class MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends Specifi @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.wildcard.toplevel.multiple'] + ['io.micronaut.data.mongodb.index.wildcard.toplevel.multiple'] } Class expectedCollectionsCreatorBeanType() { @@ -78,10 +76,6 @@ class MongoTopLevelWildcardMultipleDeclarationsIndexCreationSpec extends Specifi } } -@MongoRepository -interface TopLevelWildcardMultipleIndexedEntityRepository extends CrudRepository { -} - @MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') @MongoWildcardIndex(name = 'top_level_wildcard_multiple_idx') @MongoWildcardIndex(name = 'top_level_wildcard_projection_secret_idx', wildcardProjection = '{ "metadata.secret": 0 }') diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index 6a0cda09969..2f64ad48033 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -12,8 +12,6 @@ NOTE: Index creation is opt-in and runs during startup collection initialization When `micronaut.data.mongodb.create-indexes=true` and `micronaut.data.mongodb.create-collections=false`, Micronaut Data still performs index reconciliation/creation for collections that already exist. If a mapped collection does not exist and collection creation is disabled, index initialization for that entity is skipped. -NOTE: Query-side usage for MongoDB text/geospatial operators is documented in xref:mongo/mongoCriteriaSpecifications/mongoCriteriaExecuteQuery.adoc[Mongo criteria execute query]. - == Supported index annotations Micronaut Data currently supports the following MongoDB index annotations: @@ -138,28 +136,11 @@ For `2dsphere` indexes, ann:data.mongodb.annotation.index.MongoGeoIndexed[] also NOTE: `sphereVersion` is only valid for `2dsphere` indexes; declaring it for other geospatial kinds is rejected during startup validation (fail-fast). -Modeled geospatial values are supported for `MongoGeoPoint`, `MongoGeoMultiPoint`, `MongoGeoLineString`, `MongoGeoMultiLineString`, `MongoGeoPolygon`, `MongoGeoMultiPolygon`, `MongoGeoGeometryCollection`, and custom point-like modeled types. - -For properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[], Micronaut Data now applies `MongoGeoPointConverter` implicitly when no explicit `@MappedProperty(converter=...)` is declared. -Point-like modeled types can map coordinates through numeric properties such as `x/y`, `longitude/latitude`, or `lng|lon/lat`. - -For polygon modeled values, Micronaut Data applies `MongoGeoPolygonConverter` implicitly for `MongoGeoPolygon` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -For line-string modeled values, Micronaut Data applies `MongoGeoLineStringConverter` implicitly for `MongoGeoLineString` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -For multi-line-string modeled values, Micronaut Data applies `MongoGeoMultiLineStringConverter` implicitly for `MongoGeoMultiLineString` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -For multi-point modeled values, Micronaut Data applies `MongoGeoMultiPointConverter` implicitly for `MongoGeoMultiPoint` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -For multi-polygon modeled values, Micronaut Data applies `MongoGeoMultiPolygonConverter` implicitly for `MongoGeoMultiPolygon` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -For geometry-collection modeled values, Micronaut Data applies `MongoGeoGeometryCollectionConverter` implicitly for `MongoGeoGeometryCollection` properties annotated with ann:data.mongodb.annotation.index.MongoGeoIndexed[]. - -With `MongoGeoGeometryCollection` in place, modeled support now covers all standard GeoJSON geometry kinds (`Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`). +For `2dsphere` indexes, the supported implicit property model is MongoDB Java driver GeoJSON types such as `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, and `GeometryCollection`. -For raw Mongo query annotations (for example ann:data.mongodb.annotation.MongoFindQuery[]), supported modeled geospatial parameter types are also converted implicitly to GeoJSON persisted shapes (`type` + `coordinates`) when no explicit converter is specified. +For `2d` indexes, Micronaut Data continues to allow legacy map-backed coordinate values. -NOTE: ann:data.mongodb.annotation.index.MongoGeoIndexed[] now validates property type compatibility during startup and fails fast for unsupported non-geospatial property types. +NOTE: ann:data.mongodb.annotation.index.MongoGeoIndexed[] validates property type compatibility during startup and fails fast for unsupported property types. == Wildcard indexes From d678d01bfa9746e040c1e24094193127189858b6 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Wed, 1 Apr 2026 22:20:38 +0200 Subject: [PATCH 22/34] Cleanup tests, remove some non needed extra tests. --- .../model/jpa/criteria/PersistentEntityCriteriaBuilder.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 753efd0f11f..10f9144e61f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -53,7 +53,6 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @return The insert criteria * @since 5.0 */ - PersistentEntityCriteriaInsert createCriteriaInsert(Class targetEntity); /** @@ -64,7 +63,6 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @param ignoreCase If ignore case should be used * @return ascending ordering corresponding to the expression */ - Order sort(Expression x, boolean ascending, boolean ignoreCase); /** @@ -226,5 +224,4 @@ default Predicate ilike(Expression x, String pattern) { * @since 3.9.0 */ Predicate arrayContains(Expression x, Expression y); - } From 752ae36d07f1c26787bf410b140b27983991de15 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 09:29:16 +0200 Subject: [PATCH 23/34] Undo formatting --- .../model/jpa/criteria/PersistentEntityCriteriaBuilder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java index 10f9144e61f..4f29ca3e7f4 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCriteriaBuilder.java @@ -53,6 +53,7 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @return The insert criteria * @since 5.0 */ + PersistentEntityCriteriaInsert createCriteriaInsert(Class targetEntity); /** @@ -63,6 +64,7 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { * @param ignoreCase If ignore case should be used * @return ascending ordering corresponding to the expression */ + Order sort(Expression x, boolean ascending, boolean ignoreCase); /** From 6755710a85b0fade6a692e7942bbb4afe3b8cf16 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 09:54:54 +0200 Subject: [PATCH 24/34] More index validation and some docs --- .../annotation/index/MongoGeoIndexed.java | 14 ++++++---- .../mongodb/common/MongoEntityIndexes.java | 3 +++ .../MongoGeoIndexValidationSpec.groovy | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java index af86266b30d..cc7a9e18780 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java @@ -40,7 +40,7 @@ String name() default ""; /** - * @return The geospatial index kind. + * @return The geospatial index kind. Defaults to {@link MongoGeoIndexType#GEO_2DSPHERE}. */ MongoGeoIndexType type() default MongoGeoIndexType.GEO_2DSPHERE; @@ -60,22 +60,26 @@ String storageEngine() default ""; /** - * @return The 2dsphere index version, or -1 if unset. + * @return The 2dsphere index version, or {@code -1} if unset. Only valid for + * {@link MongoGeoIndexType#GEO_2DSPHERE}. */ int sphereVersion() default -1; /** - * @return The 2d index bits setting, or -1 if unset. + * @return The 2d index bits setting, or {@code -1} if unset. Only valid for + * {@link MongoGeoIndexType#GEO_2D}. Valid range is 1..32 inclusive. MongoDB default is 26. */ int bits() default -1; /** - * @return The 2d index minimum value, or NaN if unset. + * @return The 2d index minimum value, or {@link Double#NaN} if unset. Only valid for + * {@link MongoGeoIndexType#GEO_2D}. Represents the lower inclusive boundary. MongoDB default is -180.0. */ double min() default Double.NaN; /** - * @return The 2d index maximum value, or NaN if unset. + * @return The 2d index maximum value, or {@link Double#NaN} if unset. Only valid for + * {@link MongoGeoIndexType#GEO_2D}. Represents the upper inclusive boundary. MongoDB default is 180.0. */ double max() default Double.NaN; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 1e8d49cb8e1..f29c42e2359 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -230,6 +230,9 @@ private static List resolveFieldIndexes(RuntimePersistentEntity 32)) { + throw new IllegalStateException("Mongo 2d geospatial option 'bits' on entity [" + entity.getName() + "] must be between 1 and 32 inclusive"); + } if (type != MongoGeoIndexType.GEO_2DSPHERE && sphereVersion != null) { throw new IllegalStateException("2dsphere-specific geospatial options are only supported for Mongo 2dsphere indexes on entity [" + entity.getName() + "]"); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy index 62e3110a299..57d6b69ea52 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -30,6 +30,19 @@ class MongoGeoIndexValidationSpec extends Specification implements MongoTestProp def e = thrown(RuntimeException) e.message.contains('2d-specific geospatial options are only supported for Mongo 2d indexes') } + + void 'fails fast when 2d bits is outside supported Mongo range'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains("Mongo 2d geospatial option 'bits'") + e.message.contains('must be between 1 and 32 inclusive') + } } @MongoRepository @@ -45,3 +58,17 @@ class InvalidGeoIndexedEntity { @MongoGeoIndexed(name = 'invalid_geo_idx', type = MongoGeoIndexType.GEO_2DSPHERE, bits = 26) Point location } + +@MongoRepository +interface InvalidGeoBitsEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_geo_bits_entities') +class InvalidGeoBitsEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'invalid_geo_bits_idx', type = MongoGeoIndexType.GEO_2D, bits = 33) + Map location +} From 9d5774ad2a04e6806f8141c3175b52bd4a4b3abe Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 10:08:47 +0200 Subject: [PATCH 25/34] Fix top level wildcard indexes resolving --- .../mongodb/common/MongoEntityIndexes.java | 55 +++++++------------ .../geobits/MongoGeoBitsValidationSpec.groovy | 47 ++++++++++++++++ .../MongoGeoIndexValidationSpec.groovy | 26 --------- 3 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geobits/MongoGeoBitsValidationSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index f29c42e2359..9e8aef15710 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -125,6 +125,25 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist String mergedName = first.name(); for (int i = 1; i < indexes.size(); i++) { ResolvedIndex candidate = indexes.get(i); + if (!Objects.equals(first.fields(), candidate.fields()) + || first.unique() != candidate.unique() + || first.sparse() != candidate.sparse() + || first.hidden() != candidate.hidden() + || !Objects.equals(first.expireAfterSeconds(), candidate.expireAfterSeconds()) + || !Objects.equals(first.partialFilterExpression(), candidate.partialFilterExpression()) + || !Objects.equals(first.collation(), candidate.collation()) + || !Objects.equals(first.bits(), candidate.bits()) + || !Objects.equals(first.min(), candidate.min()) + || !Objects.equals(first.max(), candidate.max()) + || !Objects.equals(first.defaultLanguage(), candidate.defaultLanguage()) + || !Objects.equals(first.languageOverride(), candidate.languageOverride()) + || !Objects.equals(first.textIndexVersion(), candidate.textIndexVersion()) + || !Objects.equals(first.sphereVersion(), candidate.sphereVersion()) + || !Objects.equals(first.storageEngine(), candidate.storageEngine()) + || !Objects.equals(first.comment(), candidate.comment()) + || !Objects.equals(first.commitQuorum(), candidate.commitQuorum())) { + throw new IllegalStateException("Mongo top-level wildcard indexes on entity [" + entity.getName() + "] declare conflicting options for wildcard signature [$**]"); + } if (mergedName == null) { mergedName = candidate.name(); } else if (candidate.name() != null && !mergedName.equals(candidate.name())) { @@ -543,43 +562,11 @@ private static final class TextIndexState { } private record WildcardIndexSignature(List fields, - boolean unique, - boolean sparse, - boolean hidden, - @Nullable Integer expireAfterSeconds, - @Nullable String partialFilterExpression, - @Nullable String collation, - @Nullable Integer bits, - @Nullable Double min, - @Nullable Double max, - @Nullable String defaultLanguage, - @Nullable String languageOverride, - @Nullable Integer textIndexVersion, - @Nullable Integer sphereVersion, - @Nullable String wildcardProjection, - @Nullable String storageEngine, - @Nullable String comment, - @Nullable String commitQuorum) { + @Nullable String wildcardProjection) { private WildcardIndexSignature(ResolvedIndex index) { this(index.fields(), - index.unique(), - index.sparse(), - index.hidden(), - index.expireAfterSeconds(), - index.partialFilterExpression(), - index.collation(), - index.bits(), - index.min(), - index.max(), - index.defaultLanguage(), - index.languageOverride(), - index.textIndexVersion(), - index.sphereVersion(), - index.wildcardProjection(), - index.storageEngine(), - index.comment(), - index.commitQuorum()); + index.wildcardProjection()); } } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geobits/MongoGeoBitsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geobits/MongoGeoBitsValidationSpec.groovy new file mode 100644 index 00000000000..65d82c3530f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geobits/MongoGeoBitsValidationSpec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.data.mongodb.index.validation.geobits + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoGeoBitsValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.validation.geobits'] + } + + void 'fails fast when 2d bits is outside supported Mongo range'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains("Mongo 2d geospatial option 'bits'") + e.message.contains('must be between 1 and 32 inclusive') + } +} + +@MongoRepository +interface InvalidGeoBitsEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_geo_bits_entities') +class InvalidGeoBitsEntity { + @Id + @GeneratedValue + String id + + @MongoGeoIndexed(name = 'invalid_geo_bits_idx', type = MongoGeoIndexType.GEO_2D, bits = 33) + Map location +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy index 57d6b69ea52..48550aa01d9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georules/MongoGeoIndexValidationSpec.groovy @@ -31,18 +31,6 @@ class MongoGeoIndexValidationSpec extends Specification implements MongoTestProp e.message.contains('2d-specific geospatial options are only supported for Mongo 2d indexes') } - void 'fails fast when 2d bits is outside supported Mongo range'() { - when: - ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - then: - def e = thrown(RuntimeException) - e.message.contains("Mongo 2d geospatial option 'bits'") - e.message.contains('must be between 1 and 32 inclusive') - } } @MongoRepository @@ -58,17 +46,3 @@ class InvalidGeoIndexedEntity { @MongoGeoIndexed(name = 'invalid_geo_idx', type = MongoGeoIndexType.GEO_2DSPHERE, bits = 26) Point location } - -@MongoRepository -interface InvalidGeoBitsEntityRepository extends CrudRepository { -} - -@MappedEntity('invalid_geo_bits_entities') -class InvalidGeoBitsEntity { - @Id - @GeneratedValue - String id - - @MongoGeoIndexed(name = 'invalid_geo_bits_idx', type = MongoGeoIndexType.GEO_2D, bits = 33) - Map location -} From e518d50c408e8071ea53fd8603f857417f4bd004 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 10:22:56 +0200 Subject: [PATCH 26/34] Cleanup --- .../mongodb/common/MongoEntityIndexes.java | 154 ++++++++++-------- 1 file changed, 90 insertions(+), 64 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 9e8aef15710..4a1577535ea 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -58,6 +58,32 @@ @Internal public final class MongoEntityIndexes { + private static final String ATTR_BITS = "bits"; + private static final String ATTR_COLLATION = "collation"; + private static final String ATTR_COMMENT = "comment"; + private static final String ATTR_COMMIT_QUORUM = "commitQuorum"; + private static final String ATTR_DEFAULT_LANGUAGE = "defaultLanguage"; + private static final String ATTR_DIRECTION = "direction"; + private static final String ATTR_EXPIRE_AFTER_SECONDS = "expireAfterSeconds"; + private static final String ATTR_FIELDS = "fields"; + private static final String ATTR_GEO = "geo"; + private static final String ATTR_GEO_TYPE = "geoType"; + private static final String ATTR_HIDDEN = "hidden"; + private static final String ATTR_LANGUAGE_OVERRIDE = "languageOverride"; + private static final String ATTR_MAX = "max"; + private static final String ATTR_MIN = "min"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_PARTIAL_FILTER_EXPRESSION = "partialFilterExpression"; + private static final String ATTR_SPARSE = "sparse"; + private static final String ATTR_SPHERE_VERSION = "sphereVersion"; + private static final String ATTR_STORAGE_ENGINE = "storageEngine"; + private static final String ATTR_TEXT_INDEX_VERSION = "textIndexVersion"; + private static final String ATTR_TYPE = "type"; + private static final String ATTR_UNIQUE = "unique"; + private static final String ATTR_VALUE = "value"; + private static final String ATTR_WEIGHT = "weight"; + private static final String ATTR_WILDCARD_PROJECTION = "wildcardProjection"; + private final List indexes; private MongoEntityIndexes(List indexes) { @@ -94,11 +120,11 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist Map> groupedIndexes = new LinkedHashMap<>(); for (var annotation : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoWildcardIndex.class)) { ResolvedIndex index = new ResolvedIndex( - annotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField("$**", 1, null, null, null, null)), false, false, - annotation.booleanValue("hidden").orElse(false), + annotation.booleanValue(ATTR_HIDDEN).orElse(false), null, null, null, @@ -109,10 +135,10 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist null, null, null, - annotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null), - parseJsonOption(annotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), - annotation.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) + annotation.stringValue(ATTR_WILDCARD_PROJECTION).filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(annotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + annotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) ); groupedIndexes.computeIfAbsent(new WildcardIndexSignature(index), ignored -> new ArrayList<>()).add(index); } @@ -187,14 +213,14 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), - List.of(new ResolvedIndexField(persistedPath, annotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC) == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)), - annotation.booleanValue("unique").orElse(false), - annotation.booleanValue("sparse").orElse(false), - annotation.booleanValue("hidden").orElse(false), - annotation.intValue("expireAfterSeconds").isPresent() && annotation.intValue("expireAfterSeconds").getAsInt() >= 0 ? annotation.intValue("expireAfterSeconds").getAsInt() : null, - annotation.stringValue("partialFilterExpression").filter(s -> !s.isEmpty()).orElse(null), - annotation.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), + List.of(new ResolvedIndexField(persistedPath, annotation.enumValue(ATTR_DIRECTION, MongoIndexDirection.class).orElse(MongoIndexDirection.ASC) == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)), + annotation.booleanValue(ATTR_UNIQUE).orElse(false), + annotation.booleanValue(ATTR_SPARSE).orElse(false), + annotation.booleanValue(ATTR_HIDDEN).orElse(false), + annotation.intValue(ATTR_EXPIRE_AFTER_SECONDS).isPresent() && annotation.intValue(ATTR_EXPIRE_AFTER_SECONDS).getAsInt() >= 0 ? annotation.intValue(ATTR_EXPIRE_AFTER_SECONDS).getAsInt() : null, + annotation.stringValue(ATTR_PARTIAL_FILTER_EXPRESSION).filter(s -> !s.isEmpty()).orElse(null), + annotation.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), null, null, null, @@ -203,8 +229,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - annotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(annotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + annotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), null )); return; @@ -216,11 +242,11 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), + hashedAnnotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(persistedPath, null, null, "hashed", null, null)), false, false, - hashedAnnotation.booleanValue("hidden").orElse(false), + hashedAnnotation.booleanValue(ATTR_HIDDEN).orElse(false), null, null, null, @@ -232,8 +258,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - hashedAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(hashedAnnotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + hashedAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), null )); return; @@ -241,11 +267,11 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? geoAnnotation.intValue("sphereVersion").getAsInt() : null; - Integer bits = geoAnnotation.intValue("bits").isPresent() && geoAnnotation.intValue("bits").getAsInt() >= 0 ? geoAnnotation.intValue("bits").getAsInt() : null; - Double min = geoAnnotation.doubleValue("min").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("min").getAsDouble()) ? geoAnnotation.doubleValue("min").getAsDouble() : null; - Double max = geoAnnotation.doubleValue("max").isPresent() && !Double.isNaN(geoAnnotation.doubleValue("max").getAsDouble()) ? geoAnnotation.doubleValue("max").getAsDouble() : null; + MongoGeoIndexType type = geoAnnotation.enumValue(ATTR_TYPE, MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); + Integer sphereVersion = geoAnnotation.intValue(ATTR_SPHERE_VERSION).isPresent() && geoAnnotation.intValue(ATTR_SPHERE_VERSION).getAsInt() >= 0 ? geoAnnotation.intValue(ATTR_SPHERE_VERSION).getAsInt() : null; + Integer bits = geoAnnotation.intValue(ATTR_BITS).isPresent() && geoAnnotation.intValue(ATTR_BITS).getAsInt() >= 0 ? geoAnnotation.intValue(ATTR_BITS).getAsInt() : null; + Double min = geoAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(geoAnnotation.doubleValue(ATTR_MIN).getAsDouble()) ? geoAnnotation.doubleValue(ATTR_MIN).getAsDouble() : null; + Double max = geoAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(geoAnnotation.doubleValue(ATTR_MAX).getAsDouble()) ? geoAnnotation.doubleValue(ATTR_MAX).getAsDouble() : null; if (type != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d indexes on entity [" + entity.getName() + "]"); } @@ -256,11 +282,11 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), + geoAnnotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(persistedPath, null, null, type.getKey(), min, max)), false, false, - geoAnnotation.booleanValue("hidden").orElse(false), + geoAnnotation.booleanValue(ATTR_HIDDEN).orElse(false), null, null, null, @@ -272,24 +298,24 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - geoAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(geoAnnotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + geoAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), null )); return; } var wildcardAnnotation = annotationMetadata.getAnnotation(MongoWildcardIndexed.class); if (wildcardAnnotation != null) { - String wildcardProjection = wildcardAnnotation.stringValue("wildcardProjection").filter(s -> !s.isEmpty()).orElse(null); + String wildcardProjection = wildcardAnnotation.stringValue(ATTR_WILDCARD_PROJECTION).filter(s -> !s.isEmpty()).orElse(null); if (wildcardProjection != null) { throw new IllegalStateException("Mongo wildcardProjection on field-level @MongoWildcardIndexed is not supported by MongoDB. Use @MongoWildcardIndex on the entity instead for top-level wildcard projection."); } indexes.add(new ResolvedIndex( - wildcardAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + wildcardAnnotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(persistedPath + ".$**", 1, null, null, null, null)), false, false, - wildcardAnnotation.booleanValue("hidden").orElse(false), + wildcardAnnotation.booleanValue(ATTR_HIDDEN).orElse(false), null, null, null, @@ -301,8 +327,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - wildcardAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(wildcardAnnotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + wildcardAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), null )); } @@ -347,48 +373,48 @@ private static List resolveTextIndexes(RuntimePersistentEntity if (textAnnotation == null) { return; } - int weight = textAnnotation.intValue("weight").orElse(1); + int weight = textAnnotation.intValue(ATTR_WEIGHT).orElse(1); if (weight <= 0) { throw new IllegalStateException("Mongo text index weight must be greater than zero for entity [" + entity.getName() + "]"); } - String declaredName = textAnnotation.stringValue("name").filter(s -> !s.isEmpty()).orElse(null); + String declaredName = textAnnotation.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null); if (state.name == null) { state.name = declaredName; } else if (declaredName != null && !declaredName.equals(state.name)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same index name"); } - boolean declaredHidden = textAnnotation.booleanValue("hidden").orElse(false); + boolean declaredHidden = textAnnotation.booleanValue(ATTR_HIDDEN).orElse(false); if (state.hidden == null) { state.hidden = declaredHidden; } else if (!state.hidden.equals(declaredHidden)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same hidden option"); } - String declaredComment = textAnnotation.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null); + String declaredComment = textAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null); if (state.comment == null) { state.comment = declaredComment; } else if (!Objects.equals(state.comment, declaredComment)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same comment option"); } - String declaredStorageEngine = parseJsonOption(textAnnotation.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()); + String declaredStorageEngine = parseJsonOption(textAnnotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()); if (state.storageEngine == null) { state.storageEngine = declaredStorageEngine; } else if (!Objects.equals(state.storageEngine, declaredStorageEngine)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same storageEngine option"); } - String declaredDefaultLanguage = textAnnotation.stringValue("defaultLanguage").filter(s -> !s.isEmpty()).orElse(null); + String declaredDefaultLanguage = textAnnotation.stringValue(ATTR_DEFAULT_LANGUAGE).filter(s -> !s.isEmpty()).orElse(null); if (state.defaultLanguage == null) { state.defaultLanguage = declaredDefaultLanguage; } else if (!Objects.equals(state.defaultLanguage, declaredDefaultLanguage)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same defaultLanguage option"); } - String declaredLanguageOverride = textAnnotation.stringValue("languageOverride").filter(s -> !s.isEmpty()).orElse(null); + String declaredLanguageOverride = textAnnotation.stringValue(ATTR_LANGUAGE_OVERRIDE).filter(s -> !s.isEmpty()).orElse(null); if (state.languageOverride == null) { state.languageOverride = declaredLanguageOverride; } else if (!Objects.equals(state.languageOverride, declaredLanguageOverride)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same languageOverride option"); } - Integer declaredTextIndexVersion = textAnnotation.intValue("textIndexVersion").isPresent() && textAnnotation.intValue("textIndexVersion").getAsInt() >= 0 - ? textAnnotation.intValue("textIndexVersion").getAsInt() : null; + Integer declaredTextIndexVersion = textAnnotation.intValue(ATTR_TEXT_INDEX_VERSION).isPresent() && textAnnotation.intValue(ATTR_TEXT_INDEX_VERSION).getAsInt() >= 0 + ? textAnnotation.intValue(ATTR_TEXT_INDEX_VERSION).getAsInt() : null; if (declaredTextIndexVersion != null && declaredTextIndexVersion <= 0) { throw new IllegalStateException("Mongo text index version must be greater than zero for entity [" + entity.getName() + "]"); } @@ -413,8 +439,8 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit Integer indexBits = null; Double indexMin = null; Double indexMax = null; - for (var fieldAnnotation : annotationValue.getAnnotations("fields", MongoCompoundIndexField.class)) { - String path = fieldAnnotation.stringValue().orElseThrow(); + for (var fieldAnnotation : annotationValue.getAnnotations(ATTR_FIELDS, MongoCompoundIndexField.class)) { + String path = fieldAnnotation.stringValue(ATTR_VALUE).orElseThrow(); String pathForLookup = path.contains(".") ? path.replace('.', '_') : path; String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, pathForLookup) .map(persistentPath -> { @@ -428,16 +454,16 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (!seenPaths.add(persistedPath)) { throw new IllegalStateException("Duplicate Mongo index path [" + persistedPath + "] for entity [" + entity.getName() + "]"); } - boolean geo = fieldAnnotation.booleanValue("geo").orElse(false); - MongoIndexDirection direction = fieldAnnotation.enumValue("direction", MongoIndexDirection.class).orElse(MongoIndexDirection.ASC); + boolean geo = fieldAnnotation.booleanValue(ATTR_GEO).orElse(false); + MongoIndexDirection direction = fieldAnnotation.enumValue(ATTR_DIRECTION, MongoIndexDirection.class).orElse(MongoIndexDirection.ASC); if (geo) { if (direction != MongoIndexDirection.ASC) { throw new IllegalStateException("Mongo compound geospatial field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot define a numeric direction"); } - MongoGeoIndexType geoType = fieldAnnotation.enumValue("geoType", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); - Integer bits = fieldAnnotation.intValue("bits").isPresent() && fieldAnnotation.intValue("bits").getAsInt() >= 0 ? fieldAnnotation.intValue("bits").getAsInt() : null; - Double min = fieldAnnotation.doubleValue("min").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("min").getAsDouble()) ? fieldAnnotation.doubleValue("min").getAsDouble() : null; - Double max = fieldAnnotation.doubleValue("max").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("max").getAsDouble()) ? fieldAnnotation.doubleValue("max").getAsDouble() : null; + MongoGeoIndexType geoType = fieldAnnotation.enumValue(ATTR_GEO_TYPE, MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); + Integer bits = fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0 ? fieldAnnotation.intValue(ATTR_BITS).getAsInt() : null; + Double min = fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble()) ? fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble() : null; + Double max = fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()) ? fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble() : null; if (geoType != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d compound geospatial fields on entity [" + entity.getName() + "]"); } @@ -461,9 +487,9 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit } fields.add(new ResolvedIndexField(persistedPath, null, null, geoType.getKey(), min, max)); } else { - if ((fieldAnnotation.intValue("bits").isPresent() && fieldAnnotation.intValue("bits").getAsInt() >= 0) - || (fieldAnnotation.doubleValue("min").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("min").getAsDouble())) - || (fieldAnnotation.doubleValue("max").isPresent() && !Double.isNaN(fieldAnnotation.doubleValue("max").getAsDouble()))) { + if ((fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0) + || (fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble())) + || (fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()))) { throw new IllegalStateException("2d-specific geospatial options require geo=true for Mongo compound index field [" + persistedPath + "] on entity [" + entity.getName() + "]"); } fields.add(new ResolvedIndexField(persistedPath, direction == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)); @@ -472,23 +498,23 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (fields.isEmpty()) { throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] must declare at least one field"); } - if (annotationValue.intValue("expireAfterSeconds").isPresent() && annotationValue.intValue("expireAfterSeconds").getAsInt() >= 0) { + if (annotationValue.intValue(ATTR_EXPIRE_AFTER_SECONDS).isPresent() && annotationValue.intValue(ATTR_EXPIRE_AFTER_SECONDS).getAsInt() >= 0) { throw new IllegalStateException("TTL is not supported for Mongo compound index on entity [" + entity.getName() + "]"); } - String partialFilterExpression = annotationValue.stringValue("partialFilterExpression").filter(s -> !s.isEmpty()).orElse(null); - boolean sparse = annotationValue.booleanValue("sparse").orElse(false); + String partialFilterExpression = annotationValue.stringValue(ATTR_PARTIAL_FILTER_EXPRESSION).filter(s -> !s.isEmpty()).orElse(null); + boolean sparse = annotationValue.booleanValue(ATTR_SPARSE).orElse(false); if (sparse && partialFilterExpression != null) { throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] cannot define both sparse and partialFilterExpression"); } indexes.add(new ResolvedIndex( - annotationValue.stringValue("name").filter(s -> !s.isEmpty()).orElse(null), + annotationValue.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.copyOf(fields), - annotationValue.booleanValue("unique").orElse(false), + annotationValue.booleanValue(ATTR_UNIQUE).orElse(false), sparse, - annotationValue.booleanValue("hidden").orElse(false), + annotationValue.booleanValue(ATTR_HIDDEN).orElse(false), null, partialFilterExpression, - annotationValue.stringValue("collation").filter(s -> !s.isEmpty()).orElse(null), + annotationValue.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), indexBits, indexMin, indexMax, @@ -497,9 +523,9 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit null, null, null, - parseJsonOption(annotationValue.stringValue("storageEngine").filter(s -> !s.isEmpty()).orElse(null), "storageEngine", entity.getName()), - annotationValue.stringValue("comment").filter(s -> !s.isEmpty()).orElse(null), - annotationValue.stringValue("commitQuorum").filter(s -> !s.isEmpty()).orElse(null) + parseJsonOption(annotationValue.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), + annotationValue.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), + annotationValue.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) )); } return indexes; From 15344eb39d86ec295f9e792617e2152778aedcf0 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 10:40:12 +0200 Subject: [PATCH 27/34] Cleanup --- .../mongodb/common/MongoEntityIndexes.java | 1 + .../init/AbstractMongoCollectionsCreator.java | 34 +++++++------------ .../geo2d/MongoGeo2dIndexCreationSpec.groovy | 22 ++++++++++++ 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 4a1577535ea..aabebb9a138 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -55,6 +55,7 @@ * @author radovanradic * @since 5.0.0 */ +@SuppressWarnings("java:S6541") @Internal public final class MongoEntityIndexes { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index 371844eed24..d06c79a8be3 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -36,8 +36,6 @@ import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.BeanIntrospector; import io.micronaut.core.util.CollectionUtils; -import io.micronaut.data.annotation.JsonSubView; -import io.micronaut.data.annotation.JsonView; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; import io.micronaut.data.model.Association; @@ -59,7 +57,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -143,8 +140,6 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, PersistentEntity[] entities = introspections.stream() .filter(i -> !i.getBeanType().getName().contains("$")) .filter(i -> !Modifier.isAbstract(i.getBeanType().getModifiers())) - .filter(i -> !i.hasAnnotation(JsonSubView.class)) - .sorted(Comparator.comparing(i -> i.hasAnnotation(JsonView.class))) .map(beanIntrospection -> runtimeEntityRegistry.getEntity(beanIntrospection.getBeanType())) .toArray(PersistentEntity[]::new); @@ -205,13 +200,13 @@ protected void initialize(RuntimeEntityRegistry runtimeEntityRegistry, } if (createIndexes) { if (indexFailureCount > 0 && indexCreationFailurePolicy == IndexCreationFailurePolicy.WARN_AND_CONTINUE) { - LOG.warn("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", + LOG.warn("MongoDB index initialization for database: {} -> processed={}, failures={}, policy={}", databaseName, indexProcessedCount, indexFailureCount, indexCreationFailurePolicy); } else if (LOG.isInfoEnabled()) { - LOG.info("MongoDB index initialization telemetry for database: {} -> processed={}, failures={}, policy={}", + LOG.info("MongoDB index initialization for database: {} -> processed={}, failures={}, policy={}", databaseName, indexProcessedCount, indexFailureCount, @@ -566,19 +561,13 @@ static Collation toCollation(Document document) { } static @Nullable String normalizeJsonValue(@Nullable Object value) { - if (value == null) { - return null; - } - if (value instanceof Document document) { - return document.toJson(); - } - if (value instanceof org.bson.BsonDocument bsonDocument) { - return bsonDocument.toJson(); - } - if (value instanceof String stringValue) { - return normalizeJsonString(stringValue); - } - return value.toString(); + return switch (value) { + case null -> null; + case Document document -> document.toJson(); + case org.bson.BsonDocument bsonDocument -> bsonDocument.toJson(); + case String stringValue -> normalizeJsonString(stringValue); + default -> value.toString(); + }; } static @Nullable String normalizeJsonString(@Nullable String value) { @@ -629,7 +618,7 @@ private RuntimeException mapCreateIndexConflict(RuntimeException e, + ", mongoErrorCode=" + mongoIndexConflict.errorCode() + ", mongoMessage=" - + String.valueOf(mongoIndexConflict.message()), e); + + mongoIndexConflict.message(), e); } private @Nullable MongoIndexConflict findMongoIndexConflict(Throwable throwable) { @@ -673,7 +662,8 @@ private boolean isIndexConflict(int errorCode, * * @param The database type */ - interface DatabaseOperationsProvider { + @Internal + public interface DatabaseOperationsProvider { /** * Gets {@link DatabaseOperations} for given configuration. diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy index ac3ff3bdb54..5f18a8dbcf1 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geo2d/MongoGeo2dIndexCreationSpec.groovy @@ -7,8 +7,10 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.repository.CrudRepository import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -22,6 +24,9 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp @Shared MongoClient mongoClient + @Shared + Geo2dIndexedEntityRepository repository + @Override List getPackageNames() { ['io.micronaut.data.mongodb.index.geo2d'] @@ -37,6 +42,7 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp 'micronaut.data.mongodb.create-indexes' : 'true' ]) mongoClient = applicationContext.getBean(MongoClient) + repository = applicationContext.getBean(Geo2dIndexedEntityRepository) } void 'creates field 2d index'() { @@ -55,6 +61,22 @@ class MongoGeo2dIndexCreationSpec extends Specification implements MongoTestProp assert index.fields[0].kind() == '2d' } } + + void 'saves and loads map-backed 2d location via repository'() { + given: + def entity = new Geo2dIndexedEntity(location: [x: -73.99d, y: 40.75d]) + + when: + def saved = repository.save(entity) + def loaded = repository.findById(saved.id).orElseThrow() + + then: + loaded.location == [x: -73.99d, y: 40.75d] + } +} + +@MongoRepository +interface Geo2dIndexedEntityRepository extends CrudRepository { } @MappedEntity('geo2d_indexed_entities') From 46b855cd3a0e5284cc1e5bba3153c93d491f3156 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 11:20:15 +0200 Subject: [PATCH 28/34] Cleanup --- .../init/AbstractMongoCollectionsCreator.java | 4 +- .../MongoClusteredTtlValidSpec.groovy | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttlvalid/MongoClusteredTtlValidSpec.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index d06c79a8be3..cd416692136 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -55,6 +55,7 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Modifier; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -261,7 +262,8 @@ private MongoResolvedCollectionOptions resolveCollectionOptions(PersistentEntity boolean supportedTtlIdType = idType.equals(Date.class.getName()) || idType.equals(Instant.class.getName()) || idType.equals(LocalDateTime.class.getName()) - || idType.equals(OffsetDateTime.class.getName()); + || idType.equals(OffsetDateTime.class.getName()) + || idType.equals(ZonedDateTime.class.getName()); if (!supportedTtlIdType) { throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires a date/time identity type, but found [" + idType + "]"); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttlvalid/MongoClusteredTtlValidSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttlvalid/MongoClusteredTtlValidSpec.groovy new file mode 100644 index 00000000000..73dd96f71ec --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/clusteredttlvalid/MongoClusteredTtlValidSpec.groovy @@ -0,0 +1,47 @@ +package io.micronaut.data.mongodb.index.validation.clusteredttlvalid + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoClusteredIndex +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +import java.time.ZonedDateTime + +class MongoClusteredTtlValidSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.validation.clusteredttlvalid'] + } + + void 'starts when clustered TTL uses ZonedDateTime id type'() { + when: + def context = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + context?.close() + } +} + +@MongoRepository +interface ValidClusteredTtlEntityRepository extends CrudRepository { +} + +@MongoClusteredIndex(name = 'valid_clustered_ttl_idx', expireAfterSeconds = 300) +@MappedEntity('valid_clustered_ttl_entities') +class ValidClusteredTtlEntity { + @Id + ZonedDateTime id + + String name +} From 63c6b1eb8a752ff55d7b3e3b98ab14a7af9aaadb Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 12:34:16 +0200 Subject: [PATCH 29/34] Cleanup --- .../init/AbstractMongoCollectionsCreator.java | 18 +++++-- ...MongoGeoPointValueIndexCreationSpec.groovy | 3 -- ...tryCollectionValueIndexCreationSpec.groovy | 3 -- ...GeoLineStringValueIndexCreationSpec.groovy | 3 -- ...ltiLineStringValueIndexCreationSpec.groovy | 3 -- ...GeoMultiPointValueIndexCreationSpec.groovy | 3 -- ...oMultiPolygonValueIndexCreationSpec.groovy | 3 -- ...ngoGeoPolygonValueIndexCreationSpec.groovy | 3 -- ...ongoAggregatedTextIndexCreationSpec.groovy | 3 +- ...veExistingIndexAdvancedConflictSpec.groovy | 4 +- ...goReactiveExistingIndexConflictSpec.groovy | 4 +- ...ndexBootstrapWithoutCollectionsSpec.groovy | 4 +- ...lusteredCollectionCompatibilitySpec.groovy | 13 ----- ...tingClusteredCollectionConflictSpec.groovy | 13 ----- ...ctiveExistingIndexCompatibilitySpec.groovy | 13 ----- ...ExistingIndexNameReconciliationSpec.groovy | 13 ----- ...eIndexFailurePolicyWarnContinueSpec.groovy | 13 ----- ...ReactiveStorageEngineValidationSpec.groovy | 46 ------------------ ...iveWildcardProjectionValidationSpec.groovy | 45 ----------------- ...dMultipleDeclarationsValidationSpec.groovy | 48 ------------------- ...eactiveTextIndexVersionConflictSpec.groovy | 4 +- .../mongo/mongoMapping/mongoIndexes.adoc | 9 ++-- 22 files changed, 23 insertions(+), 248 deletions(-) rename data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/{reactiveexistingindexadvancedconflict => existingindexadvancedconflict}/MongoReactiveExistingIndexAdvancedConflictSpec.groovy (56%) rename data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/{reactiveexistingindexconflict => existingindexconflict}/MongoReactiveExistingIndexConflictSpec.groovy (58%) rename data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/{reactiveindexbootstrap => indexbootstrap}/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy (70%) delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy delete mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy rename data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/{reactivetextversionconflict => textversionconflict}/MongoReactiveTextIndexVersionConflictSpec.groovy (59%) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index cd416692136..2f0f4adef21 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -55,6 +55,8 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Modifier; +import java.sql.Timestamp; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -84,6 +86,16 @@ public class AbstractMongoCollectionsCreator { private static final int INDEX_OPTIONS_CONFLICT_CODE = 85; private static final int INDEX_KEY_SPECS_CONFLICT_CODE = 86; private static final String INDEX_OPTIONS_CONFLICT_CODE_NAME = "IndexOptionsConflict"; + private static final Set SUPPORTED_CLUSTERED_TTL_ID_TYPES = Set.of( + Date.class.getName(), + java.sql.Date.class.getName(), + Timestamp.class.getName(), + Instant.class.getName(), + LocalDate.class.getName(), + LocalDateTime.class.getName(), + OffsetDateTime.class.getName(), + ZonedDateTime.class.getName() + ); private static final String INDEX_KEY_SPECS_CONFLICT_CODE_NAME = "IndexKeySpecsConflict"; /** @@ -259,11 +271,7 @@ private MongoResolvedCollectionOptions resolveCollectionOptions(PersistentEntity throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires an identity property"); } String idType = identity.getTypeName(); - boolean supportedTtlIdType = idType.equals(Date.class.getName()) - || idType.equals(Instant.class.getName()) - || idType.equals(LocalDateTime.class.getName()) - || idType.equals(OffsetDateTime.class.getName()) - || idType.equals(ZonedDateTime.class.getName()); + boolean supportedTtlIdType = SUPPORTED_CLUSTERED_TTL_ID_TYPES.contains(idType); if (!supportedTtlIdType) { throw new IllegalStateException("Mongo clustered TTL collection for entity [" + entity.getName() + "] requires a date/time identity type, but found [" + idType + "]"); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy index 28287010abd..bc93c9134ff 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/MongoGeoPointValueIndexCreationSpec.groovy @@ -6,11 +6,9 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed -import io.micronaut.data.model.DataType import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -64,7 +62,6 @@ class GeoPointValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_point_location_idx') Point location } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy index 261a73e8484..1f0b6a2042f 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/geometrycollection/MongoGeoGeometryCollectionValueIndexCreationSpec.groovy @@ -6,10 +6,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import spock.lang.AutoCleanup import spock.lang.Shared @@ -65,7 +63,6 @@ class GeoGeometryCollectionValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_geometry_collection_location_idx') GeometryCollection geometry } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy index 39ea3bb9376..20bb4afbcd7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/linestring/MongoGeoLineStringValueIndexCreationSpec.groovy @@ -7,10 +7,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository @@ -95,7 +93,6 @@ class GeoLineStringValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_linestring_location_idx') LineString route } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy index 72389763958..ef00e6be65d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multilinestring/MongoGeoMultiLineStringValueIndexCreationSpec.groovy @@ -7,10 +7,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository @@ -103,7 +101,6 @@ class GeoMultiLineStringValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multilinestring_location_idx') MultiLineString paths } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy index f1e492cbcb8..31c8ee5163d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipoint/MongoGeoMultiPointValueIndexCreationSpec.groovy @@ -7,10 +7,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository @@ -95,7 +93,6 @@ class GeoMultiPointValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multipoint_location_idx') MultiPoint locations } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy index e124582727e..f7035384360 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/multipolygon/MongoGeoMultiPolygonValueIndexCreationSpec.groovy @@ -8,10 +8,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository @@ -108,7 +106,6 @@ class GeoMultiPolygonValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_multipolygon_location_idx') MultiPolygon areas } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy index c736c72d315..c20d2980aa3 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geovalue/polygon/MongoGeoPolygonValueIndexCreationSpec.groovy @@ -7,10 +7,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.annotation.TypeDef import io.micronaut.data.document.mongodb.MongoIndexInspector import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.model.DataType import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.MongoRepository import io.micronaut.data.repository.CrudRepository @@ -95,7 +93,6 @@ class GeoPolygonValueIndexedEntity { @GeneratedValue String id - @TypeDef(type = DataType.OBJECT) @MongoGeoIndexed(name = 'geo_polygon_location_idx') Polygon area } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy index 76a020b10ea..5aa5e70fafc 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -67,6 +67,5 @@ class AggregatedTextIndexedEntity { @MongoTextIndexed(name = 'aggregated_text_idx', weight = 2, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) String title - @MongoTextIndexed(name = 'aggregated_text_idx', weight = 5, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) - String description + } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy similarity index 56% rename from data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy index c6efd33ee3f..e098984d077 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexadvancedconflict/MongoReactiveExistingIndexAdvancedConflictSpec.groovy @@ -1,6 +1,4 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingindexadvancedconflict - -import io.micronaut.data.mongodb.index.validation.existingindexadvancedconflict.MongoExistingIndexAdvancedConflictSpec +package io.micronaut.data.mongodb.index.validation.existingindexadvancedconflict class MongoReactiveExistingIndexAdvancedConflictSpec extends MongoExistingIndexAdvancedConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy similarity index 58% rename from data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy index 11a38896bc3..19386060b4c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexconflict/MongoReactiveExistingIndexConflictSpec.groovy @@ -1,6 +1,4 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingindexconflict - -import io.micronaut.data.mongodb.index.validation.existingindexconflict.MongoExistingIndexConflictSpec +package io.micronaut.data.mongodb.index.validation.existingindexconflict class MongoReactiveExistingIndexConflictSpec extends MongoExistingIndexConflictSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy similarity index 70% rename from data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy index ca8b3c0b017..9bba2cbb4d7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/indexbootstrap/MongoReactiveIndexBootstrapWithoutCollectionsSpec.groovy @@ -1,6 +1,4 @@ -package io.micronaut.data.mongodb.index.validation.reactiveindexbootstrap - -import io.micronaut.data.mongodb.index.validation.indexbootstrap.MongoIndexBootstrapWithoutCollectionsSpec +package io.micronaut.data.mongodb.index.validation.indexbootstrap class MongoReactiveIndexBootstrapWithoutCollectionsSpec extends MongoIndexBootstrapWithoutCollectionsSpec { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy deleted file mode 100644 index 95b8324ce9a..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredcompatibility/MongoReactiveExistingClusteredCollectionCompatibilitySpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingclusteredcompatibility - -import io.micronaut.data.mongodb.index.validation.existingclusteredcompatibility.MongoExistingClusteredCollectionCompatibilitySpec - -class MongoReactiveExistingClusteredCollectionCompatibilitySpec extends MongoExistingClusteredCollectionCompatibilitySpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy deleted file mode 100644 index 84a9d7e3c98..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingclusteredconflict/MongoReactiveExistingClusteredCollectionConflictSpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingclusteredconflict - -import io.micronaut.data.mongodb.index.validation.existingclusteredconflict.MongoExistingClusteredCollectionConflictSpec - -class MongoReactiveExistingClusteredCollectionConflictSpec extends MongoExistingClusteredCollectionConflictSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy deleted file mode 100644 index 3b143a59019..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexcompatibility/MongoReactiveExistingIndexCompatibilitySpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingindexcompatibility - -import io.micronaut.data.mongodb.index.validation.existingindexcompatibility.MongoExistingIndexCompatibilitySpec - -class MongoReactiveExistingIndexCompatibilitySpec extends MongoExistingIndexCompatibilitySpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy deleted file mode 100644 index ce35a68b98b..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveexistingindexname/MongoReactiveExistingIndexNameReconciliationSpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactiveexistingindexname - -import io.micronaut.data.mongodb.index.validation.existingindexname.MongoExistingIndexNameReconciliationSpec - -class MongoReactiveExistingIndexNameReconciliationSpec extends MongoExistingIndexNameReconciliationSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy deleted file mode 100644 index 7053a4700fd..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactiveindexfailurepolicy/MongoReactiveIndexFailurePolicyWarnContinueSpec.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactiveindexfailurepolicy - -import io.micronaut.data.mongodb.index.validation.indexfailurepolicy.MongoIndexFailurePolicyWarnContinueSpec - -class MongoReactiveIndexFailurePolicyWarnContinueSpec extends MongoIndexFailurePolicyWarnContinueSpec { - - @Override - Map getProperties() { - super.getProperties() + [ - 'micronaut.data.mongodb.driver-type': 'reactive' - ] - } -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy deleted file mode 100644 index ac44e935863..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivestorageengine/MongoReactiveStorageEngineValidationSpec.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactivestorageengine - -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.index.MongoIndexed -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.repository.CrudRepository -import spock.lang.Specification - -class MongoReactiveStorageEngineValidationSpec extends Specification implements MongoSelectReactiveDriver { - - @Override - List getPackageNames() { - ['io.micronaut.data.mongodb.index.validation.reactivestorageengine'] - } - - void 'fails fast for invalid storageEngine JSON in reactive mode'() { - when: - ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - then: - def e = thrown(RuntimeException) - e.message.contains('Mongo storageEngine for entity') - e.message.contains('must be valid JSON') - } -} - -@MongoRepository -interface InvalidReactiveStorageEngineEntityRepository extends CrudRepository { -} - -@MappedEntity('invalid_reactive_storage_engine_entities') -class InvalidReactiveStorageEngineEntity { - @Id - @GeneratedValue - String id - - @MongoIndexed(name = 'invalid_reactive_storage_engine_idx', storageEngine = '{ bad-json }') - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy deleted file mode 100644 index 189a168c2e4..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardprojection/MongoReactiveWildcardProjectionValidationSpec.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactivewildcardprojection - -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed -import io.micronaut.data.repository.CrudRepository -import spock.lang.Specification - -class MongoReactiveWildcardProjectionValidationSpec extends Specification implements MongoSelectReactiveDriver { - - @Override - List getPackageNames() { - ['io.micronaut.data.mongodb.index.validation.reactivewildcardprojection'] - } - - void 'fails fast for field-level wildcard projection in reactive mode'() { - when: - ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - then: - def e = thrown(RuntimeException) - e.message.contains('field-level @MongoWildcardIndexed is not supported by MongoDB') - } -} - -@MongoRepository -interface InvalidReactiveWildcardProjectionEntityRepository extends CrudRepository { -} - -@MappedEntity('invalid_reactive_wildcard_projection_entities') -class InvalidReactiveWildcardProjectionEntity { - @Id - @GeneratedValue - String id - - @MongoWildcardIndexed(name = 'invalid_reactive_wildcard_projection_idx', wildcardProjection = '{ "metadata.secret": 0 }') - Map metadata -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy deleted file mode 100644 index 3a1a69335d9..00000000000 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivewildcardtoplevelmultiple/MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec.groovy +++ /dev/null @@ -1,48 +0,0 @@ -package io.micronaut.data.mongodb.index.validation.reactivewildcardtoplevelmultiple - -import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity -import io.micronaut.data.document.mongodb.reactive.MongoSelectReactiveDriver -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex -import io.micronaut.data.repository.CrudRepository -import spock.lang.Specification - -class MongoReactiveTopLevelWildcardMultipleDeclarationsValidationSpec extends Specification implements MongoSelectReactiveDriver { - - @Override - List getPackageNames() { - ['io.micronaut.data.mongodb.index.validation.reactivewildcardtoplevelmultiple'] - } - - void 'allows reactive multiple top-level wildcard declarations when wildcardProjection differs'() { - when: - def context = ApplicationContext.run(getProperties() + [ - 'micronaut.data.mongodb.create-collections': 'true', - 'micronaut.data.mongodb.create-indexes' : 'true' - ]) - - then: - noExceptionThrown() - - cleanup: - context?.close() - } -} - -@MongoRepository -interface InvalidReactiveTopLevelWildcardMultipleEntityRepository extends CrudRepository { -} - -@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_idx', wildcardProjection = '{ "metadata.secret": 0 }') -@MongoWildcardIndex(name = 'invalid_reactive_top_level_wildcard_multiple_other_idx', wildcardProjection = '{ "metadata.internal": 0 }') -@MappedEntity('invalid_reactive_top_level_wildcard_multiple_entities') -class InvalidReactiveTopLevelWildcardMultipleEntity { - @Id - @GeneratedValue - String id - - Map metadata -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy similarity index 59% rename from data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy rename to data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy index 7b4c48205b9..9c7c798f7cc 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/reactivetextversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textversionconflict/MongoReactiveTextIndexVersionConflictSpec.groovy @@ -1,6 +1,4 @@ -package io.micronaut.data.mongodb.index.validation.reactivetextversionconflict - -import io.micronaut.data.mongodb.index.validation.textversionconflict.MongoTextIndexVersionConflictSpec +package io.micronaut.data.mongodb.index.validation.textversionconflict class MongoReactiveTextIndexVersionConflictSpec extends MongoTextIndexVersionConflictSpec { diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index 2f64ad48033..27007f64e31 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -198,21 +198,22 @@ Clustered collection support follows MongoDB collection-level rules: * clustered options are applied through collection creation, not ordinary index creation * `unique` must remain `true` -* when `expireAfterSeconds` is set, the identity type must be a date/time type (`Date`, `Instant`, `LocalDateTime`, `OffsetDateTime`) +* when `expireAfterSeconds` is set, the identity type must be one of the supported date/time types (`Date`, `java.sql.Date`, `java.sql.Timestamp`, `LocalDate`, `Instant`, `LocalDateTime`, `OffsetDateTime`, `ZonedDateTime`) == Existing collections and conflicts When `create-indexes` is enabled, Micronaut Data checks existing collection indexes before creating new ones. -If an index with the same key exists but managed options differ, startup fails fast with an explicit conflict message. +If an index with the same key exists but managed options differ, index initialization reports an explicit conflict. +With `micronaut.data.mongodb.create-indexes-failure-policy=FAIL_FAST`, startup fails immediately. With `WARN_AND_CONTINUE`, the conflict is logged and startup continues. Managed reconciliation is key-first and option-aware: * existing indexes with matching key + managed options are accepted (startup continues) -* if a desired explicit name differs from an existing same-key index name, startup fails fast with an explicit name-conflict message +* if a desired explicit name differs from an existing same-key index name, index initialization reports an explicit name-conflict message and then either fails fast or logs-and-continues according to `micronaut.data.mongodb.create-indexes-failure-policy` * if a desired name is omitted, existing same-key indexes are matched by key/options (generated or existing names are not used as primary identity) Managed option matching includes: uniqueness/sparse/hidden, TTL, partial filter, collation, geospatial/text-specific options, wildcard projection, and storage engine. `comment` and `commitQuorum` are treated as index-creation command options and are not part of existing-index managed option reconciliation. -For clustered declarations, existing collection options are also compared, and conflicting clustered options fail fast. +For clustered declarations, existing collection options are also compared. Conflicts are then handled according to `micronaut.data.mongodb.create-indexes-failure-policy`. From df2d38d1f589dd55ae42cbb70a0778fe5334fc01 Mon Sep 17 00:00:00 2001 From: Radovan Radic Date: Thu, 2 Apr 2026 14:08:43 +0200 Subject: [PATCH 30/34] Remove branch build triggers Co-authored-by: Radovan Radic --- .github/workflows/graalvm-latest.yml | 1 - .github/workflows/gradle.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml index 39af250df25..24405d031dd 100644 --- a/.github/workflows/graalvm-latest.yml +++ b/.github/workflows/graalvm-latest.yml @@ -9,7 +9,6 @@ on: branches: - master - '[0-9]+.[0-9]+.x' - - radovanradic/mongodb-indexes pull_request: branches: - master diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a44ed30babb..605280bba09 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,7 +9,6 @@ on: branches: - master - '[0-9]+.[0-9]+.x' - - radovanradic/mongodb-indexes pull_request: branches: - master From c8cd58ba01109749a0e11230a9918d7f841356df Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 2 Apr 2026 14:45:11 +0200 Subject: [PATCH 31/34] Address CR comments. --- .../mongodb/annotation/index/MongoCompoundIndexes.java | 2 +- .../mongodb/annotation/index/MongoWildcardIndexes.java | 2 +- .../mongodb/init/AbstractMongoCollectionsCreator.java | 9 +++++++++ .../data/document/mongodb/MongoIndexInspector.groovy | 1 + .../MongoCompoundGeo2dOptionsIndexCreationSpec.groovy | 2 +- .../embedded/MongoEmbeddedFieldIndexCreationSpec.groovy | 2 +- .../text/MongoAggregatedTextIndexCreationSpec.groovy | 5 +++++ 7 files changed, 19 insertions(+), 4 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java index dee2b96db68..a7926aa2b0e 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java @@ -29,7 +29,7 @@ * @since 5.0.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Documented @Inherited public @interface MongoCompoundIndexes { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java index 9d40ca251e8..03673fc3048 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java @@ -29,7 +29,7 @@ * @since 5.0.0 */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Documented @Inherited public @interface MongoWildcardIndexes { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index 2f0f4adef21..fa24cb8760b 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -468,6 +468,15 @@ static IndexOptions toIndexOptions(MongoResolvedIndex index) { if (index.textIndexVersion() != null) { indexOptions.textVersion(index.textIndexVersion()); } + Document weights = new Document(); + for (MongoResolvedIndexField field : index.fields()) { + if (field.weight() != null) { + weights.append(field.path(), field.weight()); + } + } + if (!weights.isEmpty()) { + indexOptions.weights(weights); + } if (index.sphereVersion() != null) { indexOptions.sphereVersion(index.sphereVersion()); } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy index 850951482d7..f1e1071000c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoIndexInspector.groovy @@ -39,6 +39,7 @@ final class MongoIndexInspector { wildcardProjection : indexDocument.get('wildcardProjection'), min : indexDocument.get('min'), max : indexDocument.get('max'), + weights : indexDocument.get('weights'), defaultLanguage : indexDocument.getString('default_language'), languageOverride : indexDocument.getString('language_override'), textIndexVersion : indexDocument.getInteger('textIndexVersion'), diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy index 449275dda9d..a0538792784 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/options/MongoCompoundGeo2dOptionsIndexCreationSpec.groovy @@ -26,7 +26,7 @@ class MongoCompoundGeo2dOptionsIndexCreationSpec extends Specification implement @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.geocompound.index.options'] + ['io.micronaut.data.mongodb.index.geocompound.options'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy index 36021c1b597..5775374e4aa 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/simple/embedded/MongoEmbeddedFieldIndexCreationSpec.groovy @@ -19,7 +19,7 @@ class MongoEmbeddedFieldIndexCreationSpec extends Specification implements Mongo @Override List getPackageNames() { - ['io.micronaut.data.document.mongodb.simple.index.embedded'] + ['io.micronaut.data.mongodb.index.simple.embedded'] } Class expectedCollectionsCreatorBeanType() { diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy index 5aa5e70fafc..2f70fec60a9 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/text/MongoAggregatedTextIndexCreationSpec.groovy @@ -51,6 +51,8 @@ class MongoAggregatedTextIndexCreationSpec extends Specification implements Mong assert index.fields.size() == 2 assert index.fields*.path().contains('_fts') assert index.fields*.path().contains('_ftsx') + assert index.weights.title == 2 + assert index.weights.description == 5 assert index.defaultLanguage == 'french' assert index.languageOverride == 'lang' assert index.textIndexVersion == 3 @@ -67,5 +69,8 @@ class AggregatedTextIndexedEntity { @MongoTextIndexed(name = 'aggregated_text_idx', weight = 2, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) String title + @MongoTextIndexed(name = 'aggregated_text_idx', weight = 5, defaultLanguage = 'french', languageOverride = 'lang', textIndexVersion = 3) + String description + } From c2f9997f51ac6469dff692ab99b24b71c4907ef3 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 16 Apr 2026 14:13:07 +0200 Subject: [PATCH 32/34] Improvements, covering some gaps noticed in peer review. --- .../annotation/index/MongoCompoundIndex.java | 15 ++ .../index/MongoCompoundIndexField.java | 20 ++ .../annotation/index/MongoGeoIndexed.java | 15 ++ .../annotation/index/MongoHashedIndexed.java | 5 + .../annotation/index/MongoIndexed.java | 5 + .../annotation/index/MongoTextIndexed.java | 5 + .../index/MongoWildcardIndexed.java | 5 + .../mongodb/common/MongoEntityIndexes.java | 207 ++++++++++++++--- .../init/AbstractMongoCollectionsCreator.java | 100 ++++++++- .../MongoCollationIndexCreationSpec.groovy | 41 ++++ ...ongoCompoundHashedIndexCreationSpec.groovy | 74 +++++++ .../MongoCompoundTextIndexCreationSpec.groovy | 89 ++++++++ .../MongoCompoundGeoIndexCreationSpec.groovy | 3 +- .../MongoCollationValidationSpec.groovy | 3 +- ...MongoExistingIndexCompatibilitySpec.groovy | 56 +++++ ...ctingCompoundGeoSphereVersionEntity.groovy | 32 +++ ...ngoCompoundGeoOptionsValidationSpec.groovy | 26 +++ ...validCompoundGeoSphereVersionEntity.groovy | 33 +++ .../MongoGeoRulesResolutionSpec.groovy | 209 ++++++++++++++++++ ...goCompoundHashedIndexValidationSpec.groovy | 56 +++++ ...oIndexAdvancedOptionsResolutionSpec.groovy | 112 ++++++++++ ...ongoCompoundTextIndexValidationSpec.groovy | 39 ++++ .../text/MongoTextIndexValidationSpec.groovy | 22 +- .../InvalidCompoundTextAdjacencyEntity.groovy | 38 ++++ .../multiple/MultipleTextIndexEntity.groovy | 29 +++ .../textweight/InvalidTextWeightEntity.groovy | 22 ++ ...AbstractMongoCollectionsCreatorSpec.groovy | 59 +++++ .../mongo/mongoMapping/mongoIndexes.adoc | 26 ++- 28 files changed, 1283 insertions(+), 63 deletions(-) create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/hashed/MongoCompoundHashedIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/text/MongoCompoundTextIndexCreationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundconflictingsphere/ConflictingCompoundGeoSphereVersionEntity.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundsphereversion/InvalidCompoundGeoSphereVersionEntity.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georulesresolution/MongoGeoRulesResolutionSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/hashed/MongoCompoundHashedIndexValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/adjacency/InvalidCompoundTextAdjacencyEntity.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy create mode 100644 data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textweight/InvalidTextWeightEntity.groovy diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java index 5e627d08c58..5ddfe0bdd55 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java @@ -76,6 +76,21 @@ */ String collation() default ""; + /** + * @return The text index default language. + */ + String defaultLanguage() default ""; + + /** + * @return The document field that overrides language for text processing. + */ + String languageOverride() default ""; + + /** + * @return The text index version, or -1 if unset. + */ + int textIndexVersion() default -1; + /** * @return The index creation command comment. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java index 98b168f7493..b1a97e6c4db 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java @@ -44,6 +44,21 @@ */ MongoGeoIndexType geoType() default MongoGeoIndexType.GEO_2DSPHERE; + /** + * @return Whether the field should use the text key kind. + */ + boolean text() default false; + + /** + * @return The text index weight. Only valid when {@link #text()} is {@code true}. + */ + int weight() default 1; + + /** + * @return Whether the field should use the hashed key kind. + */ + boolean hashed() default false; + /** * @return Whether the field should use the geospatial key kind instead of the numeric direction. */ @@ -54,6 +69,11 @@ */ int bits() default -1; + /** + * @return The 2dsphere index version, or -1 if unset. + */ + int sphereVersion() default -1; + /** * @return The 2d index minimum value, or NaN if unset. */ diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java index cc7a9e18780..69e4b21b473 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java @@ -59,6 +59,16 @@ */ String storageEngine() default ""; + /** + * @return The partial filter expression as JSON. + */ + String partialFilterExpression() default ""; + + /** + * @return The collation definition as JSON. + */ + String collation() default ""; + /** * @return The 2dsphere index version, or {@code -1} if unset. Only valid for * {@link MongoGeoIndexType#GEO_2DSPHERE}. @@ -82,4 +92,9 @@ * {@link MongoGeoIndexType#GEO_2D}. Represents the upper inclusive boundary. MongoDB default is 180.0. */ double max() default Double.NaN; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java index 36b968c9a55..2f387064821 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java @@ -53,4 +53,9 @@ * @return The storage engine options as JSON. */ String storageEngine() default ""; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java index 7da415223f5..425315271ea 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java @@ -83,4 +83,9 @@ * @return The storage engine options as JSON. */ String storageEngine() default ""; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java index 4aab24d0ade..2db0a4d8c91 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java @@ -73,4 +73,9 @@ * @return The text index version, or -1 if unset. */ int textIndexVersion() default -1; + + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java index 6143b46416f..50667770fec 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java @@ -59,4 +59,9 @@ */ String storageEngine() default ""; + /** + * @return The createIndexes commit quorum. + */ + String commitQuorum() default ""; + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index aabebb9a138..4f875c1addc 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -44,6 +44,7 @@ import org.bson.Document; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -114,9 +115,24 @@ private static MongoEntityIndexes resolve(RuntimePersistentEntity entity) { indexes.addAll(resolveTopLevelWildcardIndexes(entity)); indexes.addAll(resolveTextIndexes(entity)); indexes.addAll(resolveCompoundIndexes(entity)); + validateResolvedIndexes(entity, indexes); return new MongoEntityIndexes(List.copyOf(indexes)); } + private static void validateResolvedIndexes(RuntimePersistentEntity entity, + List indexes) { + long textIndexCount = indexes.stream() + .filter(MongoEntityIndexes::isTextIndex) + .count(); + if (textIndexCount > 1) { + throw new IllegalStateException("MongoDB allows only one text index per collection. Entity [" + entity.getName() + "] declares " + textIndexCount + " text indexes"); + } + } + + private static boolean isTextIndex(ResolvedIndex index) { + return index.fields().stream().anyMatch(field -> "text".equals(field.kind())); + } + private static List resolveTopLevelWildcardIndexes(RuntimePersistentEntity entity) { Map> groupedIndexes = new LinkedHashMap<>(); for (var annotation : entity.getAnnotationMetadata().getAnnotationValuesByType(MongoWildcardIndex.class)) { @@ -136,7 +152,7 @@ private static List resolveTopLevelWildcardIndexes(RuntimePersist null, null, null, - annotation.stringValue(ATTR_WILDCARD_PROJECTION).filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(annotation.stringValue(ATTR_WILDCARD_PROJECTION).filter(s -> !s.isEmpty()).orElse(null), ATTR_WILDCARD_PROJECTION, entity.getName()), parseJsonOption(annotation.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), annotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), annotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) @@ -220,8 +236,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? annotation.intValue(ATTR_EXPIRE_AFTER_SECONDS).getAsInt() : null, - annotation.stringValue(ATTR_PARTIAL_FILTER_EXPRESSION).filter(s -> !s.isEmpty()).orElse(null), - annotation.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), + parseJsonOption(annotation.stringValue(ATTR_PARTIAL_FILTER_EXPRESSION).filter(s -> !s.isEmpty()).orElse(null), ATTR_PARTIAL_FILTER_EXPRESSION, entity.getName()), + parseJsonOption(annotation.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), ATTR_COLLATION, entity.getName()), null, null, null, @@ -232,7 +248,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), annotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), - null + annotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) )); return; } @@ -261,7 +277,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), hashedAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), - null + hashedAnnotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) )); return; } @@ -273,6 +289,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity= 0 ? geoAnnotation.intValue(ATTR_BITS).getAsInt() : null; Double min = geoAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(geoAnnotation.doubleValue(ATTR_MIN).getAsDouble()) ? geoAnnotation.doubleValue(ATTR_MIN).getAsDouble() : null; Double max = geoAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(geoAnnotation.doubleValue(ATTR_MAX).getAsDouble()) ? geoAnnotation.doubleValue(ATTR_MAX).getAsDouble() : null; + String partialFilterExpression = parseJsonOption(geoAnnotation.stringValue(ATTR_PARTIAL_FILTER_EXPRESSION).filter(s -> !s.isEmpty()).orElse(null), ATTR_PARTIAL_FILTER_EXPRESSION, entity.getName()); + String collation = parseJsonOption(geoAnnotation.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), ATTR_COLLATION, entity.getName()); if (type != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d indexes on entity [" + entity.getName() + "]"); } @@ -282,6 +300,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), List.of(new ResolvedIndexField(persistedPath, null, null, type.getKey(), min, max)), @@ -289,8 +308,8 @@ private static List resolveFieldIndexes(RuntimePersistentEntity resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), geoAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), - null + geoAnnotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) )); return; } @@ -330,7 +349,7 @@ private static List resolveFieldIndexes(RuntimePersistentEntity !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), wildcardAnnotation.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), - null + wildcardAnnotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null) )); } }); @@ -340,17 +359,23 @@ private static List resolveFieldIndexes(RuntimePersistentEntity entity, RuntimePersistentProperty property) { MongoGeoIndexType indexType = property.getAnnotationMetadata().enumValue(MongoGeoIndexed.class, "type", MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); - Class propertyType = property.getType(); - if ((indexType == MongoGeoIndexType.GEO_2D && Map.class.isAssignableFrom(propertyType)) - || isSupportedGeoIndexedType(propertyType)) { + validateGeoIndexedType(entity, property.getName(), property.getType(), indexType); + } + + private static void validateGeoIndexedType(RuntimePersistentEntity entity, + String propertyName, + Class propertyType, + MongoGeoIndexType indexType) { + if (isSupportedGeoIndexedType(propertyType) || isSupportedLegacyGeoValueType(propertyType)) { return; } throw new IllegalStateException("Mongo geospatial index on entity [" + entity.getName() + "] property [" - + property.getName() + + propertyName + "] requires a supported MongoDB GeoJSON type (Geometry, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, or GeometryCollection)" - + (indexType == MongoGeoIndexType.GEO_2D ? " or a Map-backed legacy 2d coordinate value" : "")); + + " or a map/list/array-backed legacy coordinate value" + + (indexType == MongoGeoIndexType.GEO_2D ? "" : " for 2dsphere indexes")); } private static boolean isSupportedGeoIndexedType(Class propertyType) { @@ -364,6 +389,25 @@ private static boolean isSupportedGeoIndexedType(Class propertyType) { || GeometryCollection.class.isAssignableFrom(propertyType); } + private static boolean isSupportedLegacyGeoValueType(Class propertyType) { + return Map.class.isAssignableFrom(propertyType) + || Collection.class.isAssignableFrom(propertyType) + || propertyType.isArray(); + } + + private static void validateGeoCollation(RuntimePersistentEntity entity, + String indexDescription, + @Nullable MongoGeoIndexType indexType, + @Nullable String collation) { + if (indexType != MongoGeoIndexType.GEO_2D || collation == null || collation.isBlank()) { + return; + } + Document collationDocument = Document.parse(collation); + if (!"simple".equals(collationDocument.getString("locale"))) { + throw new IllegalStateException("Mongo " + indexDescription + " on entity [" + entity.getName() + "] supports only collation {\"locale\":\"simple\"}"); + } + } + private static List resolveTextIndexes(RuntimePersistentEntity entity) { TextIndexState state = new TextIndexState(); PersistentEntityUtils.traversePersistentProperties(entity, false, false, (associations, property) -> { @@ -424,12 +468,18 @@ private static List resolveTextIndexes(RuntimePersistentEntity } else if (!Objects.equals(state.textIndexVersion, declaredTextIndexVersion)) { throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same textIndexVersion option"); } + String declaredCommitQuorum = textAnnotation.stringValue(ATTR_COMMIT_QUORUM).filter(s -> !s.isEmpty()).orElse(null); + if (state.commitQuorum == null) { + state.commitQuorum = declaredCommitQuorum; + } else if (!Objects.equals(state.commitQuorum, declaredCommitQuorum)) { + throw new IllegalStateException("Mongo text indexed fields on entity [" + entity.getName() + "] must use the same commitQuorum option"); + } state.fields.add(new ResolvedIndexField(toPersistedPath(associations, property), null, weight, "text", null, null)); }); if (state.fields.isEmpty()) { return List.of(); } - return List.of(new ResolvedIndex(state.name, List.copyOf(state.fields), false, false, state.hidden != null && state.hidden, null, null, null, null, null, null, state.defaultLanguage, state.languageOverride, state.textIndexVersion, null, null, state.storageEngine, state.comment, null)); + return List.of(new ResolvedIndex(state.name, List.copyOf(state.fields), false, false, state.hidden != null && state.hidden, null, null, null, null, null, null, state.defaultLanguage, state.languageOverride, state.textIndexVersion, null, null, state.storageEngine, state.comment, state.commitQuorum)); } private static List resolveCompoundIndexes(RuntimePersistentEntity entity) { @@ -440,34 +490,49 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit Integer indexBits = null; Double indexMin = null; Double indexMax = null; + Integer indexSphereVersion = null; + boolean hasGeoField = false; + boolean hasTextField = false; + boolean hasHashedField = false; + int firstTextFieldIndex = -1; + int lastTextFieldIndex = -1; for (var fieldAnnotation : annotationValue.getAnnotations(ATTR_FIELDS, MongoCompoundIndexField.class)) { String path = fieldAnnotation.stringValue(ATTR_VALUE).orElseThrow(); String pathForLookup = path.contains(".") ? path.replace('.', '_') : path; - String persistedPath = PersistentEntityUtils.getPersistentPropertyPath(entity, pathForLookup) - .map(persistentPath -> { - var propertyPath = entity.getPropertyPath(persistentPath); - if (propertyPath == null) { - throw new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]"); - } - return toPersistedPath(propertyPath); - }) + PersistentPropertyPath propertyPath = PersistentEntityUtils.getPersistentPropertyPath(entity, pathForLookup) + .map(entity::getPropertyPath) + .filter(Objects::nonNull) .orElseThrow(() -> new IllegalStateException("Invalid Mongo index path [" + path + "] for entity [" + entity.getName() + "]")); + String persistedPath = toPersistedPath(propertyPath); + Class propertyType = ((RuntimePersistentProperty) propertyPath.getProperty()).getType(); if (!seenPaths.add(persistedPath)) { throw new IllegalStateException("Duplicate Mongo index path [" + persistedPath + "] for entity [" + entity.getName() + "]"); } boolean geo = fieldAnnotation.booleanValue(ATTR_GEO).orElse(false); + boolean text = fieldAnnotation.booleanValue("text").orElse(false); + boolean hashed = fieldAnnotation.booleanValue("hashed").orElse(false); + int specialKindCount = (geo ? 1 : 0) + (text ? 1 : 0) + (hashed ? 1 : 0); + if (specialKindCount > 1) { + throw new IllegalStateException("Mongo compound index field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot declare more than one special key kind"); + } MongoIndexDirection direction = fieldAnnotation.enumValue(ATTR_DIRECTION, MongoIndexDirection.class).orElse(MongoIndexDirection.ASC); if (geo) { + hasGeoField = true; if (direction != MongoIndexDirection.ASC) { throw new IllegalStateException("Mongo compound geospatial field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot define a numeric direction"); } MongoGeoIndexType geoType = fieldAnnotation.enumValue(ATTR_GEO_TYPE, MongoGeoIndexType.class).orElse(MongoGeoIndexType.GEO_2DSPHERE); + validateGeoIndexedType(entity, persistedPath, propertyType, geoType); Integer bits = fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0 ? fieldAnnotation.intValue(ATTR_BITS).getAsInt() : null; + Integer sphereVersion = fieldAnnotation.intValue(ATTR_SPHERE_VERSION).isPresent() && fieldAnnotation.intValue(ATTR_SPHERE_VERSION).getAsInt() >= 0 ? fieldAnnotation.intValue(ATTR_SPHERE_VERSION).getAsInt() : null; Double min = fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble()) ? fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble() : null; Double max = fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()) ? fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble() : null; if (geoType != MongoGeoIndexType.GEO_2D && (bits != null || min != null || max != null)) { throw new IllegalStateException("2d-specific geospatial options are only supported for Mongo 2d compound geospatial fields on entity [" + entity.getName() + "]"); } + if (geoType != MongoGeoIndexType.GEO_2DSPHERE && sphereVersion != null) { + throw new IllegalStateException("2dsphere-specific geospatial options are only supported for Mongo 2dsphere compound geospatial fields on entity [" + entity.getName() + "]"); + } if (bits != null) { if (indexBits != null && !indexBits.equals(bits)) { throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] declares conflicting bits options for geospatial fields"); @@ -486,12 +551,49 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit } indexMax = max; } + if (sphereVersion != null) { + if (indexSphereVersion != null && !indexSphereVersion.equals(sphereVersion)) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] declares conflicting sphereVersion options for geospatial fields"); + } + indexSphereVersion = sphereVersion; + } fields.add(new ResolvedIndexField(persistedPath, null, null, geoType.getKey(), min, max)); + } else if (text) { + hasTextField = true; + if (direction != MongoIndexDirection.ASC) { + throw new IllegalStateException("Mongo compound text field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot define a numeric direction"); + } + if ((fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0) + || (fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble())) + || (fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()))) { + throw new IllegalStateException("2d-specific geospatial options are not supported for Mongo compound text field [" + persistedPath + "] on entity [" + entity.getName() + "]"); + } + int weight = fieldAnnotation.intValue("weight").orElse(1); + if (weight <= 0) { + throw new IllegalStateException("Mongo text index weight must be greater than zero for entity [" + entity.getName() + "]"); + } + if (firstTextFieldIndex == -1) { + firstTextFieldIndex = fields.size(); + } + lastTextFieldIndex = fields.size(); + fields.add(new ResolvedIndexField(persistedPath, null, weight, "text", null, null)); + } else if (hashed) { + hasHashedField = true; + if (direction != MongoIndexDirection.ASC) { + throw new IllegalStateException("Mongo compound hashed field [" + persistedPath + "] on entity [" + entity.getName() + "] cannot define a numeric direction"); + } + if ((fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0) + || (fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble())) + || (fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()))) { + throw new IllegalStateException("2d-specific geospatial options are not supported for Mongo compound hashed field [" + persistedPath + "] on entity [" + entity.getName() + "]"); + } + fields.add(new ResolvedIndexField(persistedPath, null, null, "hashed", null, null)); } else { if ((fieldAnnotation.intValue(ATTR_BITS).isPresent() && fieldAnnotation.intValue(ATTR_BITS).getAsInt() >= 0) + || (fieldAnnotation.intValue(ATTR_SPHERE_VERSION).isPresent() && fieldAnnotation.intValue(ATTR_SPHERE_VERSION).getAsInt() >= 0) || (fieldAnnotation.doubleValue(ATTR_MIN).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MIN).getAsDouble())) || (fieldAnnotation.doubleValue(ATTR_MAX).isPresent() && !Double.isNaN(fieldAnnotation.doubleValue(ATTR_MAX).getAsDouble()))) { - throw new IllegalStateException("2d-specific geospatial options require geo=true for Mongo compound index field [" + persistedPath + "] on entity [" + entity.getName() + "]"); + throw new IllegalStateException("Geospatial field-specific options require geo=true for Mongo compound index field [" + persistedPath + "] on entity [" + entity.getName() + "]"); } fields.add(new ResolvedIndexField(persistedPath, direction == MongoIndexDirection.DESC ? -1 : 1, null, null, null, null)); } @@ -499,6 +601,49 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (fields.isEmpty()) { throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] must declare at least one field"); } + long geospatialFieldCount = fields.stream().filter(field -> "2d".equals(field.kind()) || "2dsphere".equals(field.kind())).count(); + long geo2dFieldCount = fields.stream().filter(field -> "2d".equals(field.kind())).count(); + if (geo2dFieldCount > 0 && (geo2dFieldCount > 1 || geospatialFieldCount > 1 || fields.size() != 2 || !"2d".equals(fields.get(0).kind()))) { + throw new IllegalStateException("Mongo compound 2d geospatial index on entity [" + entity.getName() + "] must declare exactly two fields with the 2d field first"); + } + String defaultLanguage = annotationValue.stringValue(ATTR_DEFAULT_LANGUAGE).filter(s -> !s.isEmpty()).orElse(null); + String languageOverride = annotationValue.stringValue(ATTR_LANGUAGE_OVERRIDE).filter(s -> !s.isEmpty()).orElse(null); + Integer textIndexVersion = annotationValue.intValue(ATTR_TEXT_INDEX_VERSION).isPresent() && annotationValue.intValue(ATTR_TEXT_INDEX_VERSION).getAsInt() >= 0 + ? annotationValue.intValue(ATTR_TEXT_INDEX_VERSION).getAsInt() + : null; + if (hasTextField) { + if (hasGeoField || hasHashedField) { + throw new IllegalStateException("Mongo compound text index on entity [" + entity.getName() + "] cannot mix text fields with hashed or geospatial fields"); + } + if (annotationValue.booleanValue(ATTR_UNIQUE).orElse(false)) { + throw new IllegalStateException("Mongo compound text index on entity [" + entity.getName() + "] cannot be unique"); + } + if (annotationValue.booleanValue(ATTR_SPARSE).orElse(false)) { + throw new IllegalStateException("Mongo compound text index on entity [" + entity.getName() + "] cannot explicitly define sparse=true"); + } + for (int i = firstTextFieldIndex; i <= lastTextFieldIndex; i++) { + if (!"text".equals(fields.get(i).kind())) { + throw new IllegalStateException("Mongo compound text index on entity [" + entity.getName() + "] must declare all text fields adjacently"); + } + } + if (textIndexVersion != null && textIndexVersion <= 0) { + throw new IllegalStateException("Mongo text index version must be greater than zero for entity [" + entity.getName() + "]"); + } + } else if (defaultLanguage != null || languageOverride != null || textIndexVersion != null) { + throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] can define text-specific options only when at least one field uses text=true"); + } + if (hasHashedField) { + long hashedFieldCount = fields.stream().filter(field -> "hashed".equals(field.kind())).count(); + if (hashedFieldCount > 1) { + throw new IllegalStateException("Mongo compound hashed index on entity [" + entity.getName() + "] can declare only one hashed field"); + } + if (hasGeoField) { + throw new IllegalStateException("Mongo compound hashed index on entity [" + entity.getName() + "] cannot mix hashed and geospatial fields"); + } + if (annotationValue.booleanValue(ATTR_UNIQUE).orElse(false)) { + throw new IllegalStateException("Mongo compound hashed index on entity [" + entity.getName() + "] cannot be unique"); + } + } if (annotationValue.intValue(ATTR_EXPIRE_AFTER_SECONDS).isPresent() && annotationValue.intValue(ATTR_EXPIRE_AFTER_SECONDS).getAsInt() >= 0) { throw new IllegalStateException("TTL is not supported for Mongo compound index on entity [" + entity.getName() + "]"); } @@ -507,6 +652,9 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit if (sparse && partialFilterExpression != null) { throw new IllegalStateException("Mongo compound index on entity [" + entity.getName() + "] cannot define both sparse and partialFilterExpression"); } + partialFilterExpression = parseJsonOption(partialFilterExpression, ATTR_PARTIAL_FILTER_EXPRESSION, entity.getName()); + String collation = parseJsonOption(annotationValue.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), ATTR_COLLATION, entity.getName()); + validateGeoCollation(entity, geo2dFieldCount > 0 ? "compound 2d geospatial index" : "geospatial index", geo2dFieldCount > 0 ? MongoGeoIndexType.GEO_2D : null, collation); indexes.add(new ResolvedIndex( annotationValue.stringValue(ATTR_NAME).filter(s -> !s.isEmpty()).orElse(null), List.copyOf(fields), @@ -515,14 +663,14 @@ private static List resolveCompoundIndexes(RuntimePersistentEntit annotationValue.booleanValue(ATTR_HIDDEN).orElse(false), null, partialFilterExpression, - annotationValue.stringValue(ATTR_COLLATION).filter(s -> !s.isEmpty()).orElse(null), + collation, indexBits, indexMin, indexMax, - null, - null, - null, - null, + defaultLanguage, + languageOverride, + textIndexVersion, + indexSphereVersion, null, parseJsonOption(annotationValue.stringValue(ATTR_STORAGE_ENGINE).filter(s -> !s.isEmpty()).orElse(null), ATTR_STORAGE_ENGINE, entity.getName()), annotationValue.stringValue(ATTR_COMMENT).filter(s -> !s.isEmpty()).orElse(null), @@ -586,6 +734,7 @@ private static final class TextIndexState { private @Nullable String defaultLanguage; private @Nullable String languageOverride; private @Nullable Integer textIndexVersion; + private @Nullable String commitQuorum; } private record WildcardIndexSignature(List fields, diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java index fa24cb8760b..0c333c5c74c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreator.java @@ -19,6 +19,9 @@ import com.mongodb.MongoCommandException; import com.mongodb.MongoWriteException; import com.mongodb.client.model.Collation; +import com.mongodb.client.model.CollationAlternate; +import com.mongodb.client.model.CollationCaseFirst; +import com.mongodb.client.model.CollationMaxVariable; import com.mongodb.client.model.CollationStrength; import com.mongodb.client.model.ClusteredIndexOptions; import com.mongodb.client.model.CreateCollectionOptions; @@ -61,6 +64,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -413,13 +417,27 @@ static CreateCollectionOptions toCreateCollectionOptions(MongoResolvedCollection static List toResolvedIndexFields(Document indexDocument, Document keyDocument) { if ("text".equals(keyDocument.getString("_fts"))) { Document weights = indexDocument.get("weights", Document.class); - if (weights != null && !weights.isEmpty()) { - List fields = new ArrayList<>(weights.size()); - for (Map.Entry entry : weights.entrySet()) { - fields.add(new MongoResolvedIndexField(entry.getKey(), null, toInteger(entry.getValue()), "text", null, null)); + List fields = new ArrayList<>(); + for (Map.Entry entry : keyDocument.entrySet()) { + if ("_fts".equals(entry.getKey())) { + if (weights != null && !weights.isEmpty()) { + for (Map.Entry weightEntry : weights.entrySet()) { + fields.add(new MongoResolvedIndexField(weightEntry.getKey(), null, toInteger(weightEntry.getValue()), "text", null, null)); + } + } + continue; + } + if ("_ftsx".equals(entry.getKey())) { + continue; + } + Object value = entry.getValue(); + if (value instanceof Number number) { + fields.add(new MongoResolvedIndexField(entry.getKey(), number.intValue(), null, null, null, null)); + } else { + fields.add(new MongoResolvedIndexField(entry.getKey(), null, null, value.toString(), null, null)); } - return fields; } + return fields; } List fields = new ArrayList<>(keyDocument.size()); for (Map.Entry entry : keyDocument.entrySet()) { @@ -522,7 +540,7 @@ static Document toIndexCommandDocument(MongoResolvedIndex index) { indexDocument.append("partialFilterExpression", Document.parse(index.partialFilterExpression())); } if (index.collation() != null) { - indexDocument.append("collation", Document.parse(index.collation())); + indexDocument.append("collation", toCollationDocument(Document.parse(index.collation()))); } if (index.bits() != null) { indexDocument.append("bits", index.bits()); @@ -568,17 +586,81 @@ static Collation toCollation(Document document) { if (locale != null) { builder.locale(locale); } - Integer strength = document.getInteger("strength"); - if (strength != null) { - builder.collationStrength(CollationStrength.fromInt(strength)); + Object strength = document.get("strength"); + if (strength instanceof Number number) { + builder.collationStrength(CollationStrength.fromInt(number.intValue())); + } else if (strength instanceof String strengthName) { + builder.collationStrength(CollationStrength.valueOf(normalizeCollationEnumName(strengthName))); } Boolean caseLevel = document.getBoolean("caseLevel"); if (caseLevel != null) { builder.caseLevel(caseLevel); } + String caseFirst = document.getString("caseFirst"); + if (caseFirst != null) { + builder.collationCaseFirst(CollationCaseFirst.valueOf(normalizeCollationEnumName(caseFirst))); + } + Boolean numericOrdering = document.getBoolean("numericOrdering"); + if (numericOrdering != null) { + builder.numericOrdering(numericOrdering); + } + String alternate = document.getString("alternate"); + if (alternate != null) { + builder.collationAlternate(CollationAlternate.valueOf(normalizeCollationEnumName(alternate))); + } + String maxVariable = document.getString("maxVariable"); + if (maxVariable != null) { + builder.collationMaxVariable(CollationMaxVariable.valueOf(normalizeCollationEnumName(maxVariable))); + } + Boolean normalization = document.getBoolean("normalization"); + if (normalization != null) { + builder.normalization(normalization); + } + Boolean backwards = document.getBoolean("backwards"); + if (backwards != null) { + builder.backwards(backwards); + } return builder.build(); } + static Document toCollationDocument(Document document) { + Document collation = new Document(document); + Object strength = document.get("strength"); + if (strength instanceof String strengthName) { + collation.put("strength", toCollationStrengthValue(strengthName)); + } + normalizeCollationStringOption(collation, "caseFirst"); + normalizeCollationStringOption(collation, "alternate"); + normalizeCollationStringOption(collation, "maxVariable"); + return collation; + } + + private static String normalizeCollationEnumName(String value) { + return value.replace('-', '_').toUpperCase(Locale.ROOT); + } + + private static int toCollationStrengthValue(String value) { + return switch (CollationStrength.valueOf(normalizeCollationEnumName(value))) { + case PRIMARY -> 1; + case SECONDARY -> 2; + case TERTIARY -> 3; + case QUATERNARY -> 4; + case IDENTICAL -> 5; + }; + } + + private static void normalizeCollationStringOption(Document collation, String optionName) { + String value = collation.getString(optionName); + if (value == null) { + return; + } + collation.put(optionName, normalizeCollationServerValue(value)); + } + + private static String normalizeCollationServerValue(String value) { + return normalizeCollationEnumName(value).toLowerCase(Locale.ROOT).replace('_', '-'); + } + static @Nullable String normalizeJsonValue(@Nullable Object value) { return switch (value) { case null -> null; diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy index 23b619ba996..7df4153cb86 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/collation/MongoCollationIndexCreationSpec.groovy @@ -56,6 +56,33 @@ class MongoCollationIndexCreationSpec extends Specification implements MongoTest assert ((Document) index.collation).getString('locale') == 'en' } } + + void 'creates field index with extended collation through raw command path'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'extended_collation_indexed_entities') + assert indexes*.name.contains('extended_collation_name_idx') + def index = indexes.find { it.name == 'extended_collation_name_idx' } + assert index.fields.size() == 1 + assert index.fields[0].path() == 'name' + assert index.fields[0].order() == 1 + assert index.collation != null + Document collation = (Document) index.collation + assert collation.getString('locale') == 'en' + assert collation.getInteger('strength') == 2 + assert collation.getBoolean('caseLevel') + assert collation.getString('caseFirst') == 'upper' + assert collation.getBoolean('numericOrdering') + assert collation.getString('alternate') == 'shifted' + assert collation.getString('maxVariable') == 'space' + assert collation.getBoolean('normalization') + assert collation.getBoolean('backwards') + } + } } @MappedEntity('collation_indexed_entities') @@ -67,3 +94,17 @@ class CollationIndexedEntity { @MongoIndexed(name = 'collation_name_idx', collation = '{ "locale": "en", "strength": 2 }') String name } + +@MappedEntity('extended_collation_indexed_entities') +class ExtendedCollationIndexedEntity { + @Id + @GeneratedValue + String id + + @MongoIndexed( + name = 'extended_collation_name_idx', + comment = 'force-raw-command-path', + collation = '{ "locale": "en", "strength": "secondary", "caseLevel": true, "caseFirst": "upper", "numericOrdering": true, "alternate": "shifted", "maxVariable": "space", "normalization": true, "backwards": true }' + ) + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/hashed/MongoCompoundHashedIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/hashed/MongoCompoundHashedIndexCreationSpec.groovy new file mode 100644 index 00000000000..bad38e06f63 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/hashed/MongoCompoundHashedIndexCreationSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.data.mongodb.index.compound.hashed + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundHashedIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.compound.hashed'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates declared compound hashed index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_hashed_indexed_entities') + assert indexes*.name.contains('tenant_account_hash_idx') + def index = indexes.find { it.name == 'tenant_account_hash_idx' } + assert index.fields.size() == 2 + assert index.fields[0].path() == 'tenant_id' + assert index.fields[0].order() == 1 + assert index.fields[1].path() == 'account_id' + assert index.fields[1].kind() == 'hashed' + } + } +} + +@MongoCompoundIndex( + name = 'tenant_account_hash_idx', + fields = [ + @MongoCompoundIndexField(value = 'tenantId', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'accountId', hashed = true) + ] +) +@MappedEntity('compound_hashed_indexed_entities') +class CompoundHashedIndexedEntity { + @Id + @GeneratedValue + String id + + String tenantId + + String accountId +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/text/MongoCompoundTextIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/text/MongoCompoundTextIndexCreationSpec.groovy new file mode 100644 index 00000000000..56544f014d9 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/compound/text/MongoCompoundTextIndexCreationSpec.groovy @@ -0,0 +1,89 @@ +package io.micronaut.data.mongodb.index.compound.text + +import com.mongodb.client.MongoClient +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoIndexInspector +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class MongoCompoundTextIndexCreationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.compound.text'] + } + + Class expectedCollectionsCreatorBeanType() { + io.micronaut.data.mongodb.init.MongoCollectionsCreator + } + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + @Shared + MongoClient mongoClient = applicationContext.getBean(MongoClient) + + void 'creates declared compound text index'() { + given: + def conditions = new PollingConditions(timeout: 10, delay: 0.25) + + expect: + applicationContext.containsBean(expectedCollectionsCreatorBeanType()) + conditions.eventually { + def indexes = MongoIndexInspector.listNormalizedIndexes(mongoClient, 'test', 'compound_text_indexed_entities') + assert indexes*.name.contains('tenant_search_idx') + def index = indexes.find { it.name == 'tenant_search_idx' } + assert index.fields[0].path() == 'tenant_id' + assert index.fields[0].order() == 1 + assert index.fields*.path().contains('_fts') + assert index.fields*.path().contains('_ftsx') + assert index.fields[-1].path() == 'created_at' + assert index.fields[-1].order() == -1 + assert index.weights.title == 2 + assert index.weights.description == 5 + assert index.defaultLanguage == 'english' + assert index.languageOverride == 'docLang' + assert index.textIndexVersion == 3 + } + } +} + +@MongoCompoundIndex( + name = 'tenant_search_idx', + defaultLanguage = 'english', + languageOverride = 'docLang', + textIndexVersion = 3, + fields = [ + @MongoCompoundIndexField(value = 'tenantId', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'title', text = true, weight = 2), + @MongoCompoundIndexField(value = 'description', text = true, weight = 5), + @MongoCompoundIndexField(value = 'createdAt', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('compound_text_indexed_entities') +class CompoundTextIndexedEntity { + @Id + @GeneratedValue + String id + + String tenantId + + String title + + String description + + Long createdAt +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy index 72c1a4b7a0c..129e04d92b0 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/geocompound/MongoCompoundGeoIndexCreationSpec.groovy @@ -54,6 +54,7 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe assert index.fields.size() == 2 assert index.fields[0].path() == 'location' assert index.fields[0].kind() == '2dsphere' + assert index.sphereVersion == 3 assert index.fields[1].path() == 'name' assert index.fields[1].order() == 1 } @@ -63,7 +64,7 @@ class MongoCompoundGeoIndexCreationSpec extends Specification implements MongoTe @MongoCompoundIndex( name = 'geo_name_idx', fields = [ - @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE), + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE, sphereVersion = 3), @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) ] ) diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy index 6d632858903..3b8efbae5f7 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/collation/MongoCollationValidationSpec.groovy @@ -24,7 +24,8 @@ class MongoCollationValidationSpec extends Specification implements MongoTestPro then: def e = thrown(RuntimeException) - e.message.contains('JSON reader') + e.message.contains('Mongo collation for entity') + e.message.contains('must be valid JSON') } } diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy index 3d137614bf7..258b6af56c5 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/existingindexcompatibility/MongoExistingIndexCompatibilitySpec.groovy @@ -90,6 +90,31 @@ class MongoExistingIndexCompatibilitySpec extends Specification implements Mongo startupContext?.close() } + void 'starts successfully when matching compound text index already exists'() { + given: + prepareExistingIndex('existing_compound_text_index_entities', + new Document('key', new Document('tenant_id', 1).append('_fts', 'text').append('_ftsx', 1).append('created_at', -1)) + .append('name', 'existing_compound_text_idx') + .append('weights', new Document('title', 2).append('description', 5)) + .append('default_language', 'english') + .append('language_override', 'docLang') + .append('textIndexVersion', 3) + ) + + ApplicationContext startupContext = null + + when: + startupContext = ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-indexes': 'true' + ]) + + then: + noExceptionThrown() + + cleanup: + startupContext?.close() + } + protected void prepareExistingIndex(String collectionName, Document index) { ApplicationContext preContext = ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-indexes': 'false' @@ -153,3 +178,34 @@ class ExistingGeoIndexEntity { @MongoGeoIndexed(name = 'existing_geo_location_idx', sphereVersion = 3) Point location } + +@MongoRepository +interface ExistingCompoundTextIndexEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'existing_compound_text_idx', + defaultLanguage = 'english', + languageOverride = 'docLang', + textIndexVersion = 3, + fields = [ + @MongoCompoundIndexField(value = 'tenantId', direction = MongoIndexDirection.ASC), + @MongoCompoundIndexField(value = 'title', text = true, weight = 2), + @MongoCompoundIndexField(value = 'description', text = true, weight = 5), + @MongoCompoundIndexField(value = 'createdAt', direction = MongoIndexDirection.DESC) + ] +) +@MappedEntity('existing_compound_text_index_entities') +class ExistingCompoundTextIndexEntity { + @Id + @GeneratedValue + String id + + String tenantId + + String title + + String description + + Long createdAt +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundconflictingsphere/ConflictingCompoundGeoSphereVersionEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundconflictingsphere/ConflictingCompoundGeoSphereVersionEntity.groovy new file mode 100644 index 00000000000..651a3eeb8f9 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundconflictingsphere/ConflictingCompoundGeoSphereVersionEntity.groovy @@ -0,0 +1,32 @@ +package io.micronaut.data.mongodb.index.validation.geocompoundconflictingsphere + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.repository.CrudRepository + +@MongoRepository +interface ConflictingCompoundGeoSphereVersionEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'conflicting_compound_geo_sphere_version_idx', + fields = [ + @MongoCompoundIndexField(value = 'startLocation', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE, sphereVersion = 2), + @MongoCompoundIndexField(value = 'endLocation', geo = true, geoType = MongoGeoIndexType.GEO_2DSPHERE, sphereVersion = 3) + ] +) +@MappedEntity('conflicting_compound_geo_sphere_version_entities') +class ConflictingCompoundGeoSphereVersionEntity { + @Id + @GeneratedValue + String id + + Map startLocation + + Map endLocation +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy index fe35fa7d9c0..ab1bc16d46d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundopts/MongoCompoundGeoOptionsValidationSpec.groovy @@ -30,6 +30,32 @@ class MongoCompoundGeoOptionsValidationSpec extends Specification implements Mon def e = thrown(RuntimeException) e.message.contains('require geo=true') } + + void 'fails fast when sphereVersion is used on non-2dsphere compound geospatial field'() { + when: + ApplicationContext.run(getProperties() + [ + 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.geocompoundsphereversion'], + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('2dsphere-specific geospatial options are only supported for Mongo 2dsphere compound geospatial fields') + } + + void 'fails fast when compound geospatial fields define conflicting sphereVersion options'() { + when: + ApplicationContext.run(getProperties() + [ + 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.geocompoundconflictingsphere'], + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('declares conflicting sphereVersion options for geospatial fields') + } } @MongoRepository diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundsphereversion/InvalidCompoundGeoSphereVersionEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundsphereversion/InvalidCompoundGeoSphereVersionEntity.groovy new file mode 100644 index 00000000000..617210455e6 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/geocompoundsphereversion/InvalidCompoundGeoSphereVersionEntity.groovy @@ -0,0 +1,33 @@ +package io.micronaut.data.mongodb.index.validation.geocompoundsphereversion + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository + +@MongoRepository +interface InvalidCompoundGeoSphereVersionEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_compound_geo_sphere_version_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D, sphereVersion = 3), + @MongoCompoundIndexField(value = 'name', direction = MongoIndexDirection.ASC) + ] +) +@MappedEntity('invalid_compound_geo_sphere_version_entities') +class InvalidCompoundGeoSphereVersionEntity { + @Id + @GeneratedValue + String id + + Map location + + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georulesresolution/MongoGeoRulesResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georulesresolution/MongoGeoRulesResolutionSpec.groovy new file mode 100644 index 00000000000..b0e6dfabc30 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/georulesresolution/MongoGeoRulesResolutionSpec.groovy @@ -0,0 +1,209 @@ +package io.micronaut.data.mongodb.index.validation.georulesresolution + +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.model.runtime.RuntimePersistentEntity +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed +import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.common.MongoEntityIndexes +import spock.lang.Shared +import spock.lang.Specification + +class MongoGeoRulesResolutionSpec extends Specification { + + @Shared + Map, RuntimePersistentEntity> entities = [:] + + void 'resolves 2d index for list-backed legacy coordinates'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(ListBacked2dGeoEntity)).indexes + def index = indexes.find { it.name() == 'list_backed_2d_geo_idx' } + + then: + index != null + index.fields()[0].kind() == '2d' + } + + void 'resolves 2dsphere index for array-backed legacy coordinates'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(ArrayBacked2dsphereGeoEntity)).indexes + def index = indexes.find { it.name() == 'array_backed_2dsphere_geo_idx' } + + then: + index != null + index.fields()[0].kind() == '2dsphere' + } + + void 'allows simple collation for 2d index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(SimpleCollation2dGeoEntity)).indexes + def index = indexes.find { it.name() == 'simple_collation_2d_geo_idx' } + + then: + index != null + index.collation() == '{"locale": "simple"}' + } + + void 'fails when 2d index uses non-simple collation'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(NonSimpleCollation2dGeoEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('supports only collation {"locale":"simple"}') + } + + void 'fails when compound 2d index does not declare 2d field first'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(Compound2dGeoFieldNotFirstEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must declare exactly two fields with the 2d field first') + } + + void 'fails when compound 2d index declares more than two fields'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(Compound2dTooManyFieldsEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must declare exactly two fields with the 2d field first') + } + + void 'fails when compound geospatial field uses unsupported property type'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidCompoundGeoTypeEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('requires a supported MongoDB GeoJSON type') + } + + void 'fails when compound 2d index uses non-simple collation'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(NonSimpleCollationCompound2dGeoEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('supports only collation {"locale":"simple"}') + } + + void 'resolves compound 2d index for list-backed legacy coordinates'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(CompoundListBacked2dGeoEntity)).indexes + def index = indexes.find { it.name() == 'compound_list_backed_2d_geo_idx' } + + then: + index != null + index.fields()[0].kind() == '2d' + index.fields()[1].order() == 1 + } + + private RuntimePersistentEntity getRuntimePersistentEntity(Class type) { + RuntimePersistentEntity entity = entities.get(type) + if (entity == null) { + entity = new RuntimePersistentEntity(type) { + @Override + protected RuntimePersistentEntity getEntity(Class t) { + return getRuntimePersistentEntity(t) + } + } + entities.put(type, entity) + } + return entity + } +} + +@MappedEntity('list_backed_2d_geo_entities') +class ListBacked2dGeoEntity { + @MongoGeoIndexed(name = 'list_backed_2d_geo_idx', type = MongoGeoIndexType.GEO_2D) + List location +} + +@MappedEntity('array_backed_2dsphere_geo_entities') +class ArrayBacked2dsphereGeoEntity { + @MongoGeoIndexed(name = 'array_backed_2dsphere_geo_idx') + Double[] location +} + +@MappedEntity('simple_collation_2d_geo_entities') +class SimpleCollation2dGeoEntity { + @MongoGeoIndexed(name = 'simple_collation_2d_geo_idx', type = MongoGeoIndexType.GEO_2D, collation = '{ "locale": "simple" }') + List location +} + +@MappedEntity('non_simple_collation_2d_geo_entities') +class NonSimpleCollation2dGeoEntity { + @MongoGeoIndexed(name = 'non_simple_collation_2d_geo_idx', type = MongoGeoIndexType.GEO_2D, collation = '{ "locale": "en", "strength": 2 }') + List location +} + +@MongoCompoundIndex( + name = 'compound_2d_geo_field_not_first_idx', + fields = [ + @MongoCompoundIndexField('name'), + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D) + ] +) +@MappedEntity('compound_2d_geo_field_not_first_entities') +class Compound2dGeoFieldNotFirstEntity { + String name + List location +} + +@MongoCompoundIndex( + name = 'compound_2d_too_many_fields_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D), + @MongoCompoundIndexField('name'), + @MongoCompoundIndexField('region') + ] +) +@MappedEntity('compound_2d_too_many_fields_entities') +class Compound2dTooManyFieldsEntity { + List location + String name + String region +} + +@MongoCompoundIndex( + name = 'invalid_compound_geo_type_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true), + @MongoCompoundIndexField('name') + ] +) +@MappedEntity('invalid_compound_geo_type_entities') +class InvalidCompoundGeoTypeEntity { + String location + String name +} + +@MongoCompoundIndex( + name = 'non_simple_collation_compound_2d_geo_idx', + collation = '{ "locale": "en", "strength": 2 }', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D), + @MongoCompoundIndexField('name') + ] +) +@MappedEntity('non_simple_collation_compound_2d_geo_entities') +class NonSimpleCollationCompound2dGeoEntity { + List location + String name +} + +@MongoCompoundIndex( + name = 'compound_list_backed_2d_geo_idx', + fields = [ + @MongoCompoundIndexField(value = 'location', geo = true, geoType = MongoGeoIndexType.GEO_2D), + @MongoCompoundIndexField('name') + ] +) +@MappedEntity('compound_list_backed_2d_geo_entities') +class CompoundListBacked2dGeoEntity { + List location + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/hashed/MongoCompoundHashedIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/hashed/MongoCompoundHashedIndexValidationSpec.groovy new file mode 100644 index 00000000000..e3dd8b1429e --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/hashed/MongoCompoundHashedIndexValidationSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.data.mongodb.index.validation.hashed + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.repository.CrudRepository +import spock.lang.Specification + +class MongoCompoundHashedIndexValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.validation.hashed'] + } + + void 'fails fast when compound hashed index is declared unique'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true' + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('Mongo compound hashed index') + e.message.contains('cannot be unique') + } +} + +@MongoRepository +interface InvalidCompoundHashedIndexEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_unique_hashed_idx', + unique = true, + fields = [ + @MongoCompoundIndexField(value = 'tenantId'), + @MongoCompoundIndexField(value = 'accountId', hashed = true) + ] +) +@MappedEntity('invalid_compound_hashed_index_entities') +class InvalidCompoundHashedIndexEntity { + @Id + @GeneratedValue + String id + + String tenantId + + String accountId +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy index 76714c07858..f18ac795a11 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/options/MongoIndexAdvancedOptionsResolutionSpec.groovy @@ -9,9 +9,11 @@ import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexed import io.micronaut.data.mongodb.annotation.index.MongoGeoIndexType +import io.micronaut.data.mongodb.annotation.index.MongoHashedIndexed import io.micronaut.data.mongodb.annotation.index.MongoIndexed import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndex +import io.micronaut.data.mongodb.annotation.index.MongoWildcardIndexed import io.micronaut.data.mongodb.common.MongoEntityIndexes import org.bson.Document import spock.lang.Shared @@ -97,6 +99,35 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { e.message.contains('must use the same defaultLanguage option') } + void 'resolves commitQuorum for simple index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(SimpleCommitQuorumEntity)).indexes + def index = indexes.find { it.name() == 'simple_commit_quorum_idx' } + + then: + index != null + index.commitQuorum() == 'majority' + } + + void 'resolves commitQuorum for text index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(TextCommitQuorumEntity)).indexes + def index = indexes.find { it.name() == 'text_commit_quorum_idx' } + + then: + index != null + index.commitQuorum() == 'majority' + } + + void 'fails when text indexed fields define different commitQuorum options'() { + when: + MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidTextCommitQuorumEntity)) + + then: + def e = thrown(IllegalStateException) + e.message.contains('must use the same commitQuorum option') + } + void 'resolves embedded field simple index path'() { when: def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(EmbeddedFieldIndexedEntity)).indexes @@ -130,6 +161,19 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { index.sphereVersion() == 3 } + void 'resolves geo partialFilterExpression, collation, and commitQuorum'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(GeoAdvancedOptionsEntity)).indexes + def index = indexes.find { it.name() == 'geo_advanced_options_idx' } + + then: + index != null + Document.parse(index.partialFilterExpression()) == Document.parse('{ "active": true }') + Document.parse(index.collation()) == Document.parse('{ "locale": "en", "strength": 2 }') + index.commitQuorum() == 'majority' + index.sphereVersion() == 3 + } + void 'fails when 2dsphere sphereVersion is used on non-2dsphere index type'() { when: MongoEntityIndexes.create(getRuntimePersistentEntity(InvalidGeoSphereVersionOn2dEntity)) @@ -139,6 +183,26 @@ class MongoIndexAdvancedOptionsResolutionSpec extends Specification { e.message.contains('2dsphere-specific geospatial options are only supported for Mongo 2dsphere indexes') } + void 'resolves commitQuorum for hashed index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(HashedCommitQuorumEntity)).indexes + def index = indexes.find { it.name() == 'hashed_commit_quorum_idx' } + + then: + index != null + index.commitQuorum() == 'majority' + } + + void 'resolves commitQuorum for field-level wildcard index'() { + when: + def indexes = MongoEntityIndexes.create(getRuntimePersistentEntity(WildcardFieldCommitQuorumEntity)).indexes + def index = indexes.find { it.name() == 'wildcard_field_commit_quorum_idx' } + + then: + index != null + index.commitQuorum() == 'majority' + } + private RuntimePersistentEntity getRuntimePersistentEntity(Class type) { RuntimePersistentEntity entity = entities.get(type) if (entity == null) { @@ -160,6 +224,12 @@ class CommentSimpleEntity { String name } +@MappedEntity('simple_commit_quorum_entity') +class SimpleCommitQuorumEntity { + @MongoIndexed(name = 'simple_commit_quorum_idx', commitQuorum = 'majority') + String name +} + @MongoCompoundIndex( name = 'comment_commit_compound_idx', fields = [ @@ -209,6 +279,15 @@ class TextLanguageOptionsEntity { String description } +@MappedEntity('text_commit_quorum_entity') +class TextCommitQuorumEntity { + @MongoTextIndexed(name = 'text_commit_quorum_idx', commitQuorum = 'majority') + String title + + @MongoTextIndexed(name = 'text_commit_quorum_idx', commitQuorum = 'majority') + String description +} + @MappedEntity('invalid_text_default_language_entity') class InvalidTextDefaultLanguageEntity { @MongoTextIndexed(defaultLanguage = 'english') @@ -218,18 +297,51 @@ class InvalidTextDefaultLanguageEntity { String second } +@MappedEntity('invalid_text_commit_quorum_entity') +class InvalidTextCommitQuorumEntity { + @MongoTextIndexed(name = 'invalid_text_commit_quorum_idx', commitQuorum = 'majority') + String first + + @MongoTextIndexed(name = 'invalid_text_commit_quorum_idx', commitQuorum = 'votingMembers') + String second +} + @MappedEntity('geo_sphere_version_entity') class GeoSphereVersionEntity { @MongoGeoIndexed(name = 'geo_sphere_version_idx', sphereVersion = 3) Point location } +@MappedEntity('geo_advanced_options_entity') +class GeoAdvancedOptionsEntity { + @MongoGeoIndexed( + name = 'geo_advanced_options_idx', + sphereVersion = 3, + partialFilterExpression = '{ "active": true }', + collation = '{ "locale": "en", "strength": 2 }', + commitQuorum = 'majority' + ) + Point location +} + @MappedEntity('invalid_geo_sphere_version_on_2d_entity') class InvalidGeoSphereVersionOn2dEntity { @MongoGeoIndexed(name = 'invalid_geo_sphere_version_idx', type = MongoGeoIndexType.GEO_2D, sphereVersion = 3) Map location } +@MappedEntity('hashed_commit_quorum_entity') +class HashedCommitQuorumEntity { + @MongoHashedIndexed(name = 'hashed_commit_quorum_idx', commitQuorum = 'majority') + String key +} + +@MappedEntity('wildcard_field_commit_quorum_entity') +class WildcardFieldCommitQuorumEntity { + @MongoWildcardIndexed(name = 'wildcard_field_commit_quorum_idx', commitQuorum = 'majority') + Map metadata +} + @MappedEntity('embedded_field_indexed_entity') class EmbeddedFieldIndexedEntity { @Relation(Relation.Kind.EMBEDDED) diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy new file mode 100644 index 00000000000..564b2a455ad --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy @@ -0,0 +1,39 @@ +package io.micronaut.data.mongodb.index.validation.text + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.document.mongodb.MongoTestPropertyProvider +import spock.lang.Specification + +class MongoCompoundTextIndexValidationSpec extends Specification implements MongoTestPropertyProvider { + + @Override + List getPackageNames() { + ['io.micronaut.data.mongodb.index.validation.text'] + } + + void 'fails fast when compound text fields are not adjacent'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true', + 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.text.adjacency'] + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('must declare all text fields adjacently') + } + + void 'fails fast when entity declares multiple text indexes'() { + when: + ApplicationContext.run(getProperties() + [ + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.create-indexes' : 'true', + 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.multipletext'] + ]) + + then: + def e = thrown(RuntimeException) + e.message.contains('allows only one text index per collection') + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy index 1171aa24ba8..3fbd72a559c 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoTextIndexValidationSpec.groovy @@ -1,20 +1,14 @@ package io.micronaut.data.mongodb.index.validation.text import io.micronaut.context.ApplicationContext -import io.micronaut.data.annotation.GeneratedValue -import io.micronaut.data.annotation.Id -import io.micronaut.data.annotation.MappedEntity import io.micronaut.data.document.mongodb.MongoTestPropertyProvider -import io.micronaut.data.mongodb.annotation.MongoRepository -import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed -import io.micronaut.data.repository.CrudRepository import spock.lang.Specification class MongoTextIndexValidationSpec extends Specification implements MongoTestPropertyProvider { @Override List getPackageNames() { - ['io.micronaut.data.mongodb.index.validation.text'] + ['io.micronaut.data.mongodb.index.validation.textweight'] } void 'fails fast for invalid text weight'() { @@ -30,17 +24,3 @@ class MongoTextIndexValidationSpec extends Specification implements MongoTestPro e.cause.message.contains('Mongo text index weight must be greater than zero') } } - -@MongoRepository -interface InvalidTextWeightEntityRepository extends CrudRepository { -} - -@MappedEntity('invalid_text_weight_entities') -class InvalidTextWeightEntity { - @Id - @GeneratedValue - String id - - @MongoTextIndexed(name = 'invalid_text_weight_idx', weight = 0) - String name -} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/adjacency/InvalidCompoundTextAdjacencyEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/adjacency/InvalidCompoundTextAdjacencyEntity.groovy new file mode 100644 index 00000000000..81f42cefdf9 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/adjacency/InvalidCompoundTextAdjacencyEntity.groovy @@ -0,0 +1,38 @@ +package io.micronaut.data.mongodb.index.validation.text.adjacency + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoIndexDirection +import io.micronaut.data.repository.CrudRepository + +@MongoRepository +interface InvalidCompoundTextAdjacencyEntityRepository extends CrudRepository { +} + +@MongoCompoundIndex( + name = 'invalid_text_adjacency_idx', + fields = [ + @MongoCompoundIndexField(value = 'tenantId'), + @MongoCompoundIndexField(value = 'title', text = true, weight = 2), + @MongoCompoundIndexField(value = 'createdAt', direction = MongoIndexDirection.DESC), + @MongoCompoundIndexField(value = 'description', text = true, weight = 3) + ] +) +@MappedEntity('invalid_compound_text_adjacency_entities') +class InvalidCompoundTextAdjacencyEntity { + @Id + @GeneratedValue + String id + + String tenantId + + String title + + Long createdAt + + String description +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy new file mode 100644 index 00000000000..3a7a4b52b18 --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy @@ -0,0 +1,29 @@ +package io.micronaut.data.mongodb.index.validation.multipletext + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndex +import io.micronaut.data.mongodb.annotation.index.MongoCompoundIndexField +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed + +@MongoCompoundIndex( + name = 'compound_text_idx', + fields = [ + @MongoCompoundIndexField(value = 'tenantId'), + @MongoCompoundIndexField(value = 'description', text = true) + ] +) +@MappedEntity('multiple_text_index_entities') +class MultipleTextIndexEntity { + @Id + @GeneratedValue + String id + + String tenantId + + @MongoTextIndexed(name = 'field_text_idx') + String title + + String description +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textweight/InvalidTextWeightEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textweight/InvalidTextWeightEntity.groovy new file mode 100644 index 00000000000..c679de251ff --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/textweight/InvalidTextWeightEntity.groovy @@ -0,0 +1,22 @@ +package io.micronaut.data.mongodb.index.validation.textweight + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.mongodb.annotation.MongoRepository +import io.micronaut.data.mongodb.annotation.index.MongoTextIndexed +import io.micronaut.data.repository.CrudRepository + +@MongoRepository +interface InvalidTextWeightEntityRepository extends CrudRepository { +} + +@MappedEntity('invalid_text_weight_entities') +class InvalidTextWeightEntity { + @Id + @GeneratedValue + String id + + @MongoTextIndexed(name = 'invalid_text_weight_idx', weight = 0) + String name +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy index a8d6c9e993a..340395f2c92 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy @@ -1,5 +1,8 @@ package io.micronaut.data.mongodb.init +import com.mongodb.client.model.CollationAlternate +import com.mongodb.client.model.CollationCaseFirst +import com.mongodb.client.model.CollationMaxVariable import com.mongodb.client.model.CollationStrength import org.bson.Document import spock.lang.Specification @@ -154,4 +157,60 @@ class AbstractMongoCollectionsCreatorSpec extends Specification { collation.strength == CollationStrength.SECONDARY collation.caseLevel } + + void 'converts document to collation with extended options'() { + when: + def collation = AbstractMongoCollectionsCreator.toCollation(new Document('locale', 'en') + .append('strength', 'SECONDARY') + .append('caseLevel', true) + .append('caseFirst', 'UPPER') + .append('numericOrdering', true) + .append('alternate', 'SHIFTED') + .append('maxVariable', 'SPACE') + .append('normalization', true) + .append('backwards', true)) + + then: + collation.locale == 'en' + collation.strength == CollationStrength.SECONDARY + collation.caseLevel + collation.caseFirst == CollationCaseFirst.UPPER + collation.numericOrdering + collation.alternate == CollationAlternate.SHIFTED + collation.maxVariable == CollationMaxVariable.SPACE + collation.normalization + collation.backwards + } + + void 'normalizes string collation enum values'() { + when: + def collation = AbstractMongoCollectionsCreator.toCollation(new Document('locale', 'en') + .append('strength', 'secondary') + .append('caseFirst', 'off') + .append('alternate', 'non-ignorable') + .append('maxVariable', 'punct')) + + then: + collation.locale == 'en' + collation.strength == CollationStrength.SECONDARY + collation.caseFirst == CollationCaseFirst.OFF + collation.alternate == CollationAlternate.NON_IGNORABLE + collation.maxVariable == CollationMaxVariable.PUNCT + } + + void 'normalizes command collation document values'() { + when: + def collation = AbstractMongoCollectionsCreator.toCollationDocument(new Document('locale', 'en') + .append('strength', 'secondary') + .append('caseFirst', 'upper') + .append('alternate', 'non_ignorable') + .append('maxVariable', 'punct')) + + then: + collation.getString('locale') == 'en' + collation.getInteger('strength') == 2 + collation.getString('caseFirst') == 'upper' + collation.getString('alternate') == 'non-ignorable' + collation.getString('maxVariable') == 'punct' + } } diff --git a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc index 27007f64e31..ce0b3c7726b 100644 --- a/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc +++ b/src/main/docs/guide/mongo/mongoMapping/mongoIndexes.adoc @@ -17,7 +17,7 @@ If a mapped collection does not exist and collection creation is disabled, index Micronaut Data currently supports the following MongoDB index annotations: * ann:data.mongodb.annotation.index.MongoIndexed[] (single-field indexes) -* ann:data.mongodb.annotation.index.MongoCompoundIndex[] / ann:data.mongodb.annotation.index.MongoCompoundIndexes[] (compound indexes) +* ann:data.mongodb.annotation.index.MongoCompoundIndex[] / ann:data.mongodb.annotation.index.MongoCompoundIndexes[] (compound indexes, including numeric, text, hashed, and geospatial members) * ann:data.mongodb.annotation.index.MongoTextIndexed[] (text indexes, including multi-field aggregation) * ann:data.mongodb.annotation.index.MongoHashedIndexed[] (hashed indexes) * ann:data.mongodb.annotation.index.MongoGeoIndexed[] (geospatial indexes: `2d`, `2dsphere`) @@ -72,6 +72,14 @@ class OrderEntity { NOTE: TTL on compound indexes is rejected during startup validation because MongoDB TTL semantics are single-field oriented. This is fail-fast validation: the application startup fails immediately with a clear error instead of silently creating a non-expiring index definition. +Compound fields may also declare special key kinds: + +* `text = true` for compound text members +* `hashed = true` for a compound hashed member +* `geo = true` with `geoType` for compound geospatial members + +Compound geospatial members with `geoType = GEO_2DSPHERE` also support `sphereVersion`. + === Compound path examples Embedded dot-notation path: @@ -128,17 +136,29 @@ NOTE: For aggregated multi-field text indexes, these text-specific options must Use ann:data.mongodb.annotation.index.MongoHashedIndexed[] to create hashed indexes. +Hashed declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. + == Geospatial indexes Use ann:data.mongodb.annotation.index.MongoGeoIndexed[] for geospatial indexing (`2d` and `2dsphere`). +MongoDB currently exposes these two geospatial index kinds. + +Geospatial declarations also support index-build `comment`, create-index command `commitQuorum`, `storageEngine`, `partialFilterExpression`, and `collation`. + For `2dsphere` indexes, ann:data.mongodb.annotation.index.MongoGeoIndexed[] also supports `sphereVersion`. NOTE: `sphereVersion` is only valid for `2dsphere` indexes; declaring it for other geospatial kinds is rejected during startup validation (fail-fast). For `2dsphere` indexes, the supported implicit property model is MongoDB Java driver GeoJSON types such as `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, and `GeometryCollection`. -For `2d` indexes, Micronaut Data continues to allow legacy map-backed coordinate values. +For `2d` indexes, Micronaut Data accepts legacy coordinate values backed by maps, lists, and arrays. + +For `2dsphere` indexes, Micronaut Data also accepts legacy coordinate values backed by maps, lists, and arrays in addition to MongoDB GeoJSON driver types. + +NOTE: For legacy map/list/array coordinate models, Micronaut Data validates declared property type compatibility during startup, but it does not validate the runtime coordinate shape itself (for example element count or numeric contents). Malformed legacy coordinate values are still rejected by MongoDB when documents are written or when the index is built. + +NOTE: MongoDB `2d` indexes support only simple binary collation. Micronaut Data rejects non-simple `collation` definitions for `2d` indexes during startup validation (fail-fast). NOTE: ann:data.mongodb.annotation.index.MongoGeoIndexed[] validates property type compatibility during startup and fails fast for unsupported property types. @@ -146,6 +166,8 @@ NOTE: ann:data.mongodb.annotation.index.MongoGeoIndexed[] validates property typ Use ann:data.mongodb.annotation.index.MongoWildcardIndexed[] for field-level wildcard indexes. +Field-level wildcard declarations also support index-build `comment`, create-index command `commitQuorum`, and `storageEngine`. + Use ann:data.mongodb.annotation.index.MongoWildcardIndex[] for top-level wildcard indexes and `wildcardProjection`. ann:data.mongodb.annotation.index.MongoWildcardIndex[] is repeatable. Multiple equivalent top-level declarations for the same `$**` key and `wildcardProjection` signature are merged into a single managed index definition. From 42a250f1f93adb528e47581f4c3bbdf92ceaec94 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 16 Apr 2026 14:35:52 +0200 Subject: [PATCH 33/34] Address CR comments, use condition for collection/index creation. --- .../mongodb/conf/MongoDataConfiguration.java | 14 +++++ .../mongodb/init/MongoCollectionsCreator.java | 2 +- .../init/MongoReactiveCollectionsCreator.java | 2 +- ...ongoCompoundTextIndexValidationSpec.groovy | 2 +- .../multiple/MultipleTextIndexEntity.groovy | 2 +- ...AbstractMongoCollectionsCreatorSpec.groovy | 57 +++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java index 5a96d63dbfd..f0d1af5e0ea 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/MongoDataConfiguration.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.condition.Condition; import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.ReflectiveAccess; import org.jspecify.annotations.NullUnmarked; @@ -147,4 +148,17 @@ public boolean matches(ConditionContext context) { } } + /** + * Collection initialization enabled condition. + */ + @Internal + public static final class CollectionInitializationEnabledCondition implements Condition { + + @Override + public boolean matches(ConditionContext context) { + MongoDataConfiguration configuration = context.getBean(MongoDataConfiguration.class); + return configuration.isCreateCollections() || configuration.isCreateIndexes(); + } + } + } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java index 01da955a92f..9b4de57b88b 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoCollectionsCreator.java @@ -51,7 +51,7 @@ @Context @Internal @RequiresSyncMongo -@Requires(property = MongoDataConfiguration.PREFIX) +@Requires(condition = MongoDataConfiguration.CollectionInitializationEnabledCondition.class) public final class MongoCollectionsCreator extends AbstractMongoCollectionsCreator { @PostConstruct diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java index 5aba46cb73a..9bd09a7b9db 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/init/MongoReactiveCollectionsCreator.java @@ -53,7 +53,7 @@ @Context @Internal @RequiresReactiveMongo -@Requires(property = MongoDataConfiguration.PREFIX) +@Requires(condition = MongoDataConfiguration.CollectionInitializationEnabledCondition.class) public final class MongoReactiveCollectionsCreator extends AbstractMongoCollectionsCreator { @PostConstruct diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy index 564b2a455ad..c5fcf97c96d 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/MongoCompoundTextIndexValidationSpec.groovy @@ -29,7 +29,7 @@ class MongoCompoundTextIndexValidationSpec extends Specification implements Mong ApplicationContext.run(getProperties() + [ 'micronaut.data.mongodb.create-collections': 'true', 'micronaut.data.mongodb.create-indexes' : 'true', - 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.multipletext'] + 'mongodb.package-names' : ['io.micronaut.data.mongodb.index.validation.text.multiple'] ]) then: diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy index 3a7a4b52b18..079bc487529 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/index/validation/text/multiple/MultipleTextIndexEntity.groovy @@ -1,4 +1,4 @@ -package io.micronaut.data.mongodb.index.validation.multipletext +package io.micronaut.data.mongodb.index.validation.text.multiple import io.micronaut.data.annotation.GeneratedValue import io.micronaut.data.annotation.Id diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy index 340395f2c92..29ea81d0102 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/mongodb/init/AbstractMongoCollectionsCreatorSpec.groovy @@ -1,5 +1,8 @@ package io.micronaut.data.mongodb.init +import io.micronaut.context.ApplicationContext +import io.micronaut.context.condition.ConditionContext +import io.micronaut.data.mongodb.conf.MongoDataConfiguration import com.mongodb.client.model.CollationAlternate import com.mongodb.client.model.CollationCaseFirst import com.mongodb.client.model.CollationMaxVariable @@ -11,6 +14,60 @@ import java.util.concurrent.TimeUnit class AbstractMongoCollectionsCreatorSpec extends Specification { + void 'collection initialization condition matches when create collections is enabled'() { + given: + def configuration = new MongoDataConfiguration() + configuration.setCreateCollections(true) + ConditionContext conditionContext = Stub() { + getBean(MongoDataConfiguration) >> configuration + } + + expect: + new MongoDataConfiguration.CollectionInitializationEnabledCondition().matches(conditionContext) + } + + void 'collection initialization condition matches when create indexes is enabled'() { + given: + def configuration = new MongoDataConfiguration() + configuration.setCreateIndexes(true) + ConditionContext conditionContext = Stub() { + getBean(MongoDataConfiguration) >> configuration + } + + expect: + new MongoDataConfiguration.CollectionInitializationEnabledCondition().matches(conditionContext) + } + + void 'collection initialization condition does not match when both flags are disabled'() { + given: + def conditionContext = Stub(ConditionContext) { + getBean(MongoDataConfiguration) >> new MongoDataConfiguration() + } + + expect: + !new MongoDataConfiguration.CollectionInitializationEnabledCondition().matches(conditionContext) + } + + void 'mongo data configuration binds create indexes from leaf property'() { + given: + ApplicationContext applicationContext = ApplicationContext.builder([ + (MongoDataConfiguration.CREATE_INDEXES_PROPERTY): 'true', + 'mongodb.package-names': ['test.no.entities'] + ]) + .build() + .start() + + when: + def configuration = applicationContext.getBean(MongoDataConfiguration) + + then: + configuration.isCreateIndexes() + !configuration.isCreateCollections() + + cleanup: + applicationContext.close() + } + void 'resolves clustered collection options from collection document'() { given: def collectionDocument = new Document('name', 'events') From a2c3152288c1f46e84817bec538094ba703ee43b Mon Sep 17 00:00:00 2001 From: radovanradic Date: Fri, 29 May 2026 16:38:54 +0200 Subject: [PATCH 34/34] Update since to 5.1.0 --- .../data/mongodb/annotation/index/MongoClusteredIndex.java | 2 +- .../data/mongodb/annotation/index/MongoCompoundIndex.java | 2 +- .../data/mongodb/annotation/index/MongoCompoundIndexField.java | 2 +- .../data/mongodb/annotation/index/MongoCompoundIndexes.java | 2 +- .../data/mongodb/annotation/index/MongoGeoIndexType.java | 2 +- .../data/mongodb/annotation/index/MongoGeoIndexed.java | 2 +- .../data/mongodb/annotation/index/MongoHashedIndexed.java | 2 +- .../data/mongodb/annotation/index/MongoIndexDirection.java | 2 +- .../micronaut/data/mongodb/annotation/index/MongoIndexed.java | 2 +- .../data/mongodb/annotation/index/MongoTextIndexed.java | 2 +- .../data/mongodb/annotation/index/MongoWildcardIndex.java | 2 +- .../data/mongodb/annotation/index/MongoWildcardIndexed.java | 2 +- .../data/mongodb/annotation/index/MongoWildcardIndexes.java | 2 +- .../io/micronaut/data/mongodb/common/MongoEntityIndexes.java | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java index 2c903476343..ac9157bdb91 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoClusteredIndex.java @@ -26,7 +26,7 @@ * Declares MongoDB clustered collection options for an entity collection. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java index 5ddfe0bdd55..50a377c6788 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndex.java @@ -27,7 +27,7 @@ * Declares a compound MongoDB index for an entity. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java index b1a97e6c4db..d5aa81d2cbe 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexField.java @@ -23,7 +23,7 @@ * Declares a field within a compound MongoDB index. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java index a7926aa2b0e..5d60f9796de 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoCompoundIndexes.java @@ -26,7 +26,7 @@ * Repeatable annotation for {@link MongoCompoundIndex}. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java index 6e6e20d2987..d18c7a6689c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexType.java @@ -19,7 +19,7 @@ * Supported MongoDB geospatial index kinds. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ public enum MongoGeoIndexType { GEO_2D("2d"), diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java index 69e4b21b473..7f6c4590b1f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoGeoIndexed.java @@ -26,7 +26,7 @@ * Declares a simple MongoDB geospatial index for a property. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java index 2f387064821..0374e3bd137 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoHashedIndexed.java @@ -26,7 +26,7 @@ * Declares a simple MongoDB hashed index for a property. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java index 966da2a6b3a..79c2cf0f84f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexDirection.java @@ -19,7 +19,7 @@ * MongoDB index direction. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ public enum MongoIndexDirection { ASC, diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java index 425315271ea..295670abd29 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoIndexed.java @@ -26,7 +26,7 @@ * Declares a simple MongoDB index for a property. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java index 2db0a4d8c91..5da7b1d6a1f 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoTextIndexed.java @@ -26,7 +26,7 @@ * Declares a simple MongoDB text index for a property. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java index 285fe64cb8d..c6ed1309611 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndex.java @@ -27,7 +27,7 @@ * Declares a top-level MongoDB wildcard index for an entity. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java index 50667770fec..58c350e7f76 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexed.java @@ -26,7 +26,7 @@ * Declares a simple MongoDB wildcard index for a property. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java index 03673fc3048..cc5832eb27e 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/annotation/index/MongoWildcardIndexes.java @@ -26,7 +26,7 @@ * Repeatable annotation for {@link MongoWildcardIndex}. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java index 4f875c1addc..93ee1dc835c 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/common/MongoEntityIndexes.java @@ -54,7 +54,7 @@ * Mongo index metadata resolved at runtime. * * @author radovanradic - * @since 5.0.0 + * @since 5.1.0 */ @SuppressWarnings("java:S6541") @Internal