From ccf0a4f40b4e055bf2f253b7909bb0d3a1e122d8 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 31 Jan 2025 11:05:06 +0700 Subject: [PATCH 1/3] Jakarta Data --- ....build.internal.data-native-example.gradle | 4 +- config/checkstyle/custom-suppressions.xml | 1 + .../operations/CosmosSqlStoredQuery.java | 7 +- ...ultReactiveCosmosRepositoryOperations.java | 8 +- .../query/builder/MongoQueryBuilder2.java | 230 +- .../DefaultIdPropertyNamingStrategy.java | 3 +- .../MongoExecutorQueryMethodMatcher.java | 40 +- .../matchers/MongoRawQueryMethodMatcher.java | 4 +- .../processor/BuildMongoQuerySpec.groovy | 27 + data-hibernate-jpa/build.gradle | 7 +- ...teJakartaDataDeleteExceptionConverter.java | 58 + ...ibernateJakartaDataExceptionConverter.java | 53 + ...eJakartaDataPersistExceptionConverter.java | 54 + ...teJakartaDataUpdateExceptionConverter.java | 58 + .../AbstractHibernateOperations.java | 85 +- .../operations/HibernateJpaOperations.java | 264 +- .../hibernate/jakarta_data/entity/Box.java | 30 + .../hibernate/jakarta_data/entity/Boxes.java | 11 + .../jakarta_data/entity/Coordinate.java | 32 + .../jakarta_data/entity/EntityTests.java | 2490 +++++++++++++++++ .../entity/MultipleEntityRepo.java | 43 + .../jakarta_data/persistence/Catalog.java | 119 + .../persistence/CatalogProduct.java | 107 + .../persistence/PersistenceEntityTests.java | 400 +++ .../read/only/AsciiCharacter.java | 63 + .../read/only/AsciiCharacters.java | 97 + .../read/only/AsciiCharactersPopulator.java | 37 + .../read/only/CustomRepository.java | 28 + .../jakarta_data/read/only/IdOperations.java | 16 + .../jakarta_data/read/only/NaturalNumber.java | 79 + .../read/only/NaturalNumbers.java | 65 + .../read/only/NaturalNumbersPopulator.java | 66 + .../jakarta_data/read/only/Populator.java | 60 + .../read/only/PositiveIntegers.java | 66 + .../read/only/ReadOnlyRepository.java | 33 + .../jakarta_data/read/only/_AsciiChar.java | 31 + .../read/only/_AsciiCharacter.java | 51 + .../jakarta_data/utilities/DatabaseType.java | 30 + .../jakarta_data/utilities/TestProperty.java | 157 ++ .../utilities/TestPropertyHandler.java | 47 + .../hibernate/reactive/ReactorSpec.groovy | 2 +- ...HibernateReactiveRepositoryOperations.java | 19 +- data-jdbc/build.gradle | 3 + .../jdbc/config/DataJdbcConfiguration.java | 21 + .../DefaultJdbcRepositoryOperations.java | 53 +- .../jdbc/h2/H2CursoredPaginationSpec.groovy | 51 + .../data/jdbc/h2/H2RepositorySpec.groovy | 69 + .../data/jdbc/h2/jakarta_data/entity/Box.java | 30 + .../jdbc/h2/jakarta_data/entity/Boxes.java | 13 + .../h2/jakarta_data/entity/Coordinate.java | 32 + .../h2/jakarta_data/entity/EntityTests.java | 2456 ++++++++++++++++ .../entity/MultipleEntityRepo.java | 44 + .../h2/jakarta_data/persistence/Catalog.java | 122 + .../persistence/CatalogProduct.java | 111 + .../persistence/PersistenceEntityTests.java | 395 +++ .../read/only/AsciiCharacter.java | 63 + .../read/only/AsciiCharacters.java | 100 + .../read/only/AsciiCharactersPopulator.java | 38 + .../read/only/CustomRepository.java | 29 + .../jakarta_data/read/only/IdOperations.java | 16 + .../jakarta_data/read/only/NaturalNumber.java | 79 + .../read/only/NaturalNumbers.java | 68 + .../read/only/NaturalNumbersPopulator.java | 66 + .../h2/jakarta_data/read/only/Populator.java | 60 + .../read/only/PositiveIntegers.java | 67 + .../read/only/ReadOnlyRepository.java | 33 + .../h2/jakarta_data/read/only/_AsciiChar.java | 31 + .../read/only/_AsciiCharacter.java | 51 + .../jakarta_data/utilities/DatabaseType.java | 30 + .../jakarta_data/utilities/TestProperty.java | 157 ++ .../utilities/TestPropertyHandler.java | 47 + .../oraclexe/OracleXERepositorySpec.groovy | 1 + .../java/io/micronaut/data/annotation/By.java | 104 + .../data/annotation/ConvertException.java | 47 + .../io/micronaut/data/annotation/Delete.java | 46 + .../io/micronaut/data/annotation/Find.java | 45 + .../io/micronaut/data/annotation/Insert.java | 45 + .../io/micronaut/data/annotation/OrderBy.java | 121 + .../io/micronaut/data/annotation/Save.java | 69 + .../micronaut/data/annotation/TypeRole.java | 5 + .../io/micronaut/data/annotation/Update.java | 45 + .../data/exceptions/ExceptionConverter.java | 38 + .../data/intercept/FindAllInterceptor.java | 2 +- .../intercept/annotation/DataMethodQuery.java | 15 + .../io/micronaut/data/model/CursoredPage.java | 2 +- .../data/model/DefaultCursoredPage.java | 4 +- .../io/micronaut/data/model/DefaultPage.java | 10 +- .../java/io/micronaut/data/model/Limit.java | 73 + .../java/io/micronaut/data/model/Page.java | 9 +- .../io/micronaut/data/model/Pageable.java | 55 +- .../data/model/PersistentProperty.java | 75 +- .../java/io/micronaut/data/model/Sort.java | 16 +- ...ersistentEntityCommonAbstractCriteria.java | 3 + .../PersistentEntityCriteriaBuilder.java | 11 + .../impl/AbstractCriteriaBuilder.java | 90 +- ...bstractPersistentEntityCriteriaDelete.java | 5 + ...AbstractPersistentEntityCriteriaQuery.java | 2 +- ...bstractPersistentEntityCriteriaUpdate.java | 5 + .../impl/AbstractPersistentEntityQuery.java | 15 +- .../impl/BoundPathParameterExpression.java | 45 + .../jpa/criteria/impl/CriteriaUtils.java | 8 +- ...ntPropertyOrder.java => DefaultOrder.java} | 19 +- .../impl/LegacyQueryModelQueryBuilder.java | 20 +- ...ryResultPersistentEntityCriteriaQuery.java | 2 +- .../impl/expression/BinaryExpressionType.java | 6 +- .../impl/expression/UnaryExpressionType.java | 4 +- .../data/model/query/BindingContextImpl.java | 12 + .../data/model/query/BindingParameter.java | 16 + .../model/query/builder/QueryBuilder2.java | 29 +- .../data/model/query/builder/QueryResult.java | 72 + .../query/builder/jpa/JpaQueryBuilder2.java | 27 +- .../sql/AbstractSqlLikeQueryBuilder2.java | 582 ++-- .../query/builder/sql/SqlQueryBuilder2.java | 55 +- .../query/impl/AdvancedPredicateVisitor.java | 4 +- .../data/model/runtime/PagedQuery.java | 20 + .../model/runtime/PreparedDataOperation.java | 12 + .../data/model/runtime/PreparedQuery.java | 21 +- .../runtime/RuntimePersistentProperty.java | 23 + .../data/model/runtime/StoredQuery.java | 28 +- data-mongodb/build.gradle | 1 + .../MongoJakartaDataExceptionConverter.java | 51 + .../AbstractMongoRepositoryOperations.java | 61 +- .../operations/DefaultMongoPreparedQuery.java | 123 +- .../DefaultMongoRepositoryOperations.java | 83 +- .../operations/DefaultMongoStoredQuery.java | 134 +- ...aultReactiveMongoRepositoryOperations.java | 10 + .../document/mongodb/MongoCriteriaSpec.groovy | 4 +- .../MongoDocumentRepositorySpec.groovy | 14 + .../repositories/MongoPersonRepository.java | 4 + data-mongodb/src/test/resources/logback.xml | 2 + data-processor/build.gradle | 10 + data-processor/src/main/antlr/JDQL.g4 | 163 ++ .../jdql/JDQLCriteriaBuilderUtils.java | 583 ++++ .../jakarta/data/JakartaDataByMapper.java | 49 + .../jakarta/data/JakartaDataDeleteMapper.java | 54 + .../jakarta/data/JakartaDataFindMapper.java | 54 + .../jakarta/data/JakartaDataInsertMapper.java | 54 + .../data/JakartaDataOrderByListMapper.java | 56 + .../data/JakartaDataOrderByMapper.java | 49 + .../jakarta/data/JakartaDataParamsMapper.java | 49 + .../data/JakartaDataRepositoryMapper.java | 54 + .../jakarta/data/JakartaDataSaveMapper.java | 54 + .../jakarta/data/JakartaDataUpdateMapper.java | 54 + .../model/SourcePersistentProperty.java | 30 + ...SourcePersistentEntityCriteriaBuilder.java | 36 +- ...cePersistentEntityCriteriaBuilderImpl.java | 21 +- .../criteria/impl/QueryResultAnalyzer.java | 1 + .../impl/SourceParameterExpressionImpl.java | 25 +- ...cePersistentEntityCriteriaBuilderImpl.java | 27 +- .../visitors/MethodMatchContext.java | 51 +- .../RepositoryTypeElementVisitor.java | 265 +- .../finders/AbstractCriteriaMethodMatch.java | 26 +- .../finders/AbstractMethodMatcher.java | 1 + .../visitors/finders/CountMethodMatcher.java | 1 + .../visitors/finders/FindMethodMatcher.java | 4 + .../visitors/finders/FindersUtils.java | 2 + .../visitors/finders/MethodMatchInfo.java | 16 +- .../visitors/finders/QueryMatchId.java | 1 + .../finders/RawQueryMethodMatcher.java | 6 +- .../visitors/finders/Restrictions.java | 99 + .../visitors/finders/SaveMethodMatcher.java | 6 +- .../processor/visitors/finders/TypeUtils.java | 24 +- .../visitors/finders/UpdateMethodMatcher.java | 2 +- .../DeleteAnnotatedMethodMatcher.java | 93 + .../annotated/FindAnnotatedMethodMatcher.java | 51 + .../InsertAnnotatedMethodMatcher.java | 61 + ...akartaDataQueryAnnotatedMethodMatcher.java | 246 ++ .../annotated/SaveAnnotatedMethodMatcher.java | 61 + .../UpdateAnnotatedMethodMatcher.java | 61 + .../criteria/QueryCriteriaMethodMatch.java | 87 +- ...a.processor.visitors.finders.MethodMatcher | 7 +- ...cronaut.inject.annotation.AnnotationMapper | 11 + .../model/jpa/criteria/CriteriaSpec.groovy | 2 +- ...JakartaDataQueryLanguageBuilderSpec.groovy | 391 +++ .../data/processor/sql/BuildQuerySpec.groovy | 58 +- .../data/processor/visitors/TestUtils.groovy | 13 + .../DefaultR2dbcRepositoryOperations.java | 28 + data-runtime/build.gradle | 3 +- .../convert/DataConversionServiceFactory.java | 3 + .../convert/JakartaDataConverters.java | 144 + ...nautDataJakartaDataExceptionConverter.java | 65 + .../intercept/AbstractQueryInterceptor.java | 47 +- .../intercept/DataInterceptorResolver.java | 30 +- .../DefaultAbstractFindPageInterceptor.java | 52 +- .../intercept/DefaultFindAllInterceptor.java | 30 +- .../DefaultFindCursoredPageInterceptor.java | 16 - .../intercept/DefaultUpdateInterceptor.java | 2 +- ...efaultBindableParametersPreparedQuery.java | 23 +- .../DefaultBindableParametersStoredQuery.java | 27 +- .../internal/query/DummyPreparedQuery.java | 12 + .../sql/AbstractSqlRepositoryOperations.java | 12 +- .../internal/sql/DefaultSqlPreparedQuery.java | 105 +- .../internal/sql/DefaultSqlStoredQuery.java | 9 +- .../internal/sql/SqlPreparedQuery.java | 24 +- .../internal/sql/SqlStoredQuery.java | 7 + .../query/DefaultPreparedQueryResolver.java | 4 + .../query/internal/DefaultPreparedQuery.java | 165 +- .../query/internal/DefaultStoredQuery.java | 26 +- .../query/internal/DelegatePreparedQuery.java | 23 + .../query/internal/DelegateStoredQuery.java | 12 + .../JakartaDataDeleteExceptionConverter.java | 29 + .../data/JakartaDataExceptionConverter.java | 29 + .../JakartaDataInsertExceptionConverter.java | 29 + .../JakartaDataUpdateExceptionConverter.java | 29 + .../data/tck/tests/AbstractPageSpec.groovy | 14 +- .../tck/tests/AbstractRepositorySpec.groovy | 11 +- .../data/tck/repositories/BookRepository.java | 13 + gradle/libs.versions.toml | 13 + jakarta-data-tck/hibernate/build.gradle | 29 + .../micronaut/data/jakarta/tck/DummyBean.java | 29 + .../data/jakarta/tck/FilterExtension.java | 37 + .../tck/HibernateJakartaDataTCKSuite.java | 27 + .../org.junit.jupiter.api.extension.Extension | 1 + .../src/test/resources/application.yml | 20 + .../hibernate/src/test/resources/logback.xml | 20 + jakarta-data-tck/jdbc/build.gradle | 27 + .../data/jakarta/tck/FilterExtension.java | 41 + .../tck/MicronautJdbcDataTCKSuite.java | 27 + .../org.junit.jupiter.api.extension.Extension | 1 + .../jdbc/src/test/resources/application.yml | 14 + .../src/test/resources/aprocessor.properties | 1 + .../jdbc/src/test/resources/logback.xml | 20 + jakarta-data-tck/mongodb/build.gradle | 42 + .../data/jakarta/tck/FilterExtension.java | 43 + .../tck/MongoDBJakartaDataTCKSuite.java | 28 + .../org.junit.jupiter.api.extension.Extension | 1 + .../src/test/resources/application.yml | 9 + .../src/test/resources/aprocessor.properties | 1 + .../mongodb/src/test/resources/logback.xml | 20 + jakarta-data-tck/support/build.gradle | 32 + .../tck/ArchiveCompilationException.java | 31 + .../validation/tck/ArchiveCompiler.java | 188 ++ .../tck/ArchiveCompilerException.java | 34 + .../validation/tck/DeploymentClassLoader.java | 90 + .../validation/tck/DeploymentDir.java | 38 + .../validation/tck/TCKArchiveProcessor.java | 38 + .../tck/TckContainerConfiguration.java | 27 + .../tck/TckDeployableContainer.java | 214 ++ .../validation/tck/TckExtension.java | 41 + .../micronaut/validation/tck/TckObserver.java | 61 + .../micronaut/validation/tck/TckProtocol.java | 153 + .../tck/runtime/TestClassVisitor.java | 127 + ...icronaut.inject.visitor.TypeElementVisitor | 1 + ...boss.arquillian.core.spi.LoadableExtension | 1 + settings.gradle | 5 + 245 files changed, 16436 insertions(+), 1055 deletions(-) create mode 100644 data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataDeleteExceptionConverter.java create mode 100644 data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataExceptionConverter.java create mode 100644 data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataPersistExceptionConverter.java create mode 100644 data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataUpdateExceptionConverter.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Box.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Boxes.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Coordinate.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/EntityTests.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/MultipleEntityRepo.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/Catalog.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/CatalogProduct.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/PersistenceEntityTests.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacter.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacters.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharactersPopulator.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/CustomRepository.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/IdOperations.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumber.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbers.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbersPopulator.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/Populator.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/PositiveIntegers.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/ReadOnlyRepository.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiChar.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiCharacter.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/DatabaseType.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestProperty.java create mode 100644 data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestPropertyHandler.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Box.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Boxes.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Coordinate.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/EntityTests.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/MultipleEntityRepo.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/Catalog.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/CatalogProduct.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/PersistenceEntityTests.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacter.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacters.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharactersPopulator.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/CustomRepository.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/IdOperations.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumber.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbers.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbersPopulator.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/Populator.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/PositiveIntegers.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/ReadOnlyRepository.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiChar.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiCharacter.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/DatabaseType.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestProperty.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestPropertyHandler.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/By.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/ConvertException.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/Delete.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/Find.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/Insert.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/OrderBy.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/Save.java create mode 100644 data-model/src/main/java/io/micronaut/data/annotation/Update.java create mode 100644 data-model/src/main/java/io/micronaut/data/exceptions/ExceptionConverter.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/Limit.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/BoundPathParameterExpression.java rename data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/{PersistentPropertyOrder.java => DefaultOrder.java} (71%) create mode 100644 data-mongodb/src/main/java/io/micronaut/data/mongodb/exceptions/jakarta/data/MongoJakartaDataExceptionConverter.java create mode 100644 data-processor/src/main/antlr/JDQL.g4 create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/jdql/JDQLCriteriaBuilderUtils.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataByMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataDeleteMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataFindMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataInsertMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByListMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataParamsMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataRepositoryMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataSaveMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataUpdateMapper.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/DeleteAnnotatedMethodMatcher.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/FindAnnotatedMethodMatcher.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/InsertAnnotatedMethodMatcher.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/JakartaDataQueryAnnotatedMethodMatcher.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/SaveAnnotatedMethodMatcher.java create mode 100644 data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/UpdateAnnotatedMethodMatcher.java create mode 100644 data-processor/src/test/groovy/io/micronaut/data/processor/jdql/JakartaDataQueryLanguageBuilderSpec.groovy create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/convert/JakartaDataConverters.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/exceptions/jakarta/data/MicronautDataJakartaDataExceptionConverter.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataDeleteExceptionConverter.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataExceptionConverter.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataInsertExceptionConverter.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataUpdateExceptionConverter.java create mode 100644 jakarta-data-tck/hibernate/build.gradle create mode 100644 jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/DummyBean.java create mode 100644 jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java create mode 100644 jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/HibernateJakartaDataTCKSuite.java create mode 100644 jakarta-data-tck/hibernate/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 jakarta-data-tck/hibernate/src/test/resources/application.yml create mode 100644 jakarta-data-tck/hibernate/src/test/resources/logback.xml create mode 100644 jakarta-data-tck/jdbc/build.gradle create mode 100644 jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java create mode 100644 jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/MicronautJdbcDataTCKSuite.java create mode 100644 jakarta-data-tck/jdbc/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 jakarta-data-tck/jdbc/src/test/resources/application.yml create mode 100644 jakarta-data-tck/jdbc/src/test/resources/aprocessor.properties create mode 100644 jakarta-data-tck/jdbc/src/test/resources/logback.xml create mode 100644 jakarta-data-tck/mongodb/build.gradle create mode 100644 jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java create mode 100644 jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/MongoDBJakartaDataTCKSuite.java create mode 100644 jakarta-data-tck/mongodb/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 jakarta-data-tck/mongodb/src/test/resources/application.yml create mode 100644 jakarta-data-tck/mongodb/src/test/resources/aprocessor.properties create mode 100644 jakarta-data-tck/mongodb/src/test/resources/logback.xml create mode 100644 jakarta-data-tck/support/build.gradle create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java create mode 100644 jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor create mode 100644 jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.data-native-example.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.data-native-example.gradle index 0c1f3f39e4..3d3653ff15 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.data-native-example.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.data-native-example.gradle @@ -33,9 +33,9 @@ graalvmNative { enabled = true } binaries { - all { - logger.quiet("GRAALVM_HOME: {}", (String) System.getenv("GRAALVM_HOME")) + configureEach { if (System.getenv("GRAALVM_HOME")?.contains("graal") && System.getenv("GRAALVM_HOME")?.contains("21")) { + logger.quiet("GRAALVM_HOME: {}", (String) System.getenv("GRAALVM_HOME")) logger.quiet("Enabling strict image heap!") buildArgs.add("--strict-image-heap") } diff --git a/config/checkstyle/custom-suppressions.xml b/config/checkstyle/custom-suppressions.xml index 0421e77d82..bd8daa6909 100644 --- a/config/checkstyle/custom-suppressions.xml +++ b/config/checkstyle/custom-suppressions.xml @@ -6,4 +6,5 @@ + diff --git a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/CosmosSqlStoredQuery.java b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/CosmosSqlStoredQuery.java index ed4a9c0a4e..3ef0a816f0 100644 --- a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/CosmosSqlStoredQuery.java +++ b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/CosmosSqlStoredQuery.java @@ -16,6 +16,7 @@ package io.micronaut.data.cosmos.operations; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder2; import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.model.runtime.StoredQuery; @@ -40,10 +41,12 @@ final class CosmosSqlStoredQuery extends DefaultSqlStoredQuery { * @param runtimePersistentEntity The persistent entity * @param queryBuilder The query builder * @param update The update statement. In this case list of properties to update via API. + * @param conversionService The conversion service */ public CosmosSqlStoredQuery(StoredQuery storedQuery, RuntimePersistentEntity runtimePersistentEntity, SqlQueryBuilder2 queryBuilder, - String update) { - super(storedQuery, runtimePersistentEntity, queryBuilder); + String update, + ConversionService conversionService) { + super(storedQuery, runtimePersistentEntity, queryBuilder, conversionService); this.update = update; } diff --git a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/DefaultReactiveCosmosRepositoryOperations.java b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/DefaultReactiveCosmosRepositoryOperations.java index 276a0a0c13..836804a7e5 100644 --- a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/DefaultReactiveCosmosRepositoryOperations.java +++ b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/operations/DefaultReactiveCosmosRepositoryOperations.java @@ -183,7 +183,7 @@ public StoredQuery decorate(MethodInvocationContext context, update = queryResultStoredQuery.getQueryResult().getUpdate(); } RuntimePersistentEntity runtimePersistentEntity = runtimeEntityRegistry.getEntity(storedQuery.getRootEntity()); - return new CosmosSqlStoredQuery<>(storedQuery, runtimePersistentEntity, defaultCosmosSqlQueryBuilder, update); + return new CosmosSqlStoredQuery<>(storedQuery, runtimePersistentEntity, defaultCosmosSqlQueryBuilder, update, getConversionService()); } @Override @@ -218,7 +218,7 @@ public Mono findOne(@NonNull Class type, Object id) { @Override public Mono exists(@NonNull PreparedQuery pq) { SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); - preparedQuery.attachPageable(preparedQuery.getPageable(), true); + preparedQuery.attachPageable(preparedQuery.getPageable(), preparedQuery.getQueryLimit(), preparedQuery.getSort(), true); preparedQuery.prepare(null); SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery)); logQuery(querySpec); @@ -235,7 +235,7 @@ public Mono exists(@NonNull PreparedQuery pq) { @NonNull public Mono findOne(@NonNull PreparedQuery pq) { SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); - preparedQuery.attachPageable(preparedQuery.getPageable(), true); + preparedQuery.attachPageable(preparedQuery.getPageable(), preparedQuery.getQueryLimit(), preparedQuery.getSort(), true); preparedQuery.prepare(null); SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery)); logQuery(querySpec); @@ -276,7 +276,7 @@ public Mono count(PagedQuery pagedQuery) { @NonNull public Flux findAll(@NonNull PreparedQuery pq) { SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); - preparedQuery.attachPageable(preparedQuery.getPageable(), false); + preparedQuery.attachPageable(preparedQuery.getPageable(), preparedQuery.getQueryLimit(), preparedQuery.getSort(), false); preparedQuery.prepare(null); boolean dtoProjection = preparedQuery.isDtoProjection(); boolean isEntity = preparedQuery.getResultDataType() == DataType.ENTITY; diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder2.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder2.java index 2645f640f6..cde9c776fc 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder2.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/MongoQueryBuilder2.java @@ -28,6 +28,7 @@ import io.micronaut.data.document.mongo.MongoAnnotations; import io.micronaut.data.exceptions.MappingException; import io.micronaut.data.model.Association; +import io.micronaut.data.model.DataType; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentEntityUtils; import io.micronaut.data.model.PersistentProperty; @@ -40,16 +41,19 @@ import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; import io.micronaut.data.model.jpa.criteria.impl.SelectionVisitor; import io.micronaut.data.model.jpa.criteria.impl.expression.BinaryExpression; +import io.micronaut.data.model.jpa.criteria.impl.expression.BinaryExpressionType; import io.micronaut.data.model.jpa.criteria.impl.expression.FunctionExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.IdExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; +import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpressionType; +import io.micronaut.data.model.jpa.criteria.impl.predicate.BetweenPredicate; 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.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.InPredicate; 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; @@ -107,6 +111,8 @@ public final class MongoQueryBuilder2 implements QueryBuilder2 { * An object with this property is replaced with an actual query parameter at the runtime. */ public static final String QUERY_PARAMETER_PLACEHOLDER = "$mn_qp"; + public static final String NEGATE = "$mn_negate"; // -vale + public static final String RECIPROCATE = "$mn_reciprocate"; // 1/value public static final String MONGO_DATE_IDENTIFIER = "$date"; public static final String MONGO_ID_FIELD = "_id"; private static final String REGEX = "$regex"; @@ -161,7 +167,14 @@ public QueryResult buildSelect(AnnotationMetadata annotationMetadata, SelectQuer Map sortObj = new LinkedHashMap<>(); orders.forEach(order -> { io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath = requireProperty(order.getExpression()); - sortObj.put(persistentPropertyPath.getPathAsString(), order.isAscending() ? 1 : -1); + PersistentProperty property = persistentPropertyPath.getProperty(); + PersistentEntity owner = property.getOwner(); + PersistentProperty identity = owner.getIdentity(); + if (identity != null && identity.equals(property)) { + sortObj.put(MONGO_ID_FIELD, order.isAscending() ? 1 : -1); + } else { + sortObj.put(persistentPropertyPath.getPathAsString(), order.isAscending() ? 1 : -1); + } }); pipeline.add(Map.of("$sort", sortObj)); } else { @@ -550,21 +563,60 @@ public QueryResult buildUpdate(AnnotationMetadata annotationMetadata, UpdateQuer Map propertiesToUpdate = updateQueryDefinition.propertiesToUpdate(); Map sets = CollectionUtils.newLinkedHashMap(propertiesToUpdate.size()); + Map incs = CollectionUtils.newLinkedHashMap(propertiesToUpdate.size()); + Map muls = CollectionUtils.newLinkedHashMap(propertiesToUpdate.size()); for (Map.Entry e : propertiesToUpdate.entrySet()) { PersistentPropertyPath propertyPath = findProperty(queryState, e.getKey()); String propertyPersistName = getPropertyPersistName(propertyPath); - if (e.getValue() instanceof BindingParameter bindingParameter) { + Object value = e.getValue(); + if (value instanceof BindingParameter bindingParameter) { int index = queryState.pushParameter( bindingParameter, newBindingContext(propertyPath) ); sets.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index)); - } else { - sets.put(propertyPersistName, e.getValue()); + } else if (value instanceof LiteralExpression literalExpression) { + sets.put(propertyPersistName, literalExpression.getValue()); + } else if (value instanceof BinaryExpression binaryExpression) { + io.micronaut.data.model.jpa.criteria.PersistentPropertyPath leftProp = requireProperty(binaryExpression.getLeft()); + if (!leftProp.getProperty().equals(propertyPath.getProperty())) { + throw new IllegalStateException("Left property path does not match property path"); + } + if (binaryExpression.getRight() instanceof BindingParameter bindingParameter) { + int index = queryState.pushParameter( + bindingParameter, + newBindingContext(propertyPath) + ); + switch (binaryExpression.getType()) { + case CONCAT -> { + } + case SUM -> + incs.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index)); + case PROD -> + muls.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index)); + case QUOT -> + muls.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index, RECIPROCATE, true)); + case DIFF -> + incs.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index, NEGATE, true)); + default -> throw new IllegalStateException("Unsupported binary expression type: " + binaryExpression.getType()); + } + continue; + } + throw new IllegalArgumentException("Unsupported expression type: " + value.getClass().getName() + " " + value); } } - String update = toJsonString(Map.of("$set", sets)); + Map> map = new LinkedHashMap<>(); + if (!sets.isEmpty()) { + map.put("$set", sets); + } + if (!incs.isEmpty()) { + map.put("$inc", incs); + } + if (!muls.isEmpty()) { + map.put("$mul", muls); + } + String update = toJsonString(map); return new QueryResult() { @@ -690,7 +742,7 @@ private void append(StringBuilder sb, Object obj) { } else if (obj instanceof Number) { sb.append(obj); } else { - sb.append('\'').append(obj).append('\''); + sb.append('\'').append(obj.toString().replace("'", "\\'")).append('\''); } } @@ -864,12 +916,53 @@ public MongoPredicateVisitor(QueryState queryState, Map query) { persistentEntity = queryState.getEntity(); } - private void appendOperatorExpression(Expression leftExpression, String op, Object value) { + private void appendOperatorExpression(Expression leftExpression, String op, Expression value) { + if (leftExpression instanceof BinaryExpression binaryExpression) { + PersistentPropertyPath propertyPath; + Expression otherExpression; + if (binaryExpression.getLeft() instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath leftProp) { + propertyPath = leftProp.getPropertyPath(); + otherExpression = binaryExpression.getRight(); + } else if (binaryExpression.getRight() instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath rightProp) { + propertyPath = rightProp.getPropertyPath(); + otherExpression = binaryExpression.getLeft(); + } else { + throw new IllegalStateException("Unsupported expression type: " + binaryExpression); + } + if (binaryExpression.getType() == BinaryExpressionType.PROD) { + query.put("$expr", Map.of( + op, + asList( + Map.of("$multiply", List.of( + "$" + propertyPath.getPath(), + valueRepresentation(queryState, propertyPath, propertyPath, otherExpression) + )), + valueRepresentation(queryState, propertyPath, propertyPath, value) + ) + )); + return; + } + throw new IllegalStateException("Unsupported binary expression type: " + binaryExpression.getType()); + } + if (leftExpression instanceof UnaryExpression unaryExpression) { + PersistentPropertyPath propertyPath = CriteriaUtils.requireProperty(unaryExpression.getExpression()).getPropertyPath(); + if (unaryExpression.getType() == UnaryExpressionType.LENGTH) { + query.put("$expr", Map.of( + op, + asList( + Map.of("$strLenCP", "$" + propertyPath.getPath()), + valueRepresentation(queryState, propertyPath, propertyPath, value) + ) + )); + return; + } + throw new IllegalStateException("Unsupported unary expression type: " + unaryExpression.getType()); + } PersistentPropertyPath propertyPath = CriteriaUtils.requireProperty(leftExpression).getPropertyPath(); appendOperatorExpression(op, value, propertyPath); } - private void appendOperatorExpression(String op, Object value, PersistentPropertyPath propertyPath) { + private void appendOperatorExpression(String op, Expression value, PersistentPropertyPath propertyPath) { if (value instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { PersistentPropertyPath p2 = getRequiredProperty(persistentPropertyPath); query.put("$expr", Map.of( @@ -961,6 +1054,10 @@ public void visit(NegatedPredicate negate) { visitIn(p.getExpression(), p.getValues(), true); return; } + if (negated instanceof BetweenPredicate betweenPredicate) { + visitInBetween(betweenPredicate.getValue(), betweenPredicate.getFrom(), betweenPredicate.getTo(), true); + return; + } Map preQuery = query; query = new LinkedHashMap<>(); visitPredicate(negate.getNegated()); @@ -988,37 +1085,41 @@ public void visitIn(Expression expression, Collection values, boolean nega @Override public void visitRegexp(Expression leftExpression, Expression expression) { - Object value = expression; + Expression value = expression; if (expression instanceof LiteralExpression literalExpression) { - value = new RegexPattern((String) literalExpression.getValue()); + value = new LiteralExpression(new RegexPattern((String) literalExpression.getValue())); } appendOperatorExpression(leftExpression, REGEX, value); } @Override public void visitContains(Expression leftExpression, Expression rightExpression, boolean ignoreCase) { - handleRegexExpression(leftExpression, ignoreCase, false, false, false, rightExpression); + handleRegexExpression(leftExpression, ignoreCase, false, false, false, rightExpression, false); } @Override public void visitEndsWith(Expression leftExpression, Expression rightExpression, boolean ignoreCase) { - handleRegexExpression(leftExpression, ignoreCase, false, false, true, rightExpression); + handleRegexExpression(leftExpression, ignoreCase, false, false, true, rightExpression, false); } @Override public void visitStartsWith(Expression leftExpression, Expression rightExpression, boolean ignoreCase) { - handleRegexExpression(leftExpression, ignoreCase, false, true, false, rightExpression); + handleRegexExpression(leftExpression, ignoreCase, false, true, false, rightExpression, false); } @Override public void visit(LikePredicate likePredicate) { - if (likePredicate.isCaseInsensitive()) { - throw new UnsupportedOperationException("ILike is not supported by this implementation."); + Expression pattern = likePredicate.getPattern(); + if (pattern instanceof LiteralExpression literalExpression && literalExpression.getValue() instanceof String patternString) { + patternString = patternString + .replace("_", ".") + .replace("%", ".*"); + pattern = new LiteralExpression<>(patternString); } handleRegexExpression( likePredicate.getExpression(), - false, false, false, false, - likePredicate.getPattern()); + likePredicate.isCaseInsensitive(), likePredicate.isNegated(), false, false, + pattern, true); } @Override @@ -1028,27 +1129,48 @@ public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) { @Override public void visitEquals(Expression leftExpression, Expression rightExpression, boolean ignoreCase) { + if (leftExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { + PersistentPropertyPath propertyPath = persistentPropertyPath.getPropertyPath(); + PersistentProperty property = propertyPath.getProperty(); + if (property.isEnum() && rightExpression instanceof LiteralExpression literalExpression + && literalExpression.getValue() instanceof String stringValue) { + String typeName = property.getTypeName().replace("$", "."); + if (stringValue.startsWith(typeName)) { + for (PersistentProperty.EnumConstant enumConstant : property.getEnumConstants()) { + if (stringValue.equals(typeName + "." + enumConstant.name())) { + if (property.getDataType() == DataType.STRING) { + rightExpression = new LiteralExpression(enumConstant.name()); + } + if (property.getDataType() == DataType.INTEGER) { + rightExpression = new LiteralExpression(enumConstant.ordinal()); + } + break; + } + } + } + } + } if (ignoreCase) { - handleRegexExpression(leftExpression, true, false, true, true, rightExpression); + handleRegexExpression(leftExpression, true, false, true, true, rightExpression, false); return; } appendEquals(leftExpression, rightExpression); } - private void appendEquals(Expression leftExpression, Object value) { + private void appendEquals(Expression leftExpression, Expression value) { appendOperatorExpression(leftExpression, "$eq", value); } @Override public void visitNotEquals(Expression leftExpression, Expression rightExpression, boolean ignoreCase) { if (ignoreCase) { - handleRegexExpression(leftExpression, true, true, true, true, rightExpression); + handleRegexExpression(leftExpression, true, true, true, true, rightExpression, false); return; } appendPropertyNotEquals(leftExpression, rightExpression); } - private void appendPropertyNotEquals(Expression leftExpression, Object value) { + private void appendPropertyNotEquals(Expression leftExpression, Expression value) { appendOperatorExpression(leftExpression, "$ne", value); } @@ -1073,33 +1195,55 @@ public void visitLessThanOrEquals(Expression leftExpression, Expression ri } @Override - public void visitInBetween(Expression value, Expression from, Expression to) { - PersistentPropertyPath propertyPath = requireProperty(value).getPropertyPath(); - String propertyName = getPropertyPersistName(propertyPath); - query.put("$and", asList( - Map.of(propertyName, Map.of("$gte", valueRepresentation(queryState, propertyPath, from))), - Map.of(propertyName, Map.of("$lte", valueRepresentation(queryState, propertyPath, to))) - )); + public void visitInBetween(Expression value, Expression from, Expression to, boolean negated) { + String propertyName; + Object firstCondition; + Object secondCondition; + if (value instanceof UnaryExpression valueExp && valueExp.getType() == UnaryExpressionType.LOWER + && from instanceof UnaryExpression fromExp && fromExp.getType() == UnaryExpressionType.LOWER + && to instanceof UnaryExpression toExp && toExp.getType() == UnaryExpressionType.LOWER) { + PersistentPropertyPath propertyPath = requireProperty(valueExp.getExpression()).getPropertyPath(); + propertyName = getPropertyPersistName(propertyPath); + firstCondition = Map.of("$toLower", valueRepresentation(queryState, propertyPath, fromExp.getExpression())); + secondCondition = Map.of("$toLower", valueRepresentation(queryState, propertyPath, toExp.getExpression())); + } else { + PersistentPropertyPath propertyPath = requireProperty(value).getPropertyPath(); + propertyName = getPropertyPersistName(propertyPath); + firstCondition = valueRepresentation(queryState, propertyPath, from); + secondCondition = valueRepresentation(queryState, propertyPath, to); + } + LinkedHashMap map = new LinkedHashMap<>(); + map.put("$gte", firstCondition); + map.put("$lte", secondCondition); + if (negated) { + query.put( + propertyName, Map.of("$not", map) + ); + } else { + query.put( + propertyName, map + ); + } } @Override public void visitIsFalse(Expression expression) { - appendEquals(expression, false); + appendEquals(expression, new LiteralExpression<>(false)); } @Override public void visitIsNotNull(Expression expression) { - appendPropertyNotEquals(expression, null); + appendPropertyNotEquals(expression, new LiteralExpression<>(null)); } @Override public void visitIsNull(Expression expression) { - appendEquals(expression, null); + appendEquals(expression, new LiteralExpression<>(null)); } @Override public void visitIsTrue(Expression expression) { - appendEquals(expression, true); + appendEquals(expression, new LiteralExpression<>(true)); } @Override @@ -1156,11 +1300,16 @@ private void handleRegexExpression(Expression leftExpression, boolean negate, boolean startsWith, boolean endsWith, - Object value) { + Expression value, + boolean isLike) { PersistentPropertyPath propertyPath = CriteriaUtils.requireProperty(leftExpression).getPropertyPath(); Object filterValue; Map regexCriteria = new LinkedHashMap<>(2); - regexCriteria.put(OPTIONS, ignoreCase ? "i" : ""); + String options = ignoreCase ? "i" : ""; + if (isLike) { + options += "l"; + } + regexCriteria.put(OPTIONS, options); String regexValue; if (value instanceof BindingParameter bindingParameter) { int index = queryState.pushParameter( @@ -1168,6 +1317,8 @@ private void handleRegexExpression(Expression leftExpression, newBindingContext(propertyPath, propertyPath) ); regexValue = QUERY_PARAMETER_PLACEHOLDER + ":" + index; + } else if (value instanceof LiteralExpression literalExpression) { + regexValue = (String) literalExpression.getValue(); } else { regexValue = value.toString(); } @@ -1201,6 +1352,12 @@ private Object valueRepresentation(PropertyParameterCreator parameterCreator, PersistentPropertyPath inPropertyPath, PersistentPropertyPath outPropertyPath, Object value) { + if (value instanceof LiteralExpression literalExpression) { + value = literalExpression.getValue(); + } + if (value instanceof RegexPattern regexPattern) { + return "'" + Pattern.quote(regexPattern.value) + "'"; + } if (value instanceof LocalDate localDate) { return Map.of(MONGO_DATE_IDENTIFIER, formatDate(localDate)); } @@ -1213,9 +1370,8 @@ private Object valueRepresentation(PropertyParameterCreator parameterCreator, newBindingContext(inPropertyPath, outPropertyPath) ); return Map.of(QUERY_PARAMETER_PLACEHOLDER, index); - } else { - return asLiteral(value); } + return value; } private String formatDate(LocalDate localDate) { diff --git a/data-document-model/src/main/java/io/micronaut/data/document/serde/defaults/DefaultIdPropertyNamingStrategy.java b/data-document-model/src/main/java/io/micronaut/data/document/serde/defaults/DefaultIdPropertyNamingStrategy.java index 7346d8929a..497c79d1d3 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/serde/defaults/DefaultIdPropertyNamingStrategy.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/serde/defaults/DefaultIdPropertyNamingStrategy.java @@ -29,7 +29,8 @@ */ @Internal @Singleton -final class DefaultIdPropertyNamingStrategy implements IdPropertyNamingStrategy { +final class +DefaultIdPropertyNamingStrategy implements IdPropertyNamingStrategy { @Override public String translate(AnnotatedElement element) { return element.getName(); diff --git a/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoExecutorQueryMethodMatcher.java b/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoExecutorQueryMethodMatcher.java index 23b4704d31..260a5158ca 100644 --- a/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoExecutorQueryMethodMatcher.java +++ b/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoExecutorQueryMethodMatcher.java @@ -63,7 +63,7 @@ public MethodMatch match(MethodMatchContext matchContext) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter.getName()); + matchInfo.addParameterRole(parameter, MongoAnnotations.FILTER_ROLE); } }; @@ -72,7 +72,7 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.PIPELINE_ROLE, parameter.getName()); + matchInfo.addParameterRole(parameter, MongoAnnotations.PIPELINE_ROLE); } }; @@ -81,7 +81,7 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FIND_OPTIONS_ROLE, parameter.getName()); + matchInfo.addParameterRole(parameter, MongoAnnotations.FIND_OPTIONS_ROLE); } }; @@ -95,8 +95,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter1.getName()); - matchInfo.addParameterRole(MongoAnnotations.FIND_OPTIONS_ROLE, parameter2.getName()); + matchInfo.addParameterRole(parameter1, MongoAnnotations.FILTER_ROLE); + matchInfo.addParameterRole(parameter2, MongoAnnotations.FIND_OPTIONS_ROLE); } }; @@ -105,8 +105,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.PIPELINE_ROLE, parameter1.getName()); - matchInfo.addParameterRole(MongoAnnotations.AGGREGATE_OPTIONS_ROLE, parameter2.getName()); + matchInfo.addParameterRole(parameter1, MongoAnnotations.PIPELINE_ROLE); + matchInfo.addParameterRole(parameter2, MongoAnnotations.AGGREGATE_OPTIONS_ROLE); } }; @@ -122,8 +122,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, p1.getName()); - matchInfo.addParameterRole(TypeRole.PAGEABLE, p2.getName()); + matchInfo.addParameterRole(p1, MongoAnnotations.FILTER_ROLE); + matchInfo.addParameterRole(p2, TypeRole.PAGEABLE); // Fake query to have stored query matchContext.getMethodElement().annotate(Query.class, builder -> { builder.member(DataMethod.META_MEMBER_COUNT_QUERY, "{}"); @@ -137,8 +137,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FIND_OPTIONS_ROLE, p1.getName()); - matchInfo.addParameterRole(TypeRole.PAGEABLE, p2.getName()); + matchInfo.addParameterRole(p1, MongoAnnotations.FIND_OPTIONS_ROLE); + matchInfo.addParameterRole(p2, TypeRole.PAGEABLE); // Fake query to have stored query matchContext.getMethodElement().annotate(Query .class, builder -> { builder.member(DataMethod.META_MEMBER_COUNT_QUERY, "{}"); @@ -161,7 +161,7 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter.getName()); + matchInfo.addParameterRole(parameter, MongoAnnotations.FILTER_ROLE); } }; @@ -179,7 +179,7 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter.getName()); + matchInfo.addParameterRole(parameter, MongoAnnotations.FILTER_ROLE); } }; @@ -193,8 +193,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter1.getName()); - matchInfo.addParameterRole(MongoAnnotations.DELETE_OPTIONS_ROLE, parameter2.getName()); + matchInfo.addParameterRole(parameter1, MongoAnnotations.FILTER_ROLE); + matchInfo.addParameterRole(parameter2, MongoAnnotations.DELETE_OPTIONS_ROLE); } }; @@ -215,8 +215,8 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, parameter1.getName()); - matchInfo.addParameterRole(MongoAnnotations.UPDATE_ROLE, parameter2.getName()); + matchInfo.addParameterRole(parameter1, MongoAnnotations.FILTER_ROLE); + matchInfo.addParameterRole(parameter2, MongoAnnotations.UPDATE_ROLE); } }; @@ -231,9 +231,9 @@ protected void apply(MethodMatchInfo matchInfo) { @Override protected void apply(MethodMatchInfo matchInfo) { - matchInfo.addParameterRole(MongoAnnotations.FILTER_ROLE, filter.getName()); - matchInfo.addParameterRole(MongoAnnotations.UPDATE_ROLE, update.getName()); - matchInfo.addParameterRole(MongoAnnotations.UPDATE_OPTIONS_ROLE, options.getName()); + matchInfo.addParameterRole(filter, MongoAnnotations.FILTER_ROLE); + matchInfo.addParameterRole(update, MongoAnnotations.UPDATE_ROLE); + matchInfo.addParameterRole(options, MongoAnnotations.UPDATE_OPTIONS_ROLE); } }; diff --git a/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoRawQueryMethodMatcher.java b/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoRawQueryMethodMatcher.java index 0d21fa0061..95f6404af7 100644 --- a/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoRawQueryMethodMatcher.java +++ b/data-document-processor/src/main/java/io/micronaut/data/document/processor/matchers/MongoRawQueryMethodMatcher.java @@ -145,9 +145,9 @@ public MethodMatchInfo buildMatchInfo(MethodMatchContext matchContext) { buildRawQuery(matchContext, methodMatchInfo, entityParameter, entitiesParameter, operationType); if (entityParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITY, entityParameter.getName()); + methodMatchInfo.addParameterRole(entityParameter, TypeRole.ENTITY); } else if (entitiesParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITIES, entitiesParameter.getName()); + methodMatchInfo.addParameterRole(entitiesParameter, TypeRole.ENTITIES); } return methodMatchInfo; } diff --git a/data-document-processor/src/test/groovy/io/micronaut/data/document/processor/BuildMongoQuerySpec.groovy b/data-document-processor/src/test/groovy/io/micronaut/data/document/processor/BuildMongoQuerySpec.groovy index a5e642d5b4..df7b5df703 100644 --- a/data-document-processor/src/test/groovy/io/micronaut/data/document/processor/BuildMongoQuerySpec.groovy +++ b/data-document-processor/src/test/groovy/io/micronaut/data/document/processor/BuildMongoQuerySpec.groovy @@ -3,10 +3,37 @@ package io.micronaut.data.document.processor import io.micronaut.data.annotation.Query import io.micronaut.data.document.mongo.MongoAnnotations import io.micronaut.data.intercept.annotation.DataMethod +import io.micronaut.data.model.Pageable import io.micronaut.data.mongodb.annotation.MongoSort +import org.bson.conversions.Bson class BuildMongoQuerySpec extends AbstractDataSpec { + void "test findALL"() { + given: + def repository = buildRepository('test.MyInterface2', """ +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.mongodb.annotation.MongoRepository; +import io.micronaut.data.mongodb.annotation.MongoFindQuery; +import io.micronaut.data.document.tck.entities.Book; +import org.bson.conversions.Bson; + +@MongoRepository +interface MyInterface2 extends GenericRepository { + + Page findAll(Bson filter, Pageable pageable); + +} +""" + ) + + when: + String q = TestUtils.getQuery(repository.getRequiredMethod("findAll", Bson, Pageable)) + then: + q == "{}" + } + void "test custom method"() { given: def repository = buildRepository('test.MyInterface2', """ diff --git a/data-hibernate-jpa/build.gradle b/data-hibernate-jpa/build.gradle index 1f57d80f31..500bba9b15 100644 --- a/data-hibernate-jpa/build.gradle +++ b/data-hibernate-jpa/build.gradle @@ -15,6 +15,8 @@ dependencies { api projects.micronautDataJpa api mnSql.hibernate.core + compileOnly libs.managed.jakarta.data.api + implementation (mnSql.micronaut.hibernate.jpa) { exclude group:'org.javassist', module:'javassist' exclude group: 'io.micronaut.data' @@ -28,7 +30,9 @@ dependencies { testImplementation mnValidation.micronaut.validation.processor testImplementation mn.micronaut.inject.groovy - testImplementation projects.micronautDataProcessor + testImplementation (projects.micronautDataProcessor) { + exclude group: 'org.antlr' + } testImplementation mnRxjava2.micronaut.rxjava2 testImplementation mnReactor.micronaut.reactor testImplementation projects.micronautDataTck @@ -40,6 +44,7 @@ dependencies { testRuntimeOnly(libs.jupiter.engine) testImplementation(mnTest.micronaut.test.junit5) + testImplementation(libs.managed.jakarta.data.api) } micronaut { diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataDeleteExceptionConverter.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataDeleteExceptionConverter.java new file mode 100644 index 0000000000..a1f1b77e55 --- /dev/null +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataDeleteExceptionConverter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2025 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.hibernate.jakarta.data; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataDeleteExceptionConverter; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Singleton; +import jakarta.persistence.NoResultException; +import jakarta.persistence.NonUniqueResultException; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.StaleStateException; + +/** + * The Hibernate to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +@Singleton +@Requires(classes = OptimisticLockingFailureException.class) +final class HibernateJakartaDataDeleteExceptionConverter implements JakartaDataDeleteExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof NonUniqueResultException) { + throw new jakarta.data.exceptions.NonUniqueResultException(exception.getMessage(), exception); + } + if (exception instanceof NoResultException) { + throw new EmptyResultException(exception.getMessage(), exception); + } + if (exception instanceof StaleStateException || exception instanceof OptimisticLockException) { + throw new OptimisticLockingFailureException(exception.getMessage(), exception); + } + if (exception instanceof PersistenceException) { + return new DataException(exception.getMessage(), exception); + } + return exception; + } +} diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataExceptionConverter.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataExceptionConverter.java new file mode 100644 index 0000000000..8e47dcee5b --- /dev/null +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataExceptionConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2025 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.hibernate.jakarta.data; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataExceptionConverter; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Singleton; +import jakarta.persistence.NoResultException; +import jakarta.persistence.NonUniqueResultException; +import jakarta.persistence.PersistenceException; + +/** + * The Hibernate to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +@Singleton +@Requires(classes = OptimisticLockingFailureException.class) +final class HibernateJakartaDataExceptionConverter implements JakartaDataExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof NonUniqueResultException || exception instanceof org.hibernate.NonUniqueResultException) { + throw new jakarta.data.exceptions.NonUniqueResultException(exception.getMessage(), exception); + } + if (exception instanceof NoResultException) { + throw new EmptyResultException(exception.getMessage(), exception); + } + if (exception instanceof PersistenceException) { + return new DataException(exception.getMessage(), exception); + } + return exception; + } +} diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataPersistExceptionConverter.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataPersistExceptionConverter.java new file mode 100644 index 0000000000..2e9fcb9be3 --- /dev/null +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataPersistExceptionConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2025 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.hibernate.jakarta.data; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataInsertExceptionConverter; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EntityExistsException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Singleton; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.StaleStateException; +import org.hibernate.exception.ConstraintViolationException; + +/** + * The Hibernate to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +@Singleton +@Requires(classes = OptimisticLockingFailureException.class) +final class HibernateJakartaDataPersistExceptionConverter implements JakartaDataInsertExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof ConstraintViolationException) { + throw new EntityExistsException(exception.getMessage(), exception); + } + if (exception instanceof StaleStateException || exception instanceof OptimisticLockException) { + throw new EntityExistsException(exception.getMessage(), exception); + } + if (exception instanceof PersistenceException) { + return new DataException(exception.getMessage(), exception); + } + return exception; + } +} diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataUpdateExceptionConverter.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataUpdateExceptionConverter.java new file mode 100644 index 0000000000..ef780730ac --- /dev/null +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/jakarta/data/HibernateJakartaDataUpdateExceptionConverter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2025 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.hibernate.jakarta.data; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataUpdateExceptionConverter; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Singleton; +import jakarta.persistence.NoResultException; +import jakarta.persistence.NonUniqueResultException; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.StaleStateException; + +/** + * The Hibernate to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +@Singleton +@Requires(classes = OptimisticLockingFailureException.class) +final class HibernateJakartaDataUpdateExceptionConverter implements JakartaDataUpdateExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof NonUniqueResultException) { + throw new jakarta.data.exceptions.NonUniqueResultException(exception.getMessage(), exception); + } + if (exception instanceof NoResultException) { + throw new EmptyResultException(exception.getMessage(), exception); + } + if (exception instanceof StaleStateException || exception instanceof OptimisticLockException) { + throw new OptimisticLockingFailureException(exception.getMessage(), exception); + } + if (exception instanceof PersistenceException) { + return new DataException(exception.getMessage(), exception); + } + return exception; + } +} diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java index b5bfe60cff..9819cd311c 100644 --- a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java @@ -29,8 +29,8 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.QueryHint; import io.micronaut.data.jpa.annotation.EntityGraph; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; -import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.builder.jpa.JpaQueryBuilder; import io.micronaut.data.model.runtime.PagedQuery; @@ -62,6 +62,7 @@ import org.hibernate.graph.Graph; import org.hibernate.graph.RootGraph; import org.hibernate.graph.SubGraph; +import org.hibernate.query.SortDirection; import java.util.ArrayList; import java.util.Arrays; @@ -116,7 +117,7 @@ public PreparedQuery decorate(PreparedQuery preparedQuery) { @Override public StoredQuery decorate(StoredQuery storedQuery) { RuntimePersistentEntity runtimePersistentEntity = runtimeEntityRegistry.getEntity(storedQuery.getRootEntity()); - return new DefaultBindableParametersStoredQuery<>(storedQuery, runtimePersistentEntity); + return new DefaultBindableParametersStoredQuery<>(storedQuery, runtimePersistentEntity, dataConversionService); } /** @@ -321,8 +322,7 @@ public Map getQueryHints(@NonNull StoredQuery storedQuery) * @param The result type */ protected void collectFindOne(S session, PreparedQuery preparedQuery, ResultCollector collector) { - String query = preparedQuery.getQuery(); - collectResults(session, query, preparedQuery, preparedQuery.getPageable(), collector); + collectResults(session, preparedQuery.getQuery(), preparedQuery, preparedQuery.getQueryLimit(), preparedQuery.getSort(), collector); } /** @@ -350,14 +350,15 @@ protected void collectFindAll(S session, PreparedQuery preparedQuery, pageable = pageable.withoutSort(); } } - collectResults(session, queryStr, preparedQuery, pageable, collector); + collectResults(session, queryStr, preparedQuery, preparedQuery.getQueryLimit(), pageable.getSort(), collector); } - private void collectResults(S session, - String queryStr, - PreparedQuery preparedQuery, - Pageable pageable, - ResultCollector resultCollector) { + protected final void collectResults(S session, + String queryStr, + PreparedQuery preparedQuery, + Limit limit, + Sort sort, + ResultCollector resultCollector) { if (preparedQuery.isDtoProjection()) { P q; if (preparedQuery.isNative()) { @@ -365,13 +366,13 @@ private void collectResults(S session, } else if (queryStr.toLowerCase(Locale.ENGLISH).startsWith("select new ")) { @SuppressWarnings("unchecked") Class wrapperType = (Class) ReflectionUtils.getWrapperType(preparedQuery.getResultType()); P query = createQuery(session, queryStr, wrapperType); - bindPreparedQuery(query, preparedQuery, pageable, session); + bindPreparedQuery(query, preparedQuery, limit, sort, session); resultCollector.collect(query); return; } else { q = createQuery(session, queryStr, Tuple.class); } - bindPreparedQuery(q, preparedQuery, pageable, session); + bindPreparedQuery(q, preparedQuery, limit, sort, session); resultCollector.collectTuple(q, tuple -> { Set properties = tuple.getElements().stream().map(TupleElement::getAlias).collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))); return (new BeanIntrospectionMapper() { @@ -397,7 +398,7 @@ public ConversionService getConversionService() { Class rootEntity = preparedQuery.getRootEntity(); if (wrapperType != rootEntity) { P nativeQuery = createNativeQuery(session, queryStr, Tuple.class); - bindPreparedQuery(nativeQuery, preparedQuery, pageable, session); + bindPreparedQuery(nativeQuery, preparedQuery, limit, sort, session); resultCollector.collectTuple(nativeQuery, tuple -> { Object o = tuple.get(0); if (wrapperType.isInstance(o)) { @@ -412,7 +413,7 @@ public ConversionService getConversionService() { } else { q = createQuery(session, queryStr, wrapperType); } - bindPreparedQuery(q, preparedQuery, pageable, session); + bindPreparedQuery(q, preparedQuery, limit, sort, session); resultCollector.collect(q); } } @@ -518,9 +519,13 @@ public void bindMany(QueryParameterBinding binding, Collection values) { }; } - private void bindPreparedQuery(P q, @NonNull PreparedQuery preparedQuery, Pageable pageable, S currentSession) { + private void bindPreparedQuery(P q, + @NonNull PreparedQuery preparedQuery, + @NonNull Limit limit, + @NonNull Sort sort, + S currentSession) { bindParameters(q, preparedQuery, true); - bindPageable(q, pageable); + bindLimitAndSort(q, limit, sort, preparedQuery.getRootEntity()); bindQueryHints(q, preparedQuery, currentSession); } @@ -602,44 +607,52 @@ protected final FlushModeType getFlushModeType(AnnotationMetadata annotationMeta return annotationMetadata.getAnnotationValuesByType(QueryHint.class).stream().filter(av -> FlushModeType.class.getName().equals(av.stringValue("name").orElse(null))).map(av -> av.enumValue("value", FlushModeType.class)).findFirst().orElse(Optional.empty()).orElse(null); } - private void bindPageable(P q, @NonNull Pageable pageable) { - if (pageable == Pageable.UNPAGED) { - // no pagination - return; + private void bindLimitAndSort(@NonNull P q, @NonNull Limit limit, @NonNull Sort sort, @NotNull Class entityClass) { + if (limit.isLimited()) { + int max = limit.maxResults(); + if (max >= 0) { + setMaxResults(q, max); + } + long offset = limit.offset(); + if (offset > 0) { + setOffset(q, (int) offset); + } } - if (pageable.getMode() != Mode.OFFSET) { - throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by hibernate operations"); + if (sort != null && sort.isSorted()) { + List orders = getOrders(sort, entityClass); + setOrder(q, orders); } + } - int max = pageable.getSize(); - if (max > 0) { - setMaxResults(q, max); - } - long offset = pageable.getOffset(); - if (offset > 0) { - setOffset(q, (int) offset); + protected static List> getOrders(Sort sort, Class entityClass) { + List orderBy = sort.getOrderBy(); + List> orders = new ArrayList<>(orderBy.size()); + for (Sort.Order order : orderBy) { + orders.add(org.hibernate.query.Order.by( + entityClass, + order.getProperty(), + order.isAscending() ? SortDirection.ASCENDING : SortDirection.DESCENDING, order.isIgnoreCase()) + ); } + return orders; } protected final void collectPagedResults(CriteriaBuilder criteriaBuilder, S session, PagedQuery pagedQuery, ResultCollector resultCollector) { - Pageable pageable = pagedQuery.getPageable(); Class entity = pagedQuery.getRootEntity(); CriteriaQuery query = criteriaBuilder.createQuery(pagedQuery.getRootEntity()); Root root = query.from(entity); - bindCriteriaSort(query, root, criteriaBuilder, pageable); + bindCriteriaSort(query, root, criteriaBuilder, pagedQuery.getSort()); P q = createQuery(session, query); - bindPageable(q, pageable.withoutSort()); + bindLimitAndSort(q, pagedQuery.getQueryLimit(), pagedQuery.getSort(), entity); bindQueryHints(q, pagedQuery, session); resultCollector.collect(q); } - protected final void collectCountOf(CriteriaBuilder criteriaBuilder, S session, Class entity, @Nullable Pageable pageable, ResultCollector resultCollector) { + protected final void collectCountOf(CriteriaBuilder criteriaBuilder, S session, Class entity, @NonNull Limit limit, ResultCollector resultCollector) { CriteriaQuery countQuery = criteriaBuilder.createQuery(Long.class); countQuery.select(criteriaBuilder.count(countQuery.from(entity))); P countQ = createQuery(session, countQuery); - if (pageable != null) { - bindPageable(countQ, pageable.withoutSort()); - } + bindLimitAndSort(countQ, limit, Sort.UNSORTED, entity); resultCollector.collect(countQ); } diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/HibernateJpaOperations.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/HibernateJpaOperations.java index 5629f40155..e959456ce3 100644 --- a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/HibernateJpaOperations.java +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/HibernateJpaOperations.java @@ -16,6 +16,7 @@ package io.micronaut.data.hibernate.operations; import io.micronaut.aop.InvocationContext; +import io.micronaut.configuration.hibernate.jpa.JpaConfiguration; import io.micronaut.context.ApplicationContext; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.Parameter; @@ -23,15 +24,22 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.ConvertibleValuesMap; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.data.annotation.QueryHint; import io.micronaut.data.annotation.sql.Procedure; +import io.micronaut.data.connection.ConnectionOperations; +import io.micronaut.data.connection.ConnectionStatus; import io.micronaut.data.hibernate.conf.RequiresSyncHibernate; import io.micronaut.data.jpa.annotation.EntityGraph; import io.micronaut.data.jpa.operations.JpaRepositoryOperations; +import io.micronaut.data.model.CursoredPage; +import io.micronaut.data.model.CursoredPageable; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.BatchOperation; import io.micronaut.data.model.runtime.DeleteBatchOperation; import io.micronaut.data.model.runtime.DeleteOperation; @@ -43,6 +51,7 @@ import io.micronaut.data.model.runtime.QueryParameterBinding; import io.micronaut.data.model.runtime.RuntimeEntityRegistry; import io.micronaut.data.model.runtime.RuntimePersistentEntity; +import io.micronaut.data.model.runtime.RuntimePersistentProperty; import io.micronaut.data.model.runtime.StoredQuery; import io.micronaut.data.model.runtime.UpdateBatchOperation; import io.micronaut.data.model.runtime.UpdateOperation; @@ -70,8 +79,11 @@ import org.hibernate.graph.RootGraph; import org.hibernate.procedure.ProcedureCall; import org.hibernate.query.CommonQueryContract; +import org.hibernate.query.KeyedPage; +import org.hibernate.query.KeyedResultList; import org.hibernate.query.MutationQuery; import org.hibernate.query.Query; +import org.hibernate.query.QueryProducer; import javax.sql.DataSource; import java.util.ArrayList; @@ -85,6 +97,11 @@ import java.util.function.Function; import java.util.stream.Stream; +import static java.util.stream.Collectors.toList; +import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE; +import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_LAST_ON_PREVIOUS_PAGE; +import static org.hibernate.query.Page.page; + /** * Implementation of the {@link JpaRepositoryOperations} interface for Hibernate. * @@ -97,14 +114,18 @@ final class HibernateJpaOperations extends AbstractHibernateOperations connectionOperations; private final TransactionOperations transactionOperations; private ExecutorAsyncOperations asyncOperations; private ExecutorService executorService; + private final boolean uniqueResultOnFindOne; + private final boolean persistOrMergeOnSave; /** * Default constructor. * * @param sessionFactory The session factory + * @param connectionOperations The connection operations * @param transactionOperations The transaction operations * @param executorService The executor service for I/O tasks to use * @param runtimeEntityRegistry The runtime entity registry @@ -112,15 +133,22 @@ final class HibernateJpaOperations extends AbstractHibernateOperations connectionOperations, @NonNull @Parameter TransactionOperations transactionOperations, + @NonNull @Parameter JpaConfiguration jpaConfiguration, @Named("io") @Nullable ExecutorService executorService, RuntimeEntityRegistry runtimeEntityRegistry, DataConversionService dataConversionService) { super(runtimeEntityRegistry, dataConversionService); ArgumentUtils.requireNonNull("sessionFactory", sessionFactory); this.sessionFactory = sessionFactory; + this.connectionOperations = connectionOperations; this.transactionOperations = transactionOperations; this.executorService = executorService; + this.uniqueResultOnFindOne = new ConvertibleValuesMap<>(jpaConfiguration.getProperties()) + .get("uniqueResultOnFindOne", boolean.class, false); + this.persistOrMergeOnSave = new ConvertibleValuesMap<>(jpaConfiguration.getProperties()) + .get("persistOrMergeOnSave", boolean.class, false); } @Override @@ -255,12 +283,18 @@ public T merge(T entity) { @Override public R findOne(@NonNull PreparedQuery preparedQuery) { return executeRead(session -> { - // limit does not work with native queries and does not produce expected - // results with EntityGraph annotation and joins - boolean limitOne = !preparedQuery.isNative() && !hasEntityGraph(preparedQuery.getAnnotationMetadata()); - FirstResultCollector collector = new FirstResultCollector<>(limitOne); - collectFindOne(session, preparedQuery, collector); - return collector.result; + if (uniqueResultOnFindOne) { + UniqueResultCollector collector = new UniqueResultCollector<>(); + collectFindOne(session, preparedQuery, collector); + return collector.result; + } else { + // limit does not work with native queries and does not produce expected + // results with EntityGraph annotation and joins + boolean limitOne = !preparedQuery.isNative() && !hasEntityGraph(preparedQuery.getAnnotationMetadata()); + FirstResultCollector collector = new FirstResultCollector<>(limitOne); + collectFindOne(session, preparedQuery, collector); + return collector.result; + } }); } @@ -272,7 +306,7 @@ public boolean exists(@NonNull PreparedQuery preparedQuery) { @NonNull @Override public Iterable findAll(@NonNull PagedQuery pagedQuery) { - return executeRead(session -> findPaged(session, pagedQuery)); + return executeRead(session -> findPage(session, pagedQuery)); } @NonNull @@ -280,45 +314,87 @@ public Iterable findAll(@NonNull PagedQuery pagedQuery) { public Stream findStream(@NonNull PagedQuery pagedQuery) { return executeRead(session -> { StreamResultCollector collector = new StreamResultCollector<>(); - collectPagedResults(sessionFactory.getCriteriaBuilder(), session, pagedQuery, collector); + collectPagedResults(session.getCriteriaBuilder(), session, pagedQuery, collector); return collector.result; }); } @Override public Page findPage(@NonNull PagedQuery pagedQuery) { - return executeRead(session -> Page.of( - findPaged(session, pagedQuery), - pagedQuery.getPageable(), - countOf(session, pagedQuery, pagedQuery.getPageable()) - )); + return executeRead(session -> findPage(session, pagedQuery)); } @Override public long count(PagedQuery pagedQuery) { - return executeRead(session -> countOf(session, pagedQuery, null)); - } - - private List findPaged(Session session, PagedQuery pagedQuery) { + return executeRead(session -> countOf(session, pagedQuery, Limit.UNLIMITED)); + } + + private Page findPage(Session session, PagedQuery pagedQuery) { + if (pagedQuery instanceof PreparedQuery pq) { + PreparedQuery preparedQuery = (PreparedQuery) pq; + Pageable pageable = preparedQuery.getPageable(); + if (pageable.getMode() != Pageable.Mode.OFFSET) { + KeyedResultList keyedResultList = getKeyedResult(preparedQuery, session, pageable); + List cursors = + keyedResultList.getKeyList() + .stream() + .map(key -> Pageable.Cursor.of(key.toArray())) + .collect(toList()); + return CursoredPage.of(keyedResultList.getResultList(), pageable, cursors, -1L); + } + ListResultCollector resultCollector = new ListResultCollector<>(); + collectFindAll(session, preparedQuery, resultCollector); + return Page.of(resultCollector.result, pageable, -1L); + } ListResultCollector collector = new ListResultCollector<>(); collectPagedResults(sessionFactory.getCriteriaBuilder(), session, pagedQuery, collector); - return collector.result; + return Page.of((List) collector.result, pagedQuery.getPageable(), -1L); } - private Long countOf(Session session, PagedQuery pagedQuery, @Nullable Pageable pageable) { + private Long countOf(Session session, PagedQuery pagedQuery, Limit limit) { SingleResultCollector collector = new SingleResultCollector<>(); - collectCountOf(sessionFactory.getCriteriaBuilder(), session, pagedQuery.getRootEntity(), pageable, collector); + collectCountOf(sessionFactory.getCriteriaBuilder(), session, pagedQuery.getRootEntity(), limit, collector); return collector.result; } @NonNull @Override public Iterable findAll(@NonNull PreparedQuery preparedQuery) { - return executeRead(session -> { - ListResultCollector resultCollector = new ListResultCollector<>(); - collectFindAll(session, preparedQuery, resultCollector); - return resultCollector.result; - }); + return executeRead(session -> findAll(preparedQuery, session)); + } + + private List findAll(PreparedQuery preparedQuery, Session session) { + Pageable pageable = preparedQuery.getPageable(); + if (pageable.getMode() != Pageable.Mode.OFFSET) { + return getKeyedResult(preparedQuery, session, pageable).getResultList(); + } + ListResultCollector resultCollector = new ListResultCollector<>(); + collectFindAll(session, preparedQuery, resultCollector); + return resultCollector.result; + } + + private KeyedResultList getKeyedResult(PreparedQuery preparedQuery, Session session, Pageable pageable) { + KeyedPage keyedPage = getKeyedPage(preparedQuery, pageable); + KeyedResultListCollector resultCollector = new KeyedResultListCollector<>(keyedPage); + collectResults(session, preparedQuery.getQuery(), preparedQuery, Limit.UNLIMITED, Sort.UNSORTED, resultCollector); + return resultCollector.result; + } + + private static KeyedPage getKeyedPage(PreparedQuery preparedQuery, Pageable pageable) { + CursoredPageable cursoredPageable = (CursoredPageable) pageable; + var unkeyedPage = + page(pageable.getSize(), pageable.getNumber()) + .keyedBy(getOrders(preparedQuery.getSort(), preparedQuery.getRootEntity())); + return cursoredPageable.cursor() + .map(_cursor -> { + List els = _cursor.elements(); + var elements = (List>) els; + return switch (pageable.getMode()) { + case CURSOR_NEXT -> unkeyedPage.withKey(elements, KEY_OF_LAST_ON_PREVIOUS_PAGE); + case CURSOR_PREVIOUS -> unkeyedPage.withKey(elements, KEY_OF_FIRST_ON_NEXT_PAGE); + default -> unkeyedPage; + }; + }).orElse(unkeyedPage); } @Override @@ -329,7 +405,17 @@ public T persist(@NonNull InsertOperation operation) { return executeUpdate(operation, session, storedQuery); } T entity = operation.getEntity(); - session.persist(entity); + if (persistOrMergeOnSave) { + RuntimePersistentEntity persistentEntity = getEntity(operation.getRootEntity()); + RuntimePersistentProperty identity = persistentEntity.getIdentity(); + if (identity != null && identity.getProperty().get(entity) == null) { + session.persist(entity); + } else { + entity = session.merge(entity); + } + } else { + session.persist(entity); + } flushIfNecessary(session, operation.getAnnotationMetadata()); return entity; }); @@ -396,8 +482,20 @@ public Iterable persistAll(@NonNull InsertBatchOperation operation) { if (storedQuery != null) { return executeUpdate(operation, session, storedQuery); } - for (T entity : operation) { - session.persist(entity); + if (persistOrMergeOnSave) { + RuntimePersistentEntity persistentEntity = getEntity(operation.getRootEntity()); + RuntimePersistentProperty identity = persistentEntity.getIdentity(); + for (T entity : operation) { + if (identity != null && identity.getProperty().get(entity) == null) { + session.persist(entity); + } else { + session.merge(entity); + } + } + } else { + for (T entity : operation) { + session.persist(entity); + } } flushIfNecessary(session, operation.getAnnotationMetadata()); return operation; @@ -517,30 +615,28 @@ public int delete(@NonNull DeleteOperation operation) { @Override public Optional deleteAll(@NonNull DeleteBatchOperation operation) { StoredQuery storedQuery = operation.getStoredQuery(); - Integer result = executeWrite(session -> { - if (storedQuery != null) { - int i = 0; - for (T entity : operation) { - i += executeUpdate(session, storedQuery, operation.getInvocationContext(), entity); - } - if (flushIfNecessary(session, operation.getAnnotationMetadata())) { - for (T entity : operation) { - session.remove(entity); - } - } - return i; - } - int i = 0; - for (T entity : operation) { - session.remove(entity); - i++; - } - return i; - }); - return Optional.ofNullable(result); - } - - private int executeUpdate(Session session, StoredQuery storedQuery, InvocationContext invocationContext, T entity) { + return executeWrite(session -> { + int deleted = 0; + if (storedQuery != null) { + for (T entity : operation) { + deleted += executeUpdate(session, storedQuery, operation.getInvocationContext(), entity); + } + if (flushIfNecessary(session, operation.getAnnotationMetadata())) { + for (T entity : operation) { + session.remove(entity); + } + } + } else { + for (T entity : operation) { + session.remove(entity); + deleted++; + } + } + return Optional.of(deleted); + }); + } + + private int executeUpdate(QueryProducer session, StoredQuery storedQuery, InvocationContext invocationContext, T entity) { MutationQuery query = session.createMutationQuery(storedQuery.getQuery()); bindParameters(query, storedQuery, invocationContext, entity); return query.executeUpdate(); @@ -549,23 +645,26 @@ private int executeUpdate(Session session, StoredQuery storedQuery, In @NonNull @Override public Stream findStream(@NonNull PreparedQuery preparedQuery) { - return executeRead(session -> { + Optional> connectionStatus = connectionOperations.findConnectionStatus(); + if (connectionStatus.isPresent()) { StreamResultCollector resultCollector = new StreamResultCollector<>(); - collectFindAll(session, preparedQuery, resultCollector); + collectFindAll(connectionStatus.get().getConnection(), preparedQuery, resultCollector); return resultCollector.result; + } + // No session is present, resolve the list completely + return executeRead(session -> { + ListResultCollector resultCollector = new ListResultCollector<>(); + collectFindAll(session, preparedQuery, resultCollector); + return resultCollector.result.stream(); }); } private R executeRead(Function callback) { - return transactionOperations.executeRead(status -> callback.apply(getCurrentSession())); + return transactionOperations.executeRead(status -> callback.apply(status.getConnection())); } private R executeWrite(Function callback) { - return transactionOperations.executeWrite(status -> callback.apply(getCurrentSession())); - } - - private Session getCurrentSession() { - return sessionFactory.getCurrentSession(); + return transactionOperations.executeWrite(status -> callback.apply(status.getConnection())); } @NonNull @@ -676,6 +775,33 @@ public Optional deleteAll(CriteriaDelete query) { return Optional.ofNullable(executeWrite(session -> session.createMutationQuery(query).executeUpdate())); } + private final class KeyedResultListCollector extends ResultCollector { + + private KeyedResultList result; + private final KeyedPage keyedPage; + + private KeyedResultListCollector(KeyedPage keyedPage) { + this.keyedPage = keyedPage; + } + + @Override + protected void collectTuple(Query query, Function fn) { + KeyedResultList keyedResultList = ((Query) query).getKeyedResultList(keyedPage); + result = new KeyedResultList<>( + keyedResultList.getResultList().stream().map(fn).toList(), + keyedResultList.getKeyList(), + keyedResultList.getPage(), + keyedResultList.getNextPage(), + keyedResultList.getPreviousPage() + ); + } + + @Override + protected void collect(Query query) { + result = ((Query) query).getKeyedResultList(keyedPage); + } + } + private final class ListResultCollector extends ResultCollector { private List result; @@ -724,6 +850,24 @@ protected void collect(Query query) { } } + private final class UniqueResultCollector extends ResultCollector { + + private R result; + + @Override + protected void collectTuple(Query query, Function fn) { + Tuple tuple = (Tuple) query.uniqueResult(); + if (tuple != null) { + this.result = fn.apply(tuple); + } + } + + @Override + protected void collect(Query query) { + result = (R) query.uniqueResult(); + } + } + private final class FirstResultCollector extends ResultCollector { private final boolean limitOne; diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Box.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Box.java new file mode 100644 index 0000000000..47a6d6974b --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Box.java @@ -0,0 +1,30 @@ +package io.micronaut.data.hibernate.jakarta_data.entity; + +import io.micronaut.core.annotation.Introspected; + +@jakarta.persistence.Entity +@Introspected(accessKind = Introspected.AccessKind.FIELD) +public class Box { + @jakarta.persistence.Id + public String boxIdentifier; + + public int length; + + public int width; + + public int height; + + public static Box of(String id, int length, int width, int height) { + Box box = new Box(); + box.boxIdentifier = id; + box.length = length; + box.width = width; + box.height = height; + return box; + } + + @Override + public String toString() { + return "Box@" + Integer.toHexString(hashCode()) + ":" + length + "x" + width + "x" + height + ":" + boxIdentifier; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Boxes.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Boxes.java new file mode 100644 index 0000000000..43ee08a879 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Boxes.java @@ -0,0 +1,11 @@ +package io.micronaut.data.hibernate.jakarta_data.entity; + +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Repository; + +/** + * A repository that inherits from the built-in BasicRepository and adds no methods. + */ +@Repository +public interface Boxes extends BasicRepository { +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Coordinate.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Coordinate.java new file mode 100644 index 0000000000..98f19e28d4 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/Coordinate.java @@ -0,0 +1,32 @@ +package io.micronaut.data.hibernate.jakarta_data.entity; + +import io.micronaut.core.annotation.Introspected; + +import java.util.UUID; + +/** + * This entity includes some field types that aren't covered elsewhere in the TCK. + */ +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +@jakarta.persistence.Entity +public class Coordinate { + @jakarta.persistence.Id + public UUID id; + + public double x; + + public float y; + + public static Coordinate of(String id, double x, float y) { + Coordinate c = new Coordinate(); + c.id = UUID.nameUUIDFromBytes(id.getBytes()); + c.x = x; + c.y = y; + return c; + } + + @Override + public String toString() { + return "Coordinate@" + Integer.toHexString(hashCode()) + "(" + x + "," + y + ")" + ":" + id; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/EntityTests.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/EntityTests.java new file mode 100644 index 0000000000..82505fa804 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/EntityTests.java @@ -0,0 +1,2490 @@ +package io.micronaut.data.hibernate.jakarta_data.entity; + +import io.micronaut.context.annotation.Property; +import io.micronaut.data.connection.ConnectionOperations; +import io.micronaut.data.hibernate.H2DBProperties; +import io.micronaut.data.hibernate.jakarta_data.read.only.AsciiCharacter; +import io.micronaut.data.hibernate.jakarta_data.read.only.AsciiCharacters; +import io.micronaut.data.hibernate.jakarta_data.read.only.AsciiCharactersPopulator; +import io.micronaut.data.hibernate.jakarta_data.read.only.CustomRepository; +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber; +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber.NumberType; +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumbers; +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumbersPopulator; +import io.micronaut.data.hibernate.jakarta_data.read.only.PositiveIntegers; +import io.micronaut.data.hibernate.jakarta_data.read.only._AsciiChar; +import io.micronaut.data.hibernate.jakarta_data.read.only._AsciiCharacter; +import io.micronaut.data.hibernate.jakarta_data.utilities.DatabaseType; +import io.micronaut.data.hibernate.jakarta_data.utilities.TestProperty; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.NonUniqueResultException; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.page.PageRequest.Cursor; +import jakarta.data.page.impl.PageRecord; +import jakarta.inject.Inject; +import jakarta.persistence.PersistenceException; +import org.hibernate.Session; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hibernate.query.Order.asc; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Execute a test with an entity that is dual annotated which means this test + * can run against a provider that supports any Entity type. + */ +//@Property(name = "jpa.default.properties.hibernate.show_sql", value = "true") +@Property(name = "jpa.default.properties.uniqueResultOnFindOne", value = "true") +@Property(name = "jpa.default.properties.persistOrMergeOnSave", value = "true") +@H2DBProperties +@MicronautTest(transactional = false) +public class EntityTests { + + public static final Logger log = Logger.getLogger(EntityTests.class.getCanonicalName()); + + @Inject + Boxes boxes; + + @Inject + NaturalNumbers numbers; + + @Inject + PositiveIntegers positives; // shares same read-only data with NaturalNumbers + + @Inject + CustomRepository customRepo; // shares same read-only data with NaturalNumbers + + @Inject + AsciiCharacters characters; + + @Inject + MultipleEntityRepo shared; + + @Inject + ConnectionOperations connectionOperations; + + @BeforeEach + //Inject doesn't happen until after BeforeClass so this is necessary before each test + public void setup() { + assertNotNull(numbers); + NaturalNumbersPopulator.get().populate(numbers); + + assertNotNull(characters); + AsciiCharactersPopulator.get().populate(characters); + } + + private DatabaseType type = TestProperty.databaseType.getDatabaseType(); + + @Test + public void ensureNaturalNumberPrepopulation() { + assertEquals(100L, numbers.countAll()); + assertTrue(numbers.findById(0L).isEmpty(), "Zero should not have been in the set of natural numbers."); + assertFalse(numbers.findById(10L).get().isOdd()); + } + + @Test + public void ensureCharacterPrepopulation() { + try { + assertEquals(127L, characters.countByHexadecimalNotNull()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of the Null comparison + } else { + throw x; + } + } + + assertEquals('0', characters.findByNumericValue(48).get().getThisCharacter()); + assertTrue(characters.findByNumericValue(1).get().isControl()); + } + + @Test + public void testBasicRepository() { + + // custom method from NaturalNumbers: + try { + Stream found = numbers.findByIdBetweenOrderByNumTypeOrdinalAsc( + 50L, 59L, + Order.by(Sort.asc("id"))); + List list = found + .map(NaturalNumber::getId) + .collect(Collectors.toList()); + assertEquals(List.of(53L, 59L, // first 2 must be primes + 50L, 51L, 52L, 54L, 55L, 56L, 57L, 58L), // the remaining 8 are composite numbers + list); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + } else { + throw x; + } + } + + // built-in method from BasicRepository: + assertEquals(60L, numbers.findById(60L).orElseThrow().getId()); + } + + @Test + public void testBasicRepositoryBuiltInMethods() { + + // BasicRepository.saveAll + Iterable saved = boxes.saveAll(List.of(Box.of("TestBasicRepositoryMethods-01", 119, 120, 169), + Box.of("TestBasicRepositoryMethods-02", 20, 21, 29), + Box.of("TestBasicRepositoryMethods-03", 33, 56, 65), + Box.of("TestBasicRepositoryMethods-04", 45, 28, 53))); + Iterator savedIt = saved.iterator(); + assertEquals(true, savedIt.hasNext()); + Box box1 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-01", box1.boxIdentifier); + assertEquals(119, box1.length); + assertEquals(120, box1.width); + assertEquals(169, box1.height); + assertEquals(true, savedIt.hasNext()); + Box box2 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(20, box2.length); + assertEquals(21, box2.width); + assertEquals(29, box2.height); + assertEquals(true, savedIt.hasNext()); + Box box3 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + assertEquals(true, savedIt.hasNext()); + Box box4 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-04", box4.boxIdentifier); + assertEquals(45, box4.length); + assertEquals(28, box4.width); + assertEquals(53, box4.height); + assertEquals(false, savedIt.hasNext()); + + + // BasicRepository.save + box2.length = 21; + box2.width = 20; + box2 = boxes.save(box2); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(21, box2.length); + assertEquals(20, box2.width); + assertEquals(29, box2.height); + + Box box5 = boxes.save(Box.of("TestBasicRepositoryMethods-05", 153, 104, 185)); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + + + // BasicRepository.deleteAll(Iterable) + boxes.deleteAll(List.of(box1, box2)); + + + + assertEquals(3, boxes.findAll().count()); + + + // BasicRepository.delete + boxes.delete(box4); + + + + // BasicRepository.findAll + Stream stream = boxes.findAll(); + List list = stream.sorted(Comparator.comparing(b -> b.boxIdentifier)).collect(Collectors.toList()); + assertEquals(2, list.size()); + box4 = list.get(0); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + box5 = list.get(1); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-03"); + + + + // BasicRepository.findById + assertEquals(false, boxes.findById("TestBasicRepositoryMethods-03").isPresent()); + box5 = boxes.findById("TestBasicRepositoryMethods-05").orElseThrow(); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-05"); + + + assertEquals(0, boxes.findAll().count()); + } + + @Test + public void testBasicRepositoryMethods() { + + // BasicRepository.saveAll + Iterable saved = boxes.saveAll(List.of(Box.of("TestBasicRepositoryMethods-01", 119, 120, 169), + Box.of("TestBasicRepositoryMethods-02", 20, 21, 29), + Box.of("TestBasicRepositoryMethods-03", 33, 56, 65), + Box.of("TestBasicRepositoryMethods-04", 45, 28, 53))); + Iterator savedIt = saved.iterator(); + assertEquals(true, savedIt.hasNext()); + Box box1 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-01", box1.boxIdentifier); + assertEquals(119, box1.length); + assertEquals(120, box1.width); + assertEquals(169, box1.height); + assertEquals(true, savedIt.hasNext()); + Box box2 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(20, box2.length); + assertEquals(21, box2.width); + assertEquals(29, box2.height); + assertEquals(true, savedIt.hasNext()); + Box box3 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + assertEquals(true, savedIt.hasNext()); + Box box4 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-04", box4.boxIdentifier); + assertEquals(45, box4.length); + assertEquals(28, box4.width); + assertEquals(53, box4.height); + assertEquals(false, savedIt.hasNext()); + + + + + // BasicRepository.save + box2.length = 21; + box2.width = 20; + box2 = boxes.save(box2); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(21, box2.length); + assertEquals(20, box2.width); + assertEquals(29, box2.height); + + Box box5 = boxes.save(Box.of("TestBasicRepositoryMethods-05", 153, 104, 185)); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + + + // BasicRepository.deleteAll(Iterable) + boxes.deleteAll(List.of(box1, box2)); + + + + assertEquals(3, boxes.findAll().count()); + + + // BasicRepository.delete + boxes.delete(box4); + + + + // BasicRepository.findAll + Stream stream = boxes.findAll(); + List list = stream.sorted(Comparator.comparing(b -> b.boxIdentifier)).collect(Collectors.toList()); + assertEquals(2, list.size()); + box4 = list.get(0); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + box5 = list.get(1); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-03"); + + + + // BasicRepository.findById + assertEquals(false, boxes.findById("TestBasicRepositoryMethods-03").isPresent()); + box5 = boxes.findById("TestBasicRepositoryMethods-05").orElseThrow(); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-05"); + + + + assertEquals(0, boxes.findAll().count()); + } + + @Test + public void testBeyondFinalPage() { + PageRequest sixth = PageRequest.ofPage(6).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, sixth, Order.by(_AsciiCharacter.numericValue.asc())); + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testBeyondFinalSlice() { + PageRequest sixth = PageRequest.ofPage(6).size(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, + 8L, + sixth, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + } + + @Test + public void testBy() { + AsciiCharacter ch = characters.find('L', "4c").orElseThrow(); + assertEquals('L', ch.getThisCharacter()); + assertEquals("4c", ch.getHexadecimal()); + assertEquals(76L, ch.getId()); + assertEquals(false, ch.isControl()); + + assertEquals(true, characters.find('M', "4b").isEmpty()); + } + + @Test + public void testCommonInterfaceQueries() { + + try { + assertEquals(4L, numbers.countByIdBetween(87L, 90L)); + + assertEquals(5L, characters.countByIdBetween(86L, 90L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of Between + } else { + throw x; + } + } + + assertEquals(true, numbers.existsById(73L)); + + assertEquals(true, characters.existsById(74L)); + + assertEquals(false, numbers.existsById(-1L)); + + assertEquals(false, characters.existsById(-2L)); + + try { + assertEquals( + List.of(68L, 69L, 70L, 71L, 72L), + characters.withIdEqualOrAbove(68L, Limit.of(5))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of >= in JDQL + } else { + throw x; + } + } + + assertEquals(List.of(71L, 72L, 73L, 74L, 75L), + numbers.withIdEqualOrAbove(71L, Limit.of(5))); + } + + @Test + public void testContainsInString() { + Collection found; + try { + found = characters.findByHexadecimalContainsAndIsControlNot("4", true); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Contains. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + + assertEquals(List.of("24", "34", + "40", "41", "42", "43", + "44", "45", "46", "47", + "48", "49", "4a", "4b", + "4c", "4d", "4e", "4f", + "54", "64", "74"), + found.stream().map(AsciiCharacter::getHexadecimal).sorted().toList()); + } + + @Test + public void testDataRepository() { + try { + AsciiCharacter del = characters.findByIsControlTrueAndNumericValueBetween(33, 127); + assertEquals(127, del.getNumericValue()); + assertEquals("7f", del.getHexadecimal()); + assertEquals(true, del.isControl()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + } else { + throw x; + } + } + + try { + AsciiCharacter j = characters.findByHexadecimalIgnoreCase("6A"); + assertEquals("6a", j.getHexadecimal()); + assertEquals('j', j.getThisCharacter()); + assertEquals(106, j.getNumericValue()); + assertEquals(false, j.isControl()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + + AsciiCharacter d = characters.findByNumericValue(100).orElseThrow(); + assertEquals(100, d.getNumericValue()); + assertEquals('d', d.getThisCharacter()); + assertEquals("64", d.getHexadecimal()); + assertEquals(false, d.isControl()); + + assertEquals(true, characters.existsByThisCharacter('D')); + } + + @Test + public void testDefaultMethod() { + try { + assertEquals(List.of('W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd'), + characters.retrieveAlphaNumericIn(87L, 100L) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of Between + } else { + throw x; + } + } + } + + @Test + public void testDescendingSort() { + Stream stream; + try { + stream = characters.findByIdBetween( + 52L, 57L, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Character[]{'9', '8', '7', '6', '5', '4'}), + Arrays.toString(stream.map(AsciiCharacter::getThisCharacter).toArray())); + } + + @Test + public void testEmptyQuery() { + + try { + assertEquals(List.of('a', 'b', 'c', 'd', 'e', 'f'), + characters.all(Limit.range(97, 102), Sort.asc("id")) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + } + + @Test + public void testEmptyResultException() { + try { + AsciiCharacter ch = characters.findByHexadecimalIgnoreCase("2g"); + fail("Unexpected result of findByHexadecimalIgnoreCase(2g): " + ch.getHexadecimal()); + } catch (EmptyResultException x) { + log.info("testEmptyResultException expected to catch exception " + x + ". Printing its stack trace:"); + x.printStackTrace(System.out); + // test passes + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + return; // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + } + + @Test + public void testFalse() { + List even; + try { + even = positives.findByIsOddFalseAndIdBetween(50L, 60L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + + assertEquals(6L, even.stream().count()); + + assertEquals(List.of(50L, 52L, 54L, 56L, 58L, 60L), + even.stream().map(NaturalNumber::getId).sorted().collect(Collectors.toList())); + } + + @Test + public void testFinalPageOfUpTo10() { + PageRequest fifthPageRequest = PageRequest.ofPage(5).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, fifthPageRequest, + Order.by(_AsciiCharacter.numericValue.asc())); // 'X' to 'Z' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + AsciiCharacter ch = it.next(); + assertEquals('X', ch.getThisCharacter()); + assertEquals("58", ch.getHexadecimal()); + assertEquals(88L, ch.getId()); + assertEquals(88, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + // second result + ch = it.next(); + assertEquals('Y', ch.getThisCharacter()); + assertEquals("59", ch.getHexadecimal()); + assertEquals(89L, ch.getId()); + assertEquals(89, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + // third result + ch = it.next(); + assertEquals('Z', ch.getThisCharacter()); + assertEquals("5a", ch.getHexadecimal()); + assertEquals(90L, ch.getId()); + assertEquals(90, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + assertEquals(false, it.hasNext()); + + assertEquals(5, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(3, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testFinalSliceOfUpTo5() { + PageRequest fifth = PageRequest.ofPage(5).size(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, + 8L, + fifth, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(true, page.hasContent()); + assertEquals(5, page.pageRequest().page()); + assertEquals(2, page.numberOfElements()); + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + NaturalNumber number = it.next(); + assertEquals(3L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(1L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 2), number.getNumBitsRequired()); + + // second result + assertEquals(true, it.hasNext()); + number = it.next(); + assertEquals(2L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(1L, number.getFloorOfSquareRoot()); + assertEquals(false, number.isOdd()); + assertEquals(Short.valueOf((short) 2), number.getNumBitsRequired()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testFindAllWithPagination() { + PageRequest page2request = PageRequest.ofPage(2).size(12); + Page page2; + try { + page2 = positives.findAll(page2request, + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + + assertEquals(12, page2.numberOfElements()); + assertEquals(2, page2.pageRequest().page()); + + assertEquals(List.of(11L, 10L, 9L, // square root rounds down to 3 + 24L, 23L, 22L, 21L, 20L, 19L, 18L, 17L, 16L), // square root rounds down to 4 + page2.stream().map(n -> n.getId()).collect(Collectors.toList())); + } + + @Test + public void testFindFirst() { + Optional none; + try { + none = characters.findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc( + "h", false); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of StartsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + assertEquals(true, none.isEmpty()); + + AsciiCharacter ch = characters.findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc("4", false) + .orElseThrow(); + assertEquals('@', ch.getThisCharacter()); + assertEquals("40", ch.getHexadecimal()); + assertEquals(64, ch.getNumericValue()); + } + + @Test + public void testFindFirst3() { + AsciiCharacter[] found; + + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 40, "4", Sort.asc("numericValue")); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + + assertEquals(3, found.length); + assertEquals('4', found[0].getThisCharacter()); + assertEquals('D', found[1].getThisCharacter()); + assertEquals('T', found[2].getThisCharacter()); + } + + @Test + public void testFindList() { + List oddCompositeNumbers; + try { + oddCompositeNumbers = positives.findOdd( + true, + NumberType.COMPOSITE, + Limit.of(10), + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("numBitsRequired"), + Sort.asc("id"))); + + + assertEquals(List.of(9L, 15L, // 3 <= sqrt < 4, 4 bits + 21L, // 4 <= sqrt < 5, 5 bits + 33L, 35L, // 5 <= sqrt < 6, 6 bits + 25L, 27L, // 5 <= sqrt < 6, 5 bits + 39L, 45L, // 6 <= sqrt < 7, 6 bits + 49L), // 7 <= sqrt < 8, 6 bits + oddCompositeNumbers + .stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + } else { + throw x; + } + } + + List evenPrimeNumbers = positives.findOdd(false, NumberType.PRIME, Limit.of(9), Order.by()); + + assertEquals(1, evenPrimeNumbers.size()); + NaturalNumber num = evenPrimeNumbers.get(0); + assertEquals(2L, num.getId()); + assertEquals(1L, num.getFloorOfSquareRoot()); + assertEquals(Short.valueOf((short) 2), num.getNumBitsRequired()); + assertEquals(NumberType.PRIME, num.getNumType()); + assertEquals(false, num.isOdd()); + } + + @Test + public void testFindOne() { + AsciiCharacter j = characters.find('j'); + + assertEquals("6a", j.getHexadecimal()); + assertEquals(106L, j.getId()); + assertEquals(106, j.getNumericValue()); + assertEquals('j', j.getThisCharacter()); + } + + @Test + public void testFindOptional() { + NaturalNumber num = positives.findNumber(67L).orElseThrow(); + + assertEquals(67L, num.getId()); + assertEquals(8L, num.getFloorOfSquareRoot()); + assertEquals(Short.valueOf((short) 7), num.getNumBitsRequired()); + assertEquals(NumberType.PRIME, num.getNumType()); + assertEquals(true, num.isOdd()); + + Optional opt = positives.findNumber(-40L); + + assertEquals(false, opt.isPresent()); + } + + @Test + public void testFindPage() { + PageRequest page1Request = PageRequest.ofSize(7); + + Page page1; + try { + page1 = positives.findMatching( + 9L, + Short.valueOf((short) 7), + NumberType.COMPOSITE, + page1Request, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + + assertEquals(List.of(99L, 98L, 96L, 95L, 94L, 93L, 92L), + page1.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(true, page1.hasNext()); + + Page page2 = positives.findMatching(9L, Short.valueOf((short) 7), NumberType.COMPOSITE, + page1.nextPageRequest(), Sort.desc("id")); + + assertEquals(List.of(91L, 90L, 88L, 87L, 86L, 85L, 84L), + page2.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(true, page2.hasNext()); + + Page page3 = positives.findMatching(9L, Short.valueOf((short) 7), NumberType.COMPOSITE, + page2.nextPageRequest(), Sort.desc("id")); + + assertEquals(List.of(82L, 81L), + page3.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(false, page3.hasNext()); + } + + @Test + public void testFirstCursoredPageOf8AndNextPages() { + // The query for this test returns 1-15,25-32 in the following order: + + // 32 requires 6 bits + // 25, 26, 27, 28, 29, 30, 31 requires 5 bits + // 8, 9, 10, 11, 12, 13, 14, 15 requires 4 bits + // 4, 5, 6, 7, 8 requires 3 bits + // 2, 3 requires 2 bits + // 1 requires 1 bit + + Order order = Order.by(Sort.asc("id")); + PageRequest first8 = PageRequest.ofSize(8); + CursoredPage page; + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, first8, order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(8, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{32L, 25L, 26L, 27L, 28L, 29L, 30L, 31L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, page.nextPageRequest(), order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(8, page.numberOfElements()); + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, page.nextPageRequest(), order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(7, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{4L, 5L, 6L, 7L, 2L, 3L, 1L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testFirstCursoredPageWithoutTotalOf6AndNextPages() { + PageRequest first6 = PageRequest.ofSize(6).withoutTotal(); + CursoredPage slice; + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, first6); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(Arrays.toString(new Long[]{49L, 50L, 51L, 52L, 53L, 54L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(6, slice.numberOfElements()); + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(6, slice.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{55L, 56L, 57L, 58L, 59L, 60L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{61L, 62L, 63L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(3, slice.numberOfElements()); + } + + @Test + public void testFirstPageOf10() { + PageRequest first10 = PageRequest.ofSize(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, first10, + Order.by(_AsciiCharacter.numericValue.asc())); // '0' to 'Z' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(1, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("30:0;31:1;32:2;33:3;34:4;35:5;36:6;37:7;38:8;39:9;", // '0' to '9' + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + } + + @Test + public void testFirstSliceOf5() { + PageRequest first5 = PageRequest.ofSize(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, + 8L, + first5, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(5, page.numberOfElements()); + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + NaturalNumber number = it.next(); + assertEquals(79L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(8L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 7), number.getNumBitsRequired()); + + // second result + assertEquals(true, it.hasNext()); + assertEquals(73L, it.next().getId()); + + // third result + assertEquals(true, it.hasNext()); + assertEquals(71L, it.next().getId()); + + // fourth result + assertEquals(true, it.hasNext()); + assertEquals(67L, it.next().getId()); + + // fifth result + assertEquals(true, it.hasNext()); + number = it.next(); + assertEquals(61L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(7L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 6), number.getNumBitsRequired()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testGreaterThanEqualExists() { + try { + assertEquals(true, positives.existsByIdGreaterThan(0L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of GreaterThan + } else { + throw x; + } + } + assertEquals(true, positives.existsByIdGreaterThan(99L)); + assertEquals(false, positives.existsByIdGreaterThan(100L)); // doesn't exist because the table only has 1 to 100 + } + + @Test + public void testIn() { + Stream nonPrimes; + try { + nonPrimes = positives.findByNumTypeInOrderByIdAsc( + Set.of(NumberType.COMPOSITE, NumberType.ONE), + Limit.of(9)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of In + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + + assertEquals(List.of(1L, 4L, 6L, 8L, 9L, 10L, 12L, 14L, 15L), + nonPrimes.map(NaturalNumber::getId).collect(Collectors.toList())); + + Stream primes = positives.findByNumTypeInOrderByIdAsc(Collections.singleton(NumberType.PRIME), + Limit.of(6)); + assertEquals(List.of(2L, 3L, 5L, 7L, 11L, 13L), + primes.map(NaturalNumber::getId).collect(Collectors.toList())); + } + + @Test + public void testIgnoreCase() { + Stream found; + try { + found = characters.findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn( + "4c", "5A", Set.of("5"), + Order.by(Sort.asc("hexadecimal"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of IgnoreCase + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Column and Key-Value databases might not be capable of In + // Column and Key-Value databases might not be capable of sorting. + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + + assertEquals(List.of(Character.valueOf('L'), // 4c + Character.valueOf('M'), // 4d + Character.valueOf('N'), // 4e + Character.valueOf('O'), // 4f + Character.valueOf('P'), // 50 + Character.valueOf('Q'), // 51 + Character.valueOf('R'), // 52 + Character.valueOf('S'), // 53 + Character.valueOf('T'), // 54 + Character.valueOf('U'), // 55 + Character.valueOf('V'), // 56 + Character.valueOf('W'), // 57 + Character.valueOf('X'), // 58 + Character.valueOf('Y'), // 59 + Character.valueOf('Z')), // 5a + found.map(AsciiCharacter::getThisCharacter).collect(Collectors.toList())); + } + + @Test + public void testCursoredPageOf7FromCursor() { + // The query for this test returns 1-35 and 49 in the following order: + // + // 35 34 33 32 49 24 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 08 15 14 13 12 11 10 09 07 06 05 04 03 02 01 + // ^^^^^^ page 1 ^^^^^^ + // ^^^ previous page ^^ + // ^^^^^ next page ^^^^ + + Order order = Order.by(Sort.asc("floorOfSquareRoot"), Sort.desc("id")); + PageRequest middle7 = PageRequest.afterCursor( + Cursor.forKey((short) 5, 5L, 26L), // 20th result is 26; it requires 5 bits and its square root rounds down to 5.), + 4L, 7, true); + + CursoredPage page; + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, middle7, order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(Arrays.toString(new Long[]{25L, // 5 bits required, square root rounds down to 5 + 8L, // 4 bits required, square root rounds down to 2 + 15L, 14L, 13L, 12L, 11L // 4 bits required, square root rounds down to 3 + }), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, page.numberOfElements()); + + assertEquals(true, page.hasPrevious()); + + CursoredPage previousPage; + try { + previousPage = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, + page.previousPageRequest(), + order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{16L, // 4 bits required, square root rounds down to 4 + 31L, 30L, 29L, 28L, 27L, 26L // 5 bits required, square root rounds down to 5 + }), + Arrays.toString(previousPage.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, previousPage.numberOfElements()); + + CursoredPage nextPage; + try { + nextPage = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, + page.nextPageRequest(), + order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{10L, 9L, // 4 bits required, square root rounds down to 3 + 7L, 6L, 5L, 4L, // 3 bits required, square root rounds down to 2 + 3L // 2 bits required, square root rounds down to 1 + }), + Arrays.toString(nextPage.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, nextPage.numberOfElements()); + } + + @Test + public void testCursoredPageOfNothing() { + + CursoredPage page; + try { + // There are no positive integers less than 4 which have a square root that rounds down to something other than 1. + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(1L, 4L, PageRequest.ofPage(1L), Order.by()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(false, page.hasContent()); + assertEquals(false, page.hasNext()); + assertEquals(false, page.hasPrevious()); + assertEquals(0, page.content().size()); + assertEquals(0, page.numberOfElements()); + + try { + page.nextPageRequest(); + fail("nextPageRequest must raise NoSuchElementException when current page is empty."); + } catch (NoSuchElementException x) { + // expected + } + + try { + page.previousPageRequest(); + fail("previousPageRequest must raise NoSuchElementException when current page is empty."); + } catch (NoSuchElementException x) { + // expected + } + } + + @Test + public void testCursoredPageWithoutTotalOf9FromCursor() { + // The query for this test returns composite natural numbers under 64 in the following order: + // + // 49 50 51 52 54 55 56 57 58 60 62 63 36 38 39 40 42 44 45 46 48 25 26 27 28 30 32 33 34 35 16 18 20 21 22 24 09 10 12 14 15 04 06 08 + // ^^^^^^^^ slice 1 ^^^^^^^^^ + // ^^^^^^^^ slice 2 ^^^^^^^^^ + // ^^^^^^^^ slice 3 ^^^^^^^^^ + + PageRequest middle9 = PageRequest.afterCursor( + Cursor.forKey(6L, 46L), // 20th result is 46; its square root rounds down to 6. + 4L, 9, false); + Order order = Order.by(Sort.desc("floorOfSquareRoot"), Sort.asc("id")); + + CursoredPage slice; + try { + slice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, (short) 7, order, middle9); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(Arrays.toString(new Long[]{48L, 25L, 26L, 27L, 28L, 30L, 32L, 33L, 34L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, slice.numberOfElements()); + + assertEquals(true, slice.hasPrevious()); + CursoredPage previousSlice; + try { + previousSlice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, + (short) 7, + order, + slice.previousPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{63L, 36L, 38L, 39L, 40L, 42L, 44L, 45L, 46L}), + Arrays.toString(previousSlice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, previousSlice.numberOfElements()); + + CursoredPage nextSlice; + try { + nextSlice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, + (short) 7, + order, + slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{35L, 16L, 18L, 20L, 21L, 22L, 24L, 9L, 10L}), + Arrays.toString(nextSlice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, nextSlice.numberOfElements()); + } + + @Test + public void testCursoredPageWithoutTotalOfNothing() { + // There are no numbers larger than 30 which have a square root that rounds down to 3. + PageRequest pagination = PageRequest.ofSize(33).afterCursor(Cursor.forKey(30L)).withoutTotal(); + + CursoredPage slice; + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(3L, pagination); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(false, slice.hasContent()); + assertEquals(0, slice.content().size()); + assertEquals(0, slice.numberOfElements()); + } + + @Test + public void testLessThanWithCount() { + try { + assertEquals(91L, positives.countByIdLessThan(92L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of LessThan + } else { + throw x; + } + } + + assertEquals(0L, positives.countByIdLessThan(1L)); + } + + @Test + public void testLimit() { + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual( + 60L, + Limit.of(10), + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases are not capable of GreaterThanEqual + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{63L, 62L, 61L, 60L, // square root rounds down to 7 + 80L, 79L, 78L, 77L, 76L, 75L}), // square root rounds down to 8 + Arrays.toString(nums.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testLimitedRange() { + // Primes above 40 are: + // 41, 43, 47, 53, 59, + // 61, 67, 71, 73, 79, + // 83, 89, ... + + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual( + 40L, + Limit.range(6, 10), + Order.by( + Sort.asc("numTypeOrdinal"), // primes first + Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases are not capable of GreaterThanEqual + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{61L, 67L, 71L, 73L, 79L}), + Arrays.toString(nums.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testLimitToOneResult() { + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual(80L, Limit.of(1), Order.by()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of GreaterThanEqual + } else { + throw x; + } + } + + Iterator it = nums.iterator(); + assertEquals(true, it.hasNext()); + + NaturalNumber num = it.next(); + assertEquals(true, num.getId() >= 80L); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testLiteralEnumAndLiteralFalse() { + + NaturalNumber two; + try { + two = numbers.two().orElseThrow(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of JDQL TRUE/FALSE + } else { + throw x; + } + } + + assertEquals(2L, two.getId()); + assertEquals(NumberType.PRIME, two.getNumType()); + assertEquals(Short.valueOf((short) 2), two.getNumBitsRequired()); + } + + @Test + public void testLiteralInteger() { + + try { + assertEquals(24, characters.twentyFour()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of <= in JDQL. + // Key-Value databases might not be capable of JDQL AND. + return; + } else { + throw x; + } + } + } + + @Test + public void testLiteralString() { + + try { + assertEquals(List.of('J', 'K', 'L', 'M'), + characters.jklOr("4d") + .map(AsciiCharacter::getThisCharacter) + .sorted() + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Key-Value databases might not be capable of JDQL AND. + // Column and Key-Value databases might not be capable of JDQL IN + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + } + + @Disabled // PENDING FEATURE + @Test + public void testLiteralTrue() { + Page page1; + try { + page1 = numbers.oddsFrom21To(40L, PageRequest.ofSize(5)); +// page1 = connectionOperations.executeRead(status -> { +// return oddsFrom21To(status.getConnection(), 40L, PageRequest.ofSize(5)); +// }); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of JDQL BETWEEN + // Key-Value databases are not capable of JDQL TRUE/FALSE + return; + } else { + throw x; + } + } + + try { + assertEquals(10L, page1.totalElements()); + assertEquals(2L, page1.totalPages()); + + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + assertEquals(List.of(21L, 23L, 25L, 27L, 29L), page1.content()); + + assertEquals(true, page1.hasNext()); + + Page page2 = numbers.oddsFrom21To(40L, page1.nextPageRequest()); + + assertEquals(List.of(31L, 33L, 35L, 37L, 39L), page2.content()); + + if (page2.hasNext()) { + Page page3 = numbers.oddsFrom21To(40L, page2.nextPageRequest()); + assertEquals(false, page3.hasContent()); + assertEquals(false, page3.hasNext()); + } + } + + public Page oddsFrom21To(Session session, long max, PageRequest pageRequest) { + var _orders = new ArrayList>(); + _orders.add(asc(Long.class, "id")); + try { + long _totalResults = + pageRequest.requestTotal() + ? session.createSelectionQuery("SELECT naturalNumber_.id FROM io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber AS naturalNumber_ WHERE (naturalNumber_.isOdd = TRUE AND (naturalNumber_.id >= 21 AND naturalNumber_.id <= :p1))", Long.class) + .setParameter("p1", max) + .getResultCount() + : -1; + var _results = session.createSelectionQuery("SELECT naturalNumber_.id FROM io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber AS naturalNumber_ WHERE (naturalNumber_.isOdd = TRUE AND (naturalNumber_.id >= 21 AND naturalNumber_.id <= :p1))", Long.class) + .setParameter("p1", max) + .setFirstResult((int) (pageRequest.page()-1) * pageRequest.size()) + .setMaxResults(pageRequest.size()) + .setOrder(_orders) + .getResultList(); + return new PageRecord(pageRequest, _results, _totalResults); + } + catch (PersistenceException _ex) { + throw new DataException(_ex.getMessage(), _ex); + } + } + + @Test + public void testMixedSort() { + NaturalNumber[] nums; + try { + nums = numbers.findByIdLessThan( + 15L, + Sort.asc("numBitsRequired"), + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThan. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{1L, // 1 bit + 3L, 2L, // 2 bits + 7L, 6L, 5L, 4L, // 3 bits + 14L, 13L, 12L, 11L, 10L, 9L, 8L}), // 4 bits + Arrays.toString(Stream.of(nums).map(number -> number.getId()).toArray())); + } + + @Test + public void testNonUniqueResultException() { + try { + AsciiCharacter ch = characters.findByIsControlTrueAndNumericValueBetween(10, 15); + fail("Unexpected result of findByIsControlTrueAndNumericValueBetween(10, 15): " + ch.getHexadecimal()); + } catch (NonUniqueResultException x) { + log.info("testNonUniqueResultException expected to catch exception " + x + ". Printing its stack trace:"); + x.printStackTrace(System.out); + // test passes + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + } + + @Test + public void testNot() { + NaturalNumber[] n; + try { + n = numbers.findByNumTypeNot( + NumberType.COMPOSITE, + Limit.of(8), + Order.by(Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + assertEquals(8, n.length); + assertEquals(1L, n[0].getId()); + assertEquals(2L, n[1].getId()); + assertEquals(3L, n[2].getId()); + assertEquals(5L, n[3].getId()); + assertEquals(7L, n[4].getId()); + assertEquals(11L, n[5].getId()); + assertEquals(13L, n[6].getId()); + assertEquals(17L, n[7].getId()); + } + + @Test + public void testOr() { + Stream found; + try { + found = positives.findByNumTypeOrFloorOfSquareRoot(NumberType.ONE, 2L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of Or. + return; + } else { + throw x; + } + } + + assertEquals(List.of(1L, 4L, 5L, 6L, 7L, 8L), + found.map(NaturalNumber::getId).sorted().collect(Collectors.toList())); + } + + @Test + public void testOrderByHasPrecedenceOverPageRequestSorts() { + PageRequest pagination = PageRequest.ofSize(8); + Order order = Order.by(Sort.asc("numTypeOrdinal"), Sort.desc("id")); + + Page page; + try { + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc( + 25L, pagination, order); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThan. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{23L, 19L, 17L, // square root rounds down to 4; prime + 24L, 22L, 21L, 20L, 18L}), // square root rounds down to 4; composite + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + + assertEquals(Arrays.toString(new Long[]{16L, // square root rounds down to 4; composite + 13L, 11L, // square root rounds down to 3; prime + 15L, 14L, 12L, 10L, 9L}), // square root rounds down to 3; composite + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + + assertEquals(Arrays.toString(new Long[]{7L, 5L, // square root rounds down to 2; prime + 8L, 6L, 4L, // square root rounds down to 2; composite + 1L, // square root rounds down to 1; one + 3L, 2L}), // square root rounds down to 1; prime + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + if (page.hasNext()) { + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + assertEquals(false, page.hasContent()); + } + } + + @Test + public void testOrderByHasPrecedenceOverSorts() { + Stream nums; + try { + nums = numbers.findByIdBetweenOrderByNumTypeOrdinalAsc( + 5L, 24L, + Order.by(Sort.desc("floorOfSquareRoot"), Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{17L, 19L, 23L, // prime; square root rounds down to 4 + 11L, 13L, // prime; square root rounds down to 3 + 5L, 7L, // prime; square root rounds down to 2 + 16L, 18L, 20L, 21L, 22L, 24L, // composite; square root rounds down to 4 + 9L, 10L, 12L, 14L, 15L, // composite; square root rounds down to 3 + 6L, 8L}), // composite; square root rounds down to 2 + Arrays.toString(nums.map(number -> number.getId()).toArray())); + } + + @Test + public void testPageOfNothing() { + PageRequest pagination = PageRequest.ofSize(6); + Page page; + try { + page = characters.findByNumericValueBetween(150, 160, pagination, + Order.by(_AsciiCharacter.id.asc())); + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(0, page.content().size()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + try { + assertEquals(0L, page.totalElements()); + assertEquals(0L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testPartialQueryOrderBy() { + + assertEquals(List.of('A', 'B', 'C', 'D', 'E', 'F'), + characters.alphabetic(Limit.range(65, 70)) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testPartialQuerySelectAndOrderBy() { + + Character[] chars = characters.reverseAlphabetic(Limit.range(6, 13)); + for (int i = 0; i < chars.length; i++) { + assertEquals("zyxwvuts".charAt(i), chars[i]); + } + } + + @Test + public void testPrimaryEntityClassDeterminedByLifeCycleMethods() { + assertEquals(4L, customRepo.countByIdIn(Set.of(2L, 15L, 37L, -5L, 60L))); + + assertEquals(true, customRepo.existsByIdIn(Set.of(17L, 14L, -1L))); + + assertEquals(false, customRepo.existsByIdIn(Set.of(-10L, -12L, -14L))); + } + + @Test + public void testQueryWithNot() { + + // 'NOT LIKE' excludes '@' + // 'NOT IN' excludes 'E' and 'G' + // 'NOT BETWEEN' excludes 'H' through 'N'. + Character[] abcdfo; + try { + abcdfo = characters.getABCDFO(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Like + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(6, abcdfo.length); + for (int i = 0; i < abcdfo.length; i++) { + assertEquals("ABCDFO".charAt(i), abcdfo[i]); + } + } + + @Test + public void testQueryWithNull() { + try { + assertEquals("4a", characters.hex('J').orElseThrow()); + assertEquals("44", characters.hex('D').orElseThrow()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Contains. + // Key-Value databases might not be capable of And. + return; + } else { + throw x; + } + } + } + + @Test + public void testQueryWithOr() { + PageRequest page1Request = PageRequest.ofSize(4); + CursoredPage page1; + + try { + page1 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page1Request); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of JPQL OR. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(List.of(16L, 18L, 8L, 9L), + page1.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + assertEquals(true, page1.hasTotals()); + assertEquals(true, page1.hasNext()); + try { + assertEquals(3L, page1.totalPages()); + assertEquals(12L, page1.totalElements()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + CursoredPage page2; + + try { + page2 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page1.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(List.of(10L, 11L, 12L, 13L), + page2.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + assertEquals(true, page2.hasNext()); + + CursoredPage page3 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page2.nextPageRequest()); + + assertEquals(List.of(14L, 15L, 4L, 6L), + page3.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + if (page3.hasNext()) { + CursoredPage page4 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page3.nextPageRequest()); + assertEquals(false, page4.hasContent()); + } + } + + @Test + public void testQueryWithParenthesis() { + + try { + assertEquals( + List.of(15L, 7L, 5L, 3L, 1L), + positives.oddAndEqualToOrBelow(15L, 9L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.DOCUMENT)) { + // Document, Column, and Key-Value databases might not be capable of parentheses. + // Column and Key-Value databases might not be capable of JDQL OR. + // Key-Value databases might not be capable of < in JDQL. + // Key-Value databases might not be capable of JDQL AND. + return; + } else { + throw x; + } + } + } + + @Test + public void testSingleEntity() { + AsciiCharacter ch; + try { + ch = characters.findByHexadecimalIgnoreCase("2B"); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + return; // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + + assertEquals('+', ch.getThisCharacter()); + assertEquals("2b", ch.getHexadecimal()); + assertEquals(43, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + } + + @Test + public void testSliceOfNothing() { + PageRequest pagination = PageRequest.ofSize(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.COMPOSITE, 1L, pagination, Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(false, page.hasContent()); + assertEquals(0, page.content().size()); + assertEquals(0, page.numberOfElements()); + } + + @Test + public void testStaticMetamodelAscendingSorts() { + assertEquals(Sort.asc("id"), _AsciiChar.id.asc()); + assertEquals(Sort.ascIgnoreCase(_AsciiChar.HEXADECIMAL), _AsciiChar.hexadecimal.ascIgnoreCase()); + assertEquals(Sort.ascIgnoreCase("thisCharacter"), _AsciiChar.thisCharacter.ascIgnoreCase()); + + PageRequest pageRequest = PageRequest.ofSize(6); + Page page1; + try { + page1 = characters.findByNumericValueBetween( + 68, 90, pageRequest, + Order.by(_AsciiChar.numericValue.asc())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(List.of('D', 'E', 'F', 'G', 'H', 'I'), + page1.stream() + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testStaticMetamodelAscendingSortsPreGenerated() { + assertEquals(Sort.asc("id"), _AsciiCharacter.id.asc()); + assertEquals(Sort.asc("isControl"), _AsciiCharacter.isControl.asc()); + assertEquals(Sort.ascIgnoreCase(_AsciiCharacter.HEXADECIMAL), _AsciiCharacter.hexadecimal.ascIgnoreCase()); + assertEquals(Sort.ascIgnoreCase("thisCharacter"), _AsciiCharacter.thisCharacter.ascIgnoreCase()); + + PageRequest pageRequest = PageRequest.ofSize(7); + Page page1; + try { + page1 = characters.findByNumericValueBetween( + 100, 122, pageRequest, + Order.by(_AsciiCharacter.numericValue.asc())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(List.of('d', 'e', 'f', 'g', 'h', 'i', 'j'), + page1.stream() + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testStaticMetamodelAttributeNames() { + assertEquals(_AsciiChar.HEXADECIMAL, _AsciiChar.hexadecimal.name()); + assertEquals(_AsciiChar.ID, _AsciiChar.id.name()); + assertEquals("isControl", _AsciiChar.isControl.name()); + assertEquals(_AsciiChar.NUMERICVALUE, _AsciiChar.numericValue.name()); + assertEquals("thisCharacter", _AsciiChar.thisCharacter.name()); + } + + @Test + public void testStaticMetamodelAttributeNamesPreGenerated() { + assertEquals(_AsciiCharacter.HEXADECIMAL, _AsciiCharacter.hexadecimal.name()); + assertEquals(_AsciiCharacter.ID, _AsciiCharacter.id.name()); + assertEquals("isControl", _AsciiCharacter.isControl.name()); + assertEquals(_AsciiChar.NUMERICVALUE, _AsciiCharacter.numericValue.name()); + assertEquals("thisCharacter", _AsciiCharacter.thisCharacter.name()); + } + + @Test + public void testStaticMetamodelDescendingSorts() { + assertEquals(Sort.desc(_AsciiChar.ID), _AsciiChar.id.desc()); + assertEquals(Sort.descIgnoreCase("hexadecimal"), _AsciiChar.hexadecimal.descIgnoreCase()); + assertEquals(Sort.descIgnoreCase("thisCharacter"), _AsciiChar.thisCharacter.descIgnoreCase()); + + Sort sort = _AsciiChar.numericValue.desc(); + AsciiCharacter[] found; + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 30, "1", sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GreaterThanEqual. + return; + } else { + throw x; + } + } + assertEquals(3, found.length); + assertEquals('q', found[0].getThisCharacter()); + assertEquals('a', found[1].getThisCharacter()); + assertEquals('Q', found[2].getThisCharacter()); + } + + @Test + public void testStaticMetamodelDescendingSortsPreGenerated() { + assertEquals(Sort.desc(_AsciiCharacter.ID), _AsciiCharacter.id.desc()); + assertEquals(Sort.desc("isControl"), _AsciiCharacter.isControl.desc()); + assertEquals(Sort.descIgnoreCase("hexadecimal"), _AsciiCharacter.hexadecimal.descIgnoreCase()); + assertEquals(Sort.descIgnoreCase("thisCharacter"), _AsciiCharacter.thisCharacter.descIgnoreCase()); + + Sort sort = _AsciiCharacter.numericValue.desc(); + AsciiCharacter[] found; + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 30, "4", sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GreaterThanEqual. + return; + } else { + throw x; + } + } + assertEquals(3, found.length); + assertEquals('t', found[0].getThisCharacter()); + assertEquals('d', found[1].getThisCharacter()); + assertEquals('T', found[2].getThisCharacter()); + } + + @Test + public void testStreamsFromList() { + List chars; + try { + chars = characters.findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual( + 109, 101); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GTE/LTE. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Character[]{Character.valueOf('e'), + Character.valueOf('f'), + Character.valueOf('g'), + Character.valueOf('h'), + Character.valueOf('i'), + Character.valueOf('j'), + Character.valueOf('k'), + Character.valueOf('l'), + Character.valueOf('m')}), + Arrays.toString(chars.stream().map(ch -> ch.getThisCharacter()).sorted().toArray())); + + assertEquals(101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109, + chars.stream().mapToInt(AsciiCharacter::getNumericValue).sum()); + + Set sorted = new TreeSet<>(); + chars.forEach(ch -> sorted.add(ch.getHexadecimal())); + assertEquals(new TreeSet<>(Set.of("65", "66", "67", "68", "69", "6a", "6b", "6c", "6d")), + sorted); + + List empty = characters.findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual(115, 120); + assertEquals(false, empty.iterator().hasNext()); + assertEquals(0L, empty.stream().count()); + } + + @Test + public void testThirdAndFourthPagesOf10() { + Order order = Order.by(_AsciiCharacter.numericValue.asc()); + PageRequest third10 = PageRequest.ofPage(3).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, third10, order); // 'D' to 'M' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(3, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("44:D;45:E;46:F;47:G;48:H;49:I;4a:J;4b:K;4c:L;4d:M;", + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + + assertEquals(true, page.hasNext()); + PageRequest fourth10 = page.nextPageRequest(); + page = characters.findByNumericValueBetween(48, 90, fourth10, order); // 'N' to 'W' + + assertEquals(4, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("4e:N;4f:O;50:P;51:Q;52:R;53:S;54:T;55:U;56:V;57:W;", + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + } + + @Test + public void testThirdAndFourthSlicesOf5() { + PageRequest third5 = PageRequest.ofPage(3).size(5).withoutTotal(); + Sort sort = Sort.desc("id"); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, 8L, third5, sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(3, page.pageRequest().page()); + assertEquals(5, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{37L, 31L, 29L, 23L, 19L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + PageRequest fourth5 = page.nextPageRequest(); + + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType.PRIME, 8L, fourth5, sort); + + assertEquals(4, page.pageRequest().page()); + assertEquals(5, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{17L, 13L, 11L, 7L, 5L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testTrue() { + Iterable odd; + try { + odd = positives.findByIsOddTrueAndIdLessThanEqualOrderByIdDesc(10L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + + Iterator it = odd.iterator(); + + assertEquals(true, it.hasNext()); + assertEquals(9L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(7L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(5L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(3L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(1L, it.next().getId()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testUpdateQueryWithoutWhereClause() { + // Ensure there is no data left over from other tests: + + try { + shared.removeAll(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + boxes.deleteAll(boxes.findAll().toList()); + } else { + throw x; + } + } + + + + boxes.saveAll(List.of(Box.of("TestUpdateQueryWithoutWhereClause-01", 125, 117, 44), + Box.of("TestUpdateQueryWithoutWhereClause-02", 173, 165, 52), + Box.of("TestUpdateQueryWithoutWhereClause-03", 229, 221, 60))); + + + + boolean resized; + try { + // increases length by 12, decreases width by 12, and doubles the height + assertEquals(3L, shared.resizeAll(12, 2)); + resized = true; + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + resized = false; + } else { + throw x; + } + } + + + + if (resized) { + Box b1 = boxes.findById("TestUpdateQueryWithoutWhereClause-01").orElseThrow(); + assertEquals(137, b1.length); // increased by 12 + assertEquals(105, b1.width); // decreased by 12 + assertEquals(88, b1.height); // increased by factor of 2 + + Box b2 = boxes.findById("TestUpdateQueryWithoutWhereClause-02").orElseThrow(); + assertEquals(185, b2.length); // increased by 12 + assertEquals(153, b2.width); // decreased by 12 + assertEquals(104, b2.height); // increased by factor of 2 + + Box b3 = boxes.findById("TestUpdateQueryWithoutWhereClause-03").orElseThrow(); + assertEquals(241, b3.length); // increased by 12 + assertEquals(209, b3.width); // decreased by 12 + assertEquals(120, b3.height); // increased by factor of 2 + } + + try { + var removeAllResult = shared.removeAll(); + assertEquals(3, removeAllResult); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for removing entities. + boxes.deleteAll(boxes.findAll().toList()); + } else { + throw x; + } + } + + + + try { + assertEquals(0L, shared.resizeAll(2, 1)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + } else { + throw x; + } + } + } + + @Test + public void testUpdateQueryWithWhereClause() { + try { + // Ensure there is no data left over from other tests: + shared.deleteIfPositive(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of And. + } else if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + shared.deleteIfPositiveWithoutReturnRecords(); + } else { + throw x; + } + } + + UUID id1 = shared.create(Coordinate.of("first", 1.41d, 5.25f)).id; + UUID id2 = shared.create(Coordinate.of("second", 2.2d, 2.34f)).id; + + + + float c1yExpected; + double c1xExpected; + try { + assertEquals(true, shared.move(id1, 1.23d, 1.5f)); + c1yExpected = 3.5f; // 5.25 / 1.5 = 3.5 + c1xExpected = 1.23D; + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + c1yExpected = 5.25f; + c1xExpected = 1.41D;// no change + } else { + throw x; + } + } + + + + Coordinate c1 = shared.withUUID(id1).orElseThrow(); + assertEquals(c1xExpected, c1.x, 0.001d); + assertEquals(c1yExpected, c1.y, 0.001f); + + Coordinate c2 = shared.withUUID(id2).orElseThrow(); + assertEquals(2.2d, c2.x, 0.001d); + assertEquals(2.34f, c2.y, 0.001f); + + try { + assertEquals(2, shared.deleteIfPositive()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of And. + } else if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + shared.deleteIfPositiveWithoutReturnRecords(); + } else { + throw x; + } + } + + + assertEquals(false, shared.withUUID(id1).isPresent()); + assertEquals(false, shared.withUUID(id2).isPresent()); + } + + @Test + public void testVarargsSort() { + List list; + try { + list = numbers.findByIdLessThanEqual( + 12L, + Sort.asc("floorOfSquareRoot"), + Sort.desc("numBitsRequired"), + Sort.asc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{2L, 3L, // square root rounds down to 1; 2 bits + 1L, // square root rounds down to 1; 1 bit + 8L, // square root rounds down to 2; 4 bits + 4L, 5L, 6L, 7L, // square root rounds down to 2; 3 bits + 9L, 10L, 11L, 12L}), // square root rounds down to 3; 4 bits + Arrays.toString(list.stream().map(number -> number.getId()).toArray())); + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/MultipleEntityRepo.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/MultipleEntityRepo.java new file mode 100644 index 0000000000..0d569dee40 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/entity/MultipleEntityRepo.java @@ -0,0 +1,43 @@ +package io.micronaut.data.hibernate.jakarta_data.entity; + +import jakarta.data.repository.Insert; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.Optional; +import java.util.UUID; + +/** + * A repository that performs operations on different types of entities. + */ +@Repository +public interface MultipleEntityRepo { // Do not add a primary entity type. + + // Methods for Box entity: + + @Insert + Box[] addAll(Box... boxes); + + @Query("DELETE FROM Box") + long removeAll(); + + @Query("UPDATE Box SET length = length + ?1, width = width - ?1, height = height * ?2") + long resizeAll(int lengthIncrementWidthDecrement, int heightFactor); + + // Methods for Coordinate entity: + + @Insert + Coordinate create(Coordinate c); + + @Query("DELETE FROM Coordinate WHERE x > 0.0d AND y > 0.0f") + long deleteIfPositive(); + + @Query("DELETE FROM Coordinate WHERE x > 0.0d AND y > 0.0f") + void deleteIfPositiveWithoutReturnRecords(); + + @Query("UPDATE Coordinate SET x = :newX, y = y / :yDivisor WHERE id = :id") + boolean move(UUID id, double newX, float yDivisor); + + @Query("WHERE id = ?1") + Optional withUUID(UUID id); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/Catalog.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/Catalog.java new file mode 100644 index 0000000000..abf27adce3 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/Catalog.java @@ -0,0 +1,119 @@ +package io.micronaut.data.hibernate.jakarta_data.persistence; + +import jakarta.data.Order; +import jakarta.data.repository.By; +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.Insert; +import jakarta.data.repository.OrderBy; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; +import jakarta.data.repository.Update; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static jakarta.data.repository.By.ID; + +@Repository +public interface Catalog extends DataRepository { + + @Insert + CatalogProduct add(CatalogProduct product); + + @Insert + CatalogProduct[] addMultiple(CatalogProduct... products); + + @Find + Optional get(String productNum); + + @Update + CatalogProduct modify(CatalogProduct product); + + @Update + CatalogProduct[] modifyMultiple(CatalogProduct... products); + + @Delete + void remove(CatalogProduct product); + + @Delete + void removeMultiple(CatalogProduct... products); + + @Save + void customSave(CatalogProduct product); + + @Delete + void deleteById(@By(ID) String productNum); + + long deleteByProductNumLike(String pattern); + + long countByPriceGreaterThanEqual(Double price); + + @Query("WHERE LENGTH(name) = ?1 AND price < ?2 ORDER BY name") + List findByNameLengthAndPriceBelow(int nameLength, double maxPrice); + + @OrderBy("name") + @Query("WHERE LENGTH(name) = ?1 AND price < ?2") + List findByNameLengthAndPriceBelowNameAsc(int nameLength, double maxPrice); + + @OrderBy(value = "name", descending = true) + @Query("WHERE LENGTH(name) = ?1 AND price < ?2") + List findByNameLengthAndPriceBelowNameDesc(int nameLength, double maxPrice); + + @Find + @OrderBy(value = "name") + List allSortedByNameAsc(); + + @Find + @OrderBy(value = "name", descending = true) + List allSortedByNameDesc(); + + @Find + @OrderBy(value = "name", ignoreCase = true) + List allSortedByNameAscIgnoreCase(); + + @Find + @OrderBy(value = "name", descending = true, ignoreCase = true) + List allSortedByNameDescIgnoreCase(); + + @Find + @OrderBy(value = "name", descending = true, ignoreCase = true) + List findAll(); + + List findByNameLike(String name); + + @OrderBy(value = "price", descending = true) + Stream findByPriceNotNullAndPriceLessThanEqual(double maxPrice); + + List findByPriceNull(); + + List findByProductNumBetween(String first, String last, Order sorts); + + List findByProductNumLike(String productNum); + +// EntityManager getEntityManager(); +// +// default double sumPrices(Department... departments) { +// StringBuilder jpql = new StringBuilder("SELECT SUM(o.price) FROM Product o"); +// for (int d = 1; d <= departments.length; d++) { +// jpql.append(d == 1 ? " WHERE " : " OR "); +// jpql.append('?').append(d).append(" MEMBER OF o.departments"); +// } +// +// EntityManager em = getEntityManager(); +// TypedQuery query = em.createQuery(jpql.toString(), Double.class); +// for (int d = 1; d <= departments.length; d++) { +// query.setParameter(d, departments[d - 1]); +// } +// return query.getSingleResult(); +// } + + @Query("FROM CatalogProduct WHERE (:rate * price <= :max AND :rate * price >= :min) ORDER BY name") + Stream withTaxBetween(@Param("min") double mininunTaxAmount, + @Param("max") double maximumTaxAmount, + @Param("rate") double taxRate); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/CatalogProduct.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/CatalogProduct.java new file mode 100644 index 0000000000..9f166df71c --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/CatalogProduct.java @@ -0,0 +1,107 @@ +package io.micronaut.data.hibernate.jakarta_data.persistence; + +import jakarta.persistence.Basic; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; + +import java.util.Collections; +import java.util.Set; + +@Entity +public class CatalogProduct { + public enum Department { + APPLIANCES, AUTOMOTIVE, CLOTHING, CRAFTS, ELECTRONICS, FURNITURE, GARDEN, GROCERY, OFFICE, PHARMACY, SPORTING_GOODS, TOOLS + } + + @ElementCollection(fetch = FetchType.EAGER) + private Set departments; + + @Basic(optional = false) + private String name; + + private Double price; + + @Basic(optional = false) + @Id + private String productNum; + + @Transient + private Double surgePrice; + + @Version + private long versionNum; + + public static CatalogProduct of(String name, Double price, String productNum, Department... departments) { + return new CatalogProduct(name, price, price, productNum, departments); + } + + private CatalogProduct(String name, Double price, Double surgePrice, String productNum, Department... departments) { + this.productNum = productNum; + this.name = name; + this.price = price; + this.surgePrice = surgePrice; + this.departments = departments == null ? Collections.emptySet() : Set.of(departments); + } + + public CatalogProduct() { + //do nothing + } + + public Set getDepartments() { + return departments; + } + + public void setDepartments(Set departments) { + this.departments = departments; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public String getProductNum() { + return productNum; + } + + public void setProductNum(String productNum) { + this.productNum = productNum; + } + + public long getVersionNum() { + return this.versionNum; + } + + public Double getSurgePrice() { + return surgePrice; + } + + public void setSurgePrice(Double surgePrice) { + this.surgePrice = surgePrice; + } + + public void setVersionNum(long versionNum) { + this.versionNum = versionNum; + } + + @Override + public String toString() { + return "Product [departments=" + departments + ", name=" + name + ", price=" + price + ", productNum=" + + productNum + ", surgePrice=" + surgePrice + ", versionNum=" + versionNum + "]"; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/PersistenceEntityTests.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/PersistenceEntityTests.java new file mode 100644 index 0000000000..eb672118cf --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/persistence/PersistenceEntityTests.java @@ -0,0 +1,400 @@ +package io.micronaut.data.hibernate.jakarta_data.persistence; + +import io.micronaut.context.annotation.Property; +import io.micronaut.data.hibernate.H2DBProperties; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.exceptions.EntityExistsException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.micronaut.data.hibernate.jakarta_data.persistence.CatalogProduct.Department.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Execute tests with a Persistence specific entity with a repository that requires read and writes (AKA not read-only) + */ +//@Property(name = "jpa.default.properties.hibernate.show_sql", value = "true") +@Property(name = "jpa.default.properties.uniqueResultOnFindOne", value = "true") +@Property(name = "jpa.default.properties.persistOrMergeOnSave", value = "true") +@H2DBProperties +@MicronautTest(transactional = false) +public class PersistenceEntityTests { + + @Inject + Catalog catalog; + + @Test + public void testEntityManager() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("bicycle", 359.98, "TEST-PROD-81", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("shin guards", 8.99, "TEST-PROD-83", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("dishwasher", 788.10, "TEST-PROD-86", CatalogProduct.Department.APPLIANCES)); + catalog.customSave(CatalogProduct.of("socks", 5.99, "TEST-PROD-87", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("volleyball", 10.99, "TEST-PROD-89", CatalogProduct.Department.SPORTING_GOODS)); + +// assertEquals(385.95, catalog.sumPrices(Department.CLOTHING, Department.SPORTING_GOODS), 0.001); +// assertEquals(794.09, catalog.sumPrices(Department.CLOTHING, Department.APPLIANCES), 0.001); + } + + @Test + public void testIdAttributeWithDifferentName() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("apple", 1.19, "TEST-PROD-12", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("pear", 0.99, "TEST-PROD-14", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("orange", 1.09, "TEST-PROD-16", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("banana", 0.49, "TEST-PROD-17", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("plum", 0.89, "TEST-PROD-18", CatalogProduct.Department.GROCERY)); + + Iterable found = catalog.findByProductNumBetween("TEST-PROD-13", "TEST-PROD-17", Order.by(Sort.asc("name"))); + Iterator it = found.iterator(); + assertEquals(true, it.hasNext()); + assertEquals("banana", it.next().getName()); + assertEquals(true, it.hasNext()); + assertEquals("orange", it.next().getName()); + assertEquals(true, it.hasNext()); + assertEquals("pear", it.next().getName()); + assertEquals(false, it.hasNext()); + + assertEquals(5L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testInsertEntityThatAlreadyExists() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct prod1 = catalog.add(CatalogProduct.of("watermelon", 6.29, "TEST-PROD-94", CatalogProduct.Department.GROCERY)); + + try { + catalog.add(CatalogProduct.of("pineapple", 1.99, "TEST-PROD-94", GROCERY)); + fail("Should not be able to insert an entity that has same Id as another entity."); + } catch (EntityExistsException x) { + // expected + } + + Optional result; + result = catalog.get("TEST-PROD-94"); + assertEquals(true, result.isPresent()); + + catalog.remove(prod1); + + result = catalog.get("TEST-PROD-94"); + assertEquals(false, result.isPresent()); + } + + @Test + public void testLike() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("celery", 1.57, "TEST-PROD-31", GROCERY)); + catalog.customSave(CatalogProduct.of("mushrooms", 1.89, "TEST-PROD-32", GROCERY)); + catalog.customSave(CatalogProduct.of("carrots", 1.39, "TEST-PROD-33", GROCERY)); + + List found = catalog.findByNameLike("%r_o%"); + assertEquals(List.of("carrots", "mushrooms"), + found.stream().map(CatalogProduct::getName).sorted().collect(Collectors.toList())); + + assertEquals(3L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testNotRunOnNOSQL() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + List products = new ArrayList<>(); + products.add(CatalogProduct.of("pen", 2.50, "TEST-PROD-01")); + products.add(CatalogProduct.of("pencil", 1.25, "TEST-PROD-02")); + products.add(CatalogProduct.of("marker", 3.00, "TEST-PROD-03")); + products.add(CatalogProduct.of("calculator", 15.00, "TEST-PROD-04")); + products.add(CatalogProduct.of("ruler", 2.00, "TEST-PROD-05")); + + products.forEach(product -> catalog.customSave(product)); + + long countExpensive = catalog.countByPriceGreaterThanEqual(2.99); + assertEquals(2L, countExpensive, "Expected two products to be more than 3.00"); + + assertEquals(5L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Disabled // Pending feature + @Test + public void testMultipleInsertUpdateDelete() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct[] added = catalog.addMultiple(CatalogProduct.of("blueberries", 2.49, "TEST-PROD-95", CatalogProduct.Department.GROCERY), + CatalogProduct.of("strawberries", 2.29, "TEST-PROD-96", CatalogProduct.Department.GROCERY), + CatalogProduct.of("raspberries", 2.39, "TEST-PROD-97", CatalogProduct.Department.GROCERY)); + + assertEquals(3, added.length); + + // The position of resulting entities must match the parameter + assertEquals("blueberries", added[0].getName()); + assertEquals("TEST-PROD-95", added[0].getProductNum()); + assertEquals(2.49, added[0].getPrice(), 0.001); + assertEquals(Set.of(CatalogProduct.Department.GROCERY), added[0].getDepartments()); + CatalogProduct blueberries = added[0]; + + assertEquals("strawberries", added[1].getName()); + assertEquals("TEST-PROD-96", added[1].getProductNum()); + assertEquals(2.29, added[1].getPrice(), 0.001); + assertEquals(Set.of(CatalogProduct.Department.GROCERY), added[1].getDepartments()); + CatalogProduct strawberries = added[1]; + + assertEquals("raspberries", added[2].getName()); + assertEquals("TEST-PROD-97", added[2].getProductNum()); + assertEquals(2.39, added[2].getPrice(), 0.001); + assertEquals(Set.of(CatalogProduct.Department.GROCERY), added[2].getDepartments()); + CatalogProduct raspberries = added[2]; + + strawberries.setPrice(1.99); + raspberries.setPrice(2.34); + CatalogProduct[] modified = catalog.modifyMultiple(raspberries, strawberries); + assertEquals(2, modified.length); + + assertEquals("raspberries", modified[0].getName()); + assertEquals("TEST-PROD-97", modified[0].getProductNum()); + assertEquals(2.34, modified[0].getPrice(), 0.001); + assertEquals(Set.of(CatalogProduct.Department.GROCERY), modified[0].getDepartments()); + raspberries = modified[0]; + + assertEquals("strawberries", modified[1].getName()); + assertEquals("TEST-PROD-96", modified[1].getProductNum()); + assertEquals(1.99, modified[1].getPrice(), 0.001); + assertEquals(Set.of(CatalogProduct.Department.GROCERY), modified[1].getDepartments()); + strawberries = modified[1]; + + // Attempt to remove entities that do not exist in the database + try { + catalog.removeMultiple(CatalogProduct.of("blackberries", 2.59, "TEST-PROD-98", CatalogProduct.Department.GROCERY), + CatalogProduct.of("gooseberries", 2.79, "TEST-PROD-99", CatalogProduct.Department.GROCERY)); + fail("OptimisticLockingFailureException must be raised because the entities are not found for deletion."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + // Remove only the entities that actually exist in the database + catalog.removeMultiple(strawberries, blueberries, raspberries); + + Iterable remaining = catalog.findByProductNumBetween("TEST-PROD-95", "TEST-PROD-99", Order.by()); + assertEquals(false, remaining.iterator().hasNext()); + } + + @Test + public void testNull() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("spinach", 2.28, "TEST-PROD-51", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("broccoli", 2.49, "TEST-PROD-52", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("rhubarb", null, "TEST-PROD-53", CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("potato", 0.79, "TEST-PROD-54", CatalogProduct.Department.GROCERY)); + + Collection found = catalog.findByPriceNull(); + + assertEquals(1, found.size()); + assertEquals("rhubarb", found.iterator().next().getName()); + + assertEquals(List.of("spinach", "potato"), + catalog.findByPriceNotNullAndPriceLessThanEqual(2.30) + .map(CatalogProduct::getName) + .collect(Collectors.toList())); + + assertEquals(4L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testSort() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found = catalog.findByNameLengthAndPriceBelowNameAsc(10, 100.0); + + assertEquals(List.of("basketball", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findByNameLengthAndPriceBelowNameDesc(10, 100.0); + + assertEquals(List.of("toothpaste", "basketball"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameAsc(); + + assertEquals(List.of("baseball cap", "basketball", "chisel", "computer", "sunblock", "sweater", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDesc(); + + assertEquals(List.of("toothpaste", "sweater", "sunblock", "computer", "chisel", "basketball", "baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + } + + @Test + public void testSortIgnoreCase() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("Sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("Chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("Sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("Baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found; + + found = catalog.allSortedByNameAsc(); + + assertEquals(List.of("Baseball cap", "Chisel", "Sunblock", "Sweater", "basketball", "computer", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDesc(); + + assertEquals(List.of("toothpaste", "computer", "basketball", "Sweater", "Sunblock", "Chisel", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameAscIgnoreCase(); + + assertEquals(List.of("Baseball cap", "basketball", "Chisel", "computer", "Sunblock", "Sweater", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDescIgnoreCase(); + + assertEquals(List.of("toothpaste", "Sweater", "Sunblock", "computer", "Chisel", "basketball", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findAll(); + + assertEquals(List.of("toothpaste", "Sweater", "Sunblock", "computer", "Chisel", "basketball", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + } + + @Test + public void testQueryWithNamedParameters() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("tape measure", 7.29, "TEST-PROD-61", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("pry bar", 4.39, "TEST-PROD-62", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("hammer", 8.59, "TEST-PROD-63", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("adjustable wrench", 4.99, "TEST-PROD-64", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("framing square", 9.88, "TEST-PROD-65", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("rasp", 6.79, "TEST-PROD-66", CatalogProduct.Department.TOOLS)); + + Stream found = catalog.withTaxBetween(0.4, 0.6, 0.08125); + + assertEquals(List.of("adjustable wrench", "rasp", "tape measure"), + found.map(CatalogProduct::getName).collect(Collectors.toList())); + + assertEquals(6L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testQueryWithPositionalParameters() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.GROCERY)); + catalog.customSave(CatalogProduct.of("chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found = catalog.findByNameLengthAndPriceBelow(10, 100.0); + + assertEquals(List.of("basketball", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findByNameLengthAndPriceBelow(8, 1000.0); + + assertEquals(List.of("sunblock"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + assertEquals(7L, catalog.deleteByProductNumLike("TEST-PROD-%")); + + } + + @Disabled // Pending feature + @Test + public void testVersionedInsertUpdateDelete() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct prod1 = catalog.add(CatalogProduct.of("zucchini", 1.49, "TEST-PROD-91", CatalogProduct.Department.GROCERY)); + CatalogProduct prod2 = catalog.add(CatalogProduct.of("cucumber", 1.29, "TEST-PROD-92", CatalogProduct.Department.GROCERY)); + + long prod1InitialVersion = prod1.getVersionNum(); + long prod2InitialVersion = prod2.getVersionNum(); + + prod1.setPrice(1.59); + prod1 = catalog.modify(prod1); + + prod2.setPrice(1.39); + prod2 = catalog.modify(prod2); + + // Expect version number to change when modified + assertNotEquals(prod1InitialVersion, prod1.getVersionNum()); + assertNotEquals(prod2InitialVersion, prod2.getVersionNum()); + + long prod1SecondVersion = prod1.getVersionNum(); + + prod1.setPrice(1.54); + prod1 = catalog.modify(prod1); + + assertNotEquals(prod1SecondVersion, prod1.getVersionNum()); + assertNotEquals(prod1InitialVersion, prod1.getVersionNum()); + + // Update must not be made when the version does not match: + prod2.setVersionNum(prod2InitialVersion); + prod2.setPrice(1.34); + try { + catalog.modify(prod2); + fail("Must raise OptimisticLockingFailureException for entity instance with old version."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + catalog.remove(prod1); + + Optional found = catalog.get("TEST-PROD-91"); + assertEquals(false, found.isPresent()); + + try { + catalog.remove(prod1); // already removed + fail("Must raise OptimisticLockingFailureException for entity that was already removed from the database."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + prod2.setVersionNum(prod2InitialVersion); + try { + catalog.remove(prod2); // still at old version + fail("Must raise OptimisticLockingFailureException for entity with non-matching version."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + assertEquals(1L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacter.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacter.java new file mode 100644 index 0000000000..c43811fffe --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacter.java @@ -0,0 +1,63 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +public class AsciiCharacter implements Serializable { + private static final long serialVersionUID = 1L; + + @jakarta.persistence.Id + private long id; + + private int numericValue; + + private String hexadecimal; + + private char thisCharacter; + + private boolean isControl; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getNumericValue() { + return numericValue; + } + + public void setNumericValue(int numericValue) { + this.numericValue = numericValue; + } + + public String getHexadecimal() { + return hexadecimal; + } + + public void setHexadecimal(String hexadecimal) { + this.hexadecimal = hexadecimal; + } + + public char getThisCharacter() { + return thisCharacter; + } + + public void setThisCharacter(char thisCharacter) { + this.thisCharacter = thisCharacter; + } + + public boolean isControl() { + return isControl; + } + + public void setControl(boolean isControl) { + this.isControl = isControl; + } + +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacters.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacters.java new file mode 100644 index 0000000000..fb0f523d7a --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharacters.java @@ -0,0 +1,97 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.By; +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * This is a read only repository that represents the set of AsciiCharacters from 0-256. + * This repository will be pre-populated at test startup and verified prior to running tests. + * This interface is required to inherit only from DataRepository in order to satisfy a TCK scenario. + */ +@Repository +public interface AsciiCharacters extends DataRepository, IdOperations { + + @Query(" ") // it is valid to have a query with no clauses + Stream all(Limit limit, Sort... sort); + + @Query("ORDER BY id ASC") + Stream alphabetic(Limit limit); + + long countByHexadecimalNotNull(); + + boolean existsByThisCharacter(char ch); + + @Find + AsciiCharacter find(char thisCharacter); + + @Find + List findAll(); + + @Find + Optional find(@By("thisCharacter") char ch, + @By("hexadecimal") String hex); + + List findByHexadecimalContainsAndIsControlNot(String substring, boolean isPrintable); + + Stream findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn(String minHex, + String maxHex, + Set excludeHex, + Order sorts); + + AsciiCharacter findByHexadecimalIgnoreCase(String hex); + + Stream findByIdBetween(long minimum, long maximum, Sort sort); + + AsciiCharacter findByIsControlTrueAndNumericValueBetween(int min, int max); + + Optional findByNumericValue(int id); + + Page findByNumericValueBetween(int min, int max, PageRequest pagination, Order order); + + List findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual(int max, int min); + + AsciiCharacter[] findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith(int minValue, String lastHexDigit, Sort sort); + + Optional findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc(String firstHexDigit, boolean isControlChar); + + @Query("select thisCharacter where hexadecimal like '4_'" + + " and hexadecimal not like '%0'" + + " and thisCharacter not in ('E', 'G')" + + " and id not between 72 and 78" + + " order by id asc") + Character[] getABCDFO(); + + @Query("SELECT hexadecimal WHERE hexadecimal IS NOT NULL AND thisCharacter = ?1") + Optional hex(char ch); + + @Query("WHERE hexadecimal <> ' ORDER BY isn''t a keyword when inside a literal' AND hexadecimal IN ('4a', '4b', '4c', ?1)") + Stream jklOr(String hex); + + default Stream retrieveAlphaNumericIn(long minId, long maxId) { + return findByIdBetween(minId, maxId, Sort.asc("id")) + .filter(c -> Character.isLetterOrDigit(c.getThisCharacter())); + } + + @Query("SELECT thisCharacter ORDER BY id DESC") + Character[] reverseAlphabetic(Limit limit); + + @Save + List saveAll(List characters); + + @Query("SELECT COUNT(THIS) WHERE numericValue <= 97 AND numericValue >= 74") + long twentyFour(); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharactersPopulator.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharactersPopulator.java new file mode 100644 index 0000000000..f6a0c2eb6e --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/AsciiCharactersPopulator.java @@ -0,0 +1,37 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public class AsciiCharactersPopulator implements Populator { + + public static AsciiCharactersPopulator get() { + return new AsciiCharactersPopulator(); + } + + @Override + public void populationLogic(AsciiCharacters repo) { + List dictonary = new ArrayList<>(); + + IntStream.range(1, 128) // Some databases don't support ASCII NULL character (0) + .forEach(value -> { + AsciiCharacter inst = new AsciiCharacter(); + + inst.setId(value); + inst.setNumericValue(value); + inst.setHexadecimal(Integer.toHexString(value)); + inst.setThisCharacter((char) value); + inst.setControl(Character.isISOControl((char) value)); + + dictonary.add(inst); + }); + + repo.saveAll(dictonary); + } + + @Override + public boolean isPopulated(AsciiCharacters repo) { + return repo.countByHexadecimalNotNull() == 127L; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/CustomRepository.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/CustomRepository.java new file mode 100644 index 0000000000..c73487f1cd --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/CustomRepository.java @@ -0,0 +1,28 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.data.repository.Delete; +import jakarta.data.repository.Insert; +import jakarta.data.repository.Repository; + +import java.util.List; +import java.util.Set; + +/** + * Do not add methods or inheritance to this interface. + * Its purpose is to test that without inheriting from a built-in repository, + * the lifecycle methods with the same entity class are what identifies the + * primary entity class to use for the count and exist methods. + */ +@Repository +public interface CustomRepository { + + @Insert + void add(List list); + + long countByIdIn(Set ids); + + boolean existsByIdIn(Set ids); + + @Delete + void remove(List list); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/IdOperations.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/IdOperations.java new file mode 100644 index 0000000000..9f0a53185f --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/IdOperations.java @@ -0,0 +1,16 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.data.Limit; +import jakarta.data.repository.Query; + +import java.util.List; +/** + * This interface contains common operations for the NaturalNumbers and AsciiCharacters repositories. + */ +public interface IdOperations { + long countByIdBetween(long minimum, long maximum); + + boolean existsById(long id); + @Query("SELECT id WHERE id >= :inclusiveMin ORDER BY id ASC") + List withIdEqualOrAbove(long inclusiveMin, Limit limit); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumber.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumber.java new file mode 100644 index 0000000000..b33a31b1fa --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumber.java @@ -0,0 +1,79 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +public class NaturalNumber implements Serializable { + private static final long serialVersionUID = 1L; + + public enum NumberType { + ONE, PRIME, COMPOSITE + } + + @jakarta.persistence.Id + private long id; //AKA the value + + private boolean isOdd; + + private Short numBitsRequired; + + // Sorting on enum types is vendor-specific in Jakarta Data. + // Use numTypeOrdinal for sorting instead. + @jakarta.persistence.Enumerated(jakarta.persistence.EnumType.STRING) + private NumberType numType; // enum of ONE | PRIME | COMPOSITE + + private int numTypeOrdinal; // ordinal value of numType + + private long floorOfSquareRoot; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public boolean isOdd() { + return isOdd; + } + + public void setOdd(boolean isOdd) { + this.isOdd = isOdd; + } + + public Short getNumBitsRequired() { + return numBitsRequired; + } + + public void setNumBitsRequired(Short numBitsRequired) { + this.numBitsRequired = numBitsRequired; + } + + public NumberType getNumType() { + return numType; + } + + public void setNumType(NumberType numType) { + this.numType = numType; + } + + public int getNumTypeOrdinal() { + return numTypeOrdinal; + } + + public void setNumTypeOrdinal(int value) { + numTypeOrdinal = value; + } + + public long getFloorOfSquareRoot() { + return floorOfSquareRoot; + } + + public void setFloorOfSquareRoot(long floorOfSquareRoot) { + this.floorOfSquareRoot = floorOfSquareRoot; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbers.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbers.java new file mode 100644 index 0000000000..4660b3b1bb --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbers.java @@ -0,0 +1,65 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber.NumberType; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * This is a read only repository that represents the set of Natural Numbers from 1-100. + * This repository will be pre-populated at test startup and verified prior to running tests. + * + * TODO figure out a way to make this a ReadOnlyRepository instead. + */ +@Repository +public interface NaturalNumbers extends BasicRepository, IdOperations { + + long countAll(); + + CursoredPage findByFloorOfSquareRootOrderByIdAsc(long sqrtFloor, + PageRequest pagination); + + Stream findByIdBetweenOrderByNumTypeOrdinalAsc(long minimum, + long maximum, + Order sorts); + + List findByIdGreaterThanEqual(long minimum, + Limit limit, + Order sorts); + + NaturalNumber[] findByIdLessThan(long exclusiveMax, Sort primarySort, Sort secondarySort); + + List findByIdLessThanEqual(long maximum, Sort... sorts); + + Page findByIdLessThanOrderByFloorOfSquareRootDesc(long exclusiveMax, + PageRequest pagination, + Order order); + + CursoredPage findByNumTypeAndNumBitsRequiredLessThan(NumberType type, + short bitsUnder, + Order order, + PageRequest pagination); + + NaturalNumber[] findByNumTypeNot(NumberType notThisType, Limit limit, Order sorts); + + Page findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType type, + long maxSqrtFloor, + PageRequest pagination, + Sort sort); + + @Query("SELECT id WHERE isOdd = true AND id BETWEEN 21 AND ?1 ORDER BY id ASC") + Page oddsFrom21To(long max, PageRequest pageRequest); + + @Query("WHERE isOdd = false AND numType = io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber.NumberType.PRIME") + Optional two(); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbersPopulator.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbersPopulator.java new file mode 100644 index 0000000000..398b5e244b --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/NaturalNumbersPopulator.java @@ -0,0 +1,66 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + + +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber.NumberType; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public class NaturalNumbersPopulator implements Populator { + + public static NaturalNumbersPopulator get() { + return new NaturalNumbersPopulator(); + } + + @Override + public boolean isPopulated(NaturalNumbers repo) { + return repo.countAll() == 100L; + } + + @Override + public void populationLogic(NaturalNumbers repo) { + List dictonary = new ArrayList<>(); + + IntStream.range(1, 101) + .forEach(id -> { + NaturalNumber inst = new NaturalNumber(); + + boolean isOne = id == 1; + boolean isOdd = id % 2 == 1; + long sqrRoot = squareRoot(id); + boolean isPrime = isOdd ? isPrime(id, sqrRoot) : (id == 2); + NumberType numType = isOne ? NumberType.ONE : isPrime ? NumberType.PRIME : NumberType.COMPOSITE; + + inst.setId(id); + inst.setOdd(isOdd); + inst.setNumBitsRequired(bitsRequired(id)); + inst.setNumType(numType); + inst.setNumTypeOrdinal(numType.ordinal()); + inst.setFloorOfSquareRoot(sqrRoot); + + dictonary.add(inst); + }); + + repo.saveAll(dictonary); + } + + private static Short bitsRequired(int value) { + return (short) (Math.floor(Math.log(value) / Math.log(2)) + 1); + } + + private static long squareRoot(int value) { + return (long) Math.floor(Math.sqrt(value)); + } + + private static boolean isPrime(int value, long largestPossibleFactor) { + if(value == 1) + return false; + + for(int i = 2; i <= largestPossibleFactor; i++) { + if( value % i == 0 ) + return false; + } + return true; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/Populator.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/Populator.java new file mode 100644 index 0000000000..ba04703465 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/Populator.java @@ -0,0 +1,60 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import java.util.logging.Logger; + +/** + * Aids in the population of repositories with entities for read-only testing. + * + * @param A repository + */ +public interface Populator { + // INTERFACE METHODS + + /** + * The logic that adds one or more entities to this repository. + * + * @param repo - this repository + */ + void populationLogic(T repo); + + /** + * A logical test that can verify if a repository is already populated or not. + * Typically, this is by verifying the count of entities saved in the repository. + * + * @param repo - this repository + * + * @return true if the repository is populated, false otherwise. + */ + boolean isPopulated(T repo); + + //DEFAULT METHODS + + public static final Logger log = Logger.getLogger(Populator.class.getCanonicalName()); + + /** + * Short circuiting method to to populate a repository that is not already populated. + * Uses the isPopulated() method to determine if a repository is populated or not. + * + * @param repo - this repository + */ + public default void populate(T repo) { + if(isPopulated(repo)) { + return; + } + + final String repoName = repo.getClass().getSimpleName(); + + log.info(repoName + " populating"); + populationLogic(repo); + + log.info(repoName + " waiting for eventual consistency"); + + log.info(repoName + " verifying"); + if(! isPopulated(repo)) { + throw new RuntimeException("Repository " + repoName + " was not populated"); + } + + log.info(repoName + " populated"); + } + +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/PositiveIntegers.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/PositiveIntegers.java new file mode 100644 index 0000000000..27868186de --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/PositiveIntegers.java @@ -0,0 +1,66 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import io.micronaut.data.hibernate.jakarta_data.read.only.NaturalNumber.NumberType; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * This is a read only repository that shares the same data (and entity type) + * as the NaturalNumbers repository: the positive integers 1-100. + * This repository is pre-populated at test startup and verified prior to running tests. + */ +@Repository +public interface PositiveIntegers extends BasicRepository { + long countByIdLessThan(long number); + + boolean existsByIdGreaterThan(Long number); + + CursoredPage findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(long excludeSqrt, + long eclusiveMax, + PageRequest pagination, + Order order); + + List findByIsOddTrueAndIdLessThanEqualOrderByIdDesc(long max); + + List findByIsOddFalseAndIdBetween(long min, long max); + + Stream findByNumTypeInOrderByIdAsc(Set types, Limit limit); + + Stream findByNumTypeOrFloorOfSquareRoot(NumberType type, long floor); + + @Find + Page findMatching(long floorOfSquareRoot, Short numBitsRequired, NumberType numType, + PageRequest pagination, Sort... sorts); + + @Find + Optional findNumber(long id); + + @Find + List findOdd(boolean isOdd, NumberType numType, Limit limit, Order sorts); + + @Query("Select id Where isOdd = true and (id = :id or id < :exclusiveMax) Order by id Desc") + List oddAndEqualToOrBelow(long id, long exclusiveMax); + + // Per the spec: The 'and' operator has higher precedence than 'or'. + @Query("WHERE numBitsRequired = :bits OR numType = :type AND id < :xmax") + CursoredPage withBitCountOrOfTypeAndBelow(@Param("bits") short bitsRequired, + @Param("type") NumberType numberType, + @Param("xmax") long exclusiveMax, + Sort sort1, + Sort sort2, + PageRequest pageRequest); +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/ReadOnlyRepository.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/ReadOnlyRepository.java new file mode 100644 index 0000000000..f6cf7d8f8d --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/ReadOnlyRepository.java @@ -0,0 +1,33 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Save; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +//FIXME - Are user defined repository interfaces like this allowed via the Specification? +// Currently failing in test environment +// java.lang.IllegalArgumentException: @Repository ee.jakarta.tck.data.framework.readonly.NaturalNumbers does not specify an entity class. +// To correct this, have the repository interface extend DataRepository or another built-in repository interface and supply the entity class as the first parameter. +@Deprecated //Not currently in use +public interface ReadOnlyRepository extends DataRepository{ + + // WRITE - default method + // Necessary for pre-population + @Save + List saveAll(List entities); + + // READ - default methods + Optional findById(K id); + + boolean existsById(K id); + + Stream findAll(); + + Stream findByIdIn(List ids); + + long count(); + +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiChar.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiChar.java new file mode 100644 index 0000000000..1ec49b4941 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiChar.java @@ -0,0 +1,31 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.data.metamodel.Attribute; +import jakarta.data.metamodel.SortableAttribute; +import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; +import jakarta.data.metamodel.impl.AttributeRecord; +import jakarta.data.metamodel.impl.SortableAttributeRecord; +import jakarta.data.metamodel.impl.TextAttributeRecord; + +/** + * This static metamodel class tests what a user might explicitly provide, + * in which case the Jakarta Data provider will need to initialize the attributes. + */ +@StaticMetamodel(AsciiCharacter.class) +public class _AsciiChar { + public static final String ID = "id"; + public static final String HEXADECIMAL = "hexadecimal"; + public static final String NUMERICVALUE = "numericValue"; + + public static final SortableAttribute id = new SortableAttributeRecord<>("id"); + public static final TextAttribute hexadecimal = new TextAttributeRecord<>("hexadecimal"); + public static final Attribute isControl = new AttributeRecord<>("isControl"); // user decided it didn't care about sorting for this one + public static final SortableAttribute numericValue = new SortableAttributeRecord<>("numericValue"); + public static final TextAttribute thisCharacter = new TextAttributeRecord<>("thisCharacter"); + + // Avoids the checkstyle error, + // HideUtilityClassConstructor: Utility classes should not have a public or default constructor + private _AsciiChar() { + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiCharacter.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiCharacter.java new file mode 100644 index 0000000000..65bccba897 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/read/only/_AsciiCharacter.java @@ -0,0 +1,51 @@ +package io.micronaut.data.hibernate.jakarta_data.read.only; + +import jakarta.annotation.Generated; +import jakarta.data.Sort; +import jakarta.data.metamodel.SortableAttribute; +import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; + +/** + * This static metamodel class represents what an annotation processor-based approach + * might generate. + */ +@Generated("ee.jakarta.tck.data.mock.generator") +@StaticMetamodel(AsciiCharacter.class) +public class _AsciiCharacter { + public static final String ID = "id"; + public static final String HEXADECIMAL = "hexadecimal"; + public static final String NUMERICVALUE = "numericValue"; + + public static final SortableAttribute id = new NumericAttr("id"); + public static final TextAttribute hexadecimal = new TextAttr("hexadecimal"); + public static final SortableAttribute isControl = new BooleanAttr("isControl"); + public static final SortableAttribute numericValue = new NumericAttr("numericValue"); + public static final TextAttribute thisCharacter = new TextAttr("thisCharacter"); + + private static record BooleanAttr(String name, Sort asc, Sort desc) + implements SortableAttribute { + private BooleanAttr(String name) { + this(name, Sort.asc(name), Sort.desc(name)); + } + }; + + private static record NumericAttr(String name, Sort asc, Sort desc) + implements SortableAttribute { + private NumericAttr(String name) { + this(name, Sort.asc(name), Sort.desc(name)); + } + }; + + private static record TextAttr(String name, Sort asc, Sort ascIgnoreCase, + Sort desc, Sort descIgnoreCase) implements TextAttribute { + private TextAttr(String name) { + this(name, Sort.asc(name), Sort.ascIgnoreCase(name), Sort.desc(name), Sort.descIgnoreCase(name)); + } + }; + + // Avoids the checkstyle error, + // HideUtilityClassConstructor: Utility classes should not have a public or default constructor + private _AsciiCharacter() { + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/DatabaseType.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/DatabaseType.java new file mode 100644 index 0000000000..604b684af4 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/DatabaseType.java @@ -0,0 +1,30 @@ +package io.micronaut.data.hibernate.jakarta_data.utilities; + +import java.util.Arrays; + +/** + * This enum represents the configured DatabaseType based on the {@link TestProperty} databaseType + */ +public enum DatabaseType { + OTHER(Integer.MAX_VALUE), //No database type was configured + RELATIONAL(100), + GRAPH(50), + DOCUMENT(40), + TIME_SERIES(30), + COLUMN(20), + KEY_VALUE(10); + + private int flexibility; + + private DatabaseType(int flexibility) { + this.flexibility = flexibility; + } + + public static DatabaseType valueOfIgnoreCase(String value) { + return Arrays.stream(DatabaseType.values()).filter(type -> type.name().equalsIgnoreCase(value)).findAny().orElse(DatabaseType.OTHER); + } + + public boolean isKeywordSupportAtOrBelow(DatabaseType benchmark) { + return this.flexibility <= benchmark.flexibility; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestProperty.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestProperty.java new file mode 100644 index 0000000000..a037f90484 --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestProperty.java @@ -0,0 +1,157 @@ +package io.micronaut.data.hibernate.jakarta_data.utilities; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + *

This enum represents the different test properties used within this TCK. + * Each one is given a description and documentation will automatically be created in the TCK distribution.

+ * + *

When a test property is requested from the client, we expect these properties to be available from the system.

+ */ +public enum TestProperty { + //Java properties that should always be set by the JVM + javaHome (true, "java.home", + "Path to the java executable used to create the current JVM"), + javaSpecVer (true, "java.specification.version", + "Specification version of the java executable"), + javaTempDir (true, "java.io.tmpdir", + "The path to a temporary directory where a copy of the signature file will be created"), + javaVer (true, "java.version", + "Full version of the java executable"), + + //TCK specific properties + skipDeployment (false, "jakarta.tck.skip.deployment", + "If true, run in SE mode and do not use Arquillian deployment, if false run in EE mode and use Arquillian deployments. " + + "Default: false", "false"), + pollFrequency (false, "jakarta.tck.poll.frequency", + "Time in seconds between polls of the repository to verify read-only data was successfully written. " + + "Default: 1 second", "1"), + pollTimeout (false, "jakarta.tck.poll.timeout", + "Time in seconds when we will stop polling to verify read-only data was successfully written. " + + "Default: 60 seconds", "60"), + delay (false, "jakarta.tck.consistency.delay", + "Time in seconds after verifying read-only data was successfully written to respository " + + "for repository to have consistency. " + + "Default: none"), + databaseType (false, "jakarta.tck.database.type", + "The type of database being used. Valid values are " + Arrays.asList(DatabaseType.values()).toString() + " (case insensitive). " + + "The database type is used to make assertions based on the keywords supported by the underlying database. " + + "Default: OTHER", "OTHER"), + databaseName (false, "jakarta.tck.database.name", + "The name of database being used. The database name is used to make assertions based on the underlying database. " + + "Default: none"), + + //Signature testing properties + signatureClasspath (false, "signature.sigTestClasspath", "The path to the Jakarta Data API JAR used by your implementation. " + + "Required for standalone testing, but optional when testing on a Jakarta EE profile. " + + "Default: none"), + signatureImageDir (true, "jimage.dir", "The path to a directory that is readable and writable that " + + "the signature test will cache Java SE modules as classes. " + + "Default: none"); + + private boolean required; + private String key; + private String value; + private String description; + + // CONSTRUCTORS + private TestProperty(boolean required, String key, String description) { + this(required, key, description, null); + } + + private TestProperty(boolean required, String key, String description, String defaultValue) { + this.required = required; + this.key = key; + this.description = description; + this.value = getValue(defaultValue); + } + + // GETTERS + public boolean isRequired() { + return required; + } + + public String getKey() { + return key; + } + + public String getDescription() { + return description; + } + + // COMPARISONS + public boolean equals(String expectedValue) { + return getValue().equalsIgnoreCase(expectedValue); + } + + public boolean isSet() { + if(value == null) + return false; + if(value.isBlank() || value.isEmpty()) { + return false; + } + return true; + } + + // CONVERTERS + public long getLong() throws IllegalStateException, NumberFormatException { + return Long.parseLong(value); + } + + public int getInt() throws IllegalStateException, NumberFormatException { + return Integer.parseInt(value); + } + + public boolean getBoolean() { + return Boolean.parseBoolean(value); + } + + public DatabaseType getDatabaseType() { + return DatabaseType.valueOfIgnoreCase(value); + } + + /** + * Get the test property value. + * + * @return the property value + * @throws IllegalStateException if required and no property was found + */ + public String getValue() { + if(required && value == null) + throw new IllegalStateException("Could not obtain a value for system property: " + key); + + return value; + } + + private String getValue(String defaultVal) throws IllegalStateException { + final Logger log = Logger.getLogger(TestProperty.class.getCanonicalName()); + + String valueLocal = null; + log.fine("Searching for property: " + key); + + // Client: get property from system + if(valueLocal == null) { + valueLocal = System.getProperty(key); + log.fine("Value from system: " + valueLocal); + } + + //Container: get property from properties file + if(valueLocal == null) { + valueLocal = TestPropertyHandler.loadProperties().getProperty(key); + log.fine("Value from resource file: " + valueLocal); + } + + //Default: get default property + if(valueLocal == null) { + valueLocal = defaultVal; + log.fine("Value set to default: " + valueLocal); + } + + if (valueLocal == null) { + log.fine("Property was not set, value: " + null); + } + + return valueLocal; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestPropertyHandler.java b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestPropertyHandler.java new file mode 100644 index 0000000000..e671e4674a --- /dev/null +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/jakarta_data/utilities/TestPropertyHandler.java @@ -0,0 +1,47 @@ +package io.micronaut.data.hibernate.jakarta_data.utilities; + +import java.io.InputStream; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * This uitlity class handles the caching and loading of test properties between the + * client and container when tests are run inside an Arquillian container. + */ +public class TestPropertyHandler { + + private static final Logger log = Logger.getLogger(TestPropertyHandler.class.getCanonicalName()); + + private static final String PROP_FILE = "tck.properties"; + private static Properties foundProperties; + + private TestPropertyHandler() { + //UTILITY CLASS + } + + /** + * Container: Load properties from the TestProperty cache file, and return a properties object. + * If any error occurs in finding the cache file, or loading the properties, + * then an empty properties object is returned. + * + * @return - the cached properties, or an empty properties object. + */ + static Properties loadProperties() { + if (foundProperties != null) { + return foundProperties; + } + + //Try to load property file + foundProperties = new Properties(); + InputStream propsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROP_FILE); + if (propsStream != null) { + try { + foundProperties.load(propsStream); + } catch (Exception e) { + log.info("Attempted to load properties from resource " + PROP_FILE + " but failed. Because: " + e.getLocalizedMessage()); + } + } + + return foundProperties; + } +} diff --git a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/reactive/ReactorSpec.groovy b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/reactive/ReactorSpec.groovy index 434c9db9c9..a0d923bbff 100644 --- a/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/reactive/ReactorSpec.groovy +++ b/data-hibernate-jpa/src/test/groovy/io/micronaut/data/hibernate/reactive/ReactorSpec.groovy @@ -29,7 +29,7 @@ import spock.lang.Specification @MicronautTest(rollback = false, packages = "io.micronaut.data.tck.entities") @H2DBProperties -@Property(name = 'jpa.default.properties.hibernate.show_sql', value = 'true') +//@Property(name = 'jpa.default.properties.hibernate.show_sql', value = 'true') class ReactorSpec extends Specification{ @Inject diff --git a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java index 39b4d4491a..5d70f60a0f 100644 --- a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java +++ b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/operations/DefaultHibernateReactiveRepositoryOperations.java @@ -20,15 +20,14 @@ import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.data.annotation.QueryHint; import io.micronaut.data.connection.reactive.ReactorConnectionOperations; import io.micronaut.data.hibernate.conf.RequiresReactiveHibernate; import io.micronaut.data.hibernate.operations.AbstractHibernateOperations; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Page; -import io.micronaut.data.model.Pageable; import io.micronaut.data.model.runtime.DeleteBatchOperation; import io.micronaut.data.model.runtime.DeleteOperation; import io.micronaut.data.model.runtime.InsertBatchOperation; @@ -240,8 +239,18 @@ public Flux findAll(PagedQuery pagedQuery) { @Override public Mono> findPage(PagedQuery pagedQuery) { + if (pagedQuery instanceof PreparedQuery pg) { + PreparedQuery preparedQuery = (PreparedQuery) pg; + return findAll(preparedQuery) + .collectList() + .map(content -> Page.of( + content, + pagedQuery.getPageable(), + -1L + )); + } return operation(session -> findPaged(session, pagedQuery).collectList() - .flatMap(resultList -> countOf(session, pagedQuery.getRootEntity(), pagedQuery.getPageable()) + .flatMap(resultList -> countOf(session, pagedQuery.getRootEntity(), pagedQuery.getQueryLimit()) .map(total -> Page.of(resultList, pagedQuery.getPageable(), total)))); } @@ -256,9 +265,9 @@ private Flux findPaged(Stage.Session session, PagedQuery pagedQuery) { return collector.result; } - private Mono countOf(Stage.Session session, Class entity, @Nullable Pageable pageable) { + private Mono countOf(Stage.Session session, Class entity, Limit limit) { SingleResultCollector collector = new SingleResultCollector<>(); - collectCountOf(sessionFactory.getCriteriaBuilder(), session, entity, pageable, collector); + collectCountOf(sessionFactory.getCriteriaBuilder(), session, entity, limit, collector); return collector.result; } diff --git a/data-jdbc/build.gradle b/data-jdbc/build.gradle index ad51719732..f9272cb8f9 100644 --- a/data-jdbc/build.gradle +++ b/data-jdbc/build.gradle @@ -66,6 +66,9 @@ dependencies { testImplementation mnSerde.micronaut.serde.support testImplementation mnTestResources.micronaut.test.resources.extensions.junit.platform + testRuntimeOnly(libs.jupiter.engine) + testImplementation(mnTest.micronaut.test.junit5) + testImplementation(libs.managed.jakarta.data.api) testRuntimeOnly mnSerde.micronaut.serde.oracle.jdbc.json testRuntimeOnly mnFlyway.micronaut.flyway diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/config/DataJdbcConfiguration.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/config/DataJdbcConfiguration.java index 819e9c7a39..a208b88925 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/config/DataJdbcConfiguration.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/config/DataJdbcConfiguration.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.EachProperty; import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.Named; @@ -56,6 +57,12 @@ public class DataJdbcConfiguration implements Named, Toggleable { private boolean allowConnectionPerOperation = true; private boolean enabled = true; + /** + * Fail on multiple results for findOne. + */ + @NextMajorVersion("Make the default") + private boolean uniqueResultOnFindOne; + /** * The configuration. * @param name The configuration name @@ -190,4 +197,18 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + /** + * @return Is unique result required on find one + */ + public boolean isUniqueResultOnFindOne() { + return uniqueResultOnFindOne; + } + + /** + * @param uniqueResultOnFindOne Is unique result required on find one + */ + public void setUniqueResultOnFindOne(boolean uniqueResultOnFindOne) { + this.uniqueResultOnFindOne = uniqueResultOnFindOne; + } } diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index 906b014fd5..0a01fec4fb 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -31,8 +31,10 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.data.connection.ConnectionDefinition; import io.micronaut.data.connection.ConnectionOperations; +import io.micronaut.data.connection.ConnectionStatus; import io.micronaut.data.connection.annotation.Connectable; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.exceptions.NonUniqueResultException; import io.micronaut.data.jdbc.config.DataJdbcConfiguration; import io.micronaut.data.jdbc.convert.JdbcConversionContext; import io.micronaut.data.jdbc.mapper.ColumnIndexCallableResultReader; @@ -44,9 +46,11 @@ import io.micronaut.data.jdbc.mapper.SqlResultConsumer; import io.micronaut.data.jdbc.runtime.ConnectionCallback; import io.micronaut.data.jdbc.runtime.PreparedStatementCallback; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.model.runtime.AttributeConverterRegistry; @@ -90,6 +94,7 @@ import io.micronaut.data.runtime.operations.internal.SyncCascadeOperations; import io.micronaut.data.runtime.operations.internal.query.BindableParametersStoredQuery; import io.micronaut.data.runtime.operations.internal.sql.AbstractSqlRepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import io.micronaut.data.runtime.operations.internal.sql.SqlJsonColumnMapperProvider; import io.micronaut.data.runtime.operations.internal.sql.SqlPreparedQuery; import io.micronaut.data.runtime.operations.internal.sql.SqlStoredQuery; @@ -161,6 +166,7 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository private final ColumnIndexCallableResultReader columnIndexCallableResultReader; private final Map> sqlExceptionMappers = new EnumMap<>(Dialect.class); + /** * Default constructor. * @@ -349,7 +355,8 @@ public R findOne(@NonNull PreparedQuery pq) { } private R findOne(Connection connection, SqlPreparedQuery preparedQuery) { - try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, true)) { + boolean limitToSingleResult = !jdbcConfiguration.isUniqueResultOnFindOne(); + try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, limitToSingleResult)) { preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery)); try (ResultSet rs = ps.executeQuery()) { SqlTypeMapper mapper = createMapper(preparedQuery, ResultSet.class); @@ -361,12 +368,19 @@ private R findOne(Connection connection, SqlPreparedQuery preparedQ if (rs.next()) { oneMapper.processRow(rs); } - while (hasJoins && rs.next()) { - oneMapper.processRow(rs); + if (hasJoins) { + while (rs.next()) { + oneMapper.processRow(rs); + } + } else if (jdbcConfiguration.isUniqueResultOnFindOne() && rs.next()) { + throw new NonUniqueResultException("Multiple results found for query: " + preparedQuery.getQuery()); } result = oneMapper.getResult(); } else if (rs.next()) { result = mapper.map(rs, preparedQuery.getResultType()); + if (jdbcConfiguration.isUniqueResultOnFindOne() && rs.next()) { + throw new NonUniqueResultException("Multiple results found for query: " + preparedQuery.getQuery()); + } } else { result = null; } @@ -445,8 +459,11 @@ public boolean exists(@NonNull PreparedQuery pq) { @NonNull @Override public Stream findStream(@NonNull PreparedQuery preparedQuery) { - ConnectionContext connectionContext = getConnectionCtx(); - return findStream(preparedQuery, connectionContext.connection, connectionContext.needsToBeClosed); + Optional> connectionStatus = connectionOperations.findConnectionStatus(); + if (connectionStatus.isPresent()) { + return findStream(preparedQuery, connectionStatus.get().getConnection(), false); + } + return findAll(preparedQuery).stream(); } private Stream findStream(@NonNull PreparedQuery pq, Connection connection, boolean closeConnection) { @@ -535,7 +552,7 @@ private void closeResultSet(Connection connection, PreparedStatement ps, ResultS @NonNull @Override - public Iterable findAll(@NonNull PreparedQuery preparedQuery) { + public List findAll(@NonNull PreparedQuery preparedQuery) { SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(preparedQuery); return executeRead(connection -> findAll(connection, sqlPreparedQuery, true), sqlPreparedQuery.getInvocationContext()); } @@ -727,7 +744,7 @@ public T findOne(@NonNull Class type, @NonNull Object id) { @NonNull @Override public Iterable findAll(@NonNull PagedQuery query) { - throw new UnsupportedOperationException("The findAll method without an explicit query is not supported. Use findAll(PreparedQuery) instead"); + return findPage(query).getContent(); } @Override @@ -743,6 +760,28 @@ public Stream findStream(@NonNull PagedQuery query) { @Override public Page findPage(@NonNull PagedQuery query) { + if (query instanceof PreparedQuery pq) { + PreparedQuery preparedQuery = (PreparedQuery) pq; + Pageable pageable = preparedQuery.getPageable(); + List results = findAll(preparedQuery); + if (pageable.getMode() == Pageable.Mode.OFFSET) { + return Page.of(results, pageable, -1L); + } + if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors; + List resultList = (List) results; + if (preparedQuery.getResultDataType() == DataType.ENTITY) { + cursors = sqlPreparedQuery.createCursors(resultList, pageable); + } else if (sqlPreparedQuery.isDtoProjection()) { + RuntimePersistentEntity runtimePersistentEntity = (RuntimePersistentEntity) getEntity(sqlPreparedQuery.getResultType()); + cursors = sqlPreparedQuery.createCursors(resultList, pageable, runtimePersistentEntity); + } else { + throw new IllegalStateException("CursoredPage cannot produce projection result"); + } + return CursoredPage.of(results, pageable, cursors, -1L); + } + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + } throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findAll(PreparedQuery) instead"); } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy index 038d465520..e15ba3ef0a 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy @@ -15,6 +15,10 @@ */ package io.micronaut.data.jdbc.h2 +import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort +import io.micronaut.data.tck.entities.Person import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec @@ -43,4 +47,51 @@ class H2CursoredPaginationSpec extends AbstractCursoredPageSpec { return br } + void "test pageable list with row removal XX"() { + when: "10 people are paged" + def pageable = Pageable.from(0, 10, sorting) // The first pageable can be non-cursored + Page page = personRepository.retrieve(pageable) // The retrieve method explicitly returns CursoredPage + + then: "The data is correct" + page.content.size() == 10 + page.content[0].name == elem1 + page.content[1].name == elem2 + page.hasNext() + + when: "The next page is selected after deletion" + personRepository.delete(page.content[1]) + personRepository.delete(page.content[9]) + page = personRepository.retrieve(page.nextPageable()) + + then: "it is correct" + page.offset == 10 + page.pageNumber == 1 + page.content[0].name == elem10 + page.content[9].name == elem19 + page.content.size() == 10 + page.hasNext() + page.hasPrevious() + + when: "The previous page is selected" + pageable = page.previousPageable() + page = personRepository.retrieve(pageable) + + then: "it is correct" + page.offset == 0 + page.pageNumber == 0 + page.content[0].name == elem1 + page.content.size() == 8 + page.getCursor(7).isPresent() + page.getCursor(8).isEmpty() + !page.hasPrevious() + page.hasNext() + + where: + sorting | elem1 | elem2 | elem10 | elem19 + null | "AAAAA00" | "AAAAA01" | "BBBBB00" | "BBBBB09" + Sort.of(Sort.Order.desc("id")) | "ZZZZZ09" | "ZZZZZ08" | "YYYYY09" | "YYYYY00" + Sort.of(Sort.Order.asc("name")) | "AAAAA00" | "AAAAA00" | "AAAAA03" | "AAAAA06" + Sort.of(Sort.Order.desc("name")) | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ06" | "ZZZZZ03" + } + } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy index ffd9136525..5285828796 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy @@ -16,6 +16,10 @@ package io.micronaut.data.jdbc.h2 import groovy.transform.Memoized +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort +import io.micronaut.data.tck.entities.Book +import io.micronaut.data.tck.entities.Student import io.micronaut.data.tck.entities.embedded.BookEntity import io.micronaut.data.tck.entities.embedded.BookState import io.micronaut.data.tck.entities.embedded.ResourceEntity @@ -330,4 +334,69 @@ class H2RepositorySpec extends AbstractRepositorySpec implements H2TestPropertyP cleanup: bookEntityRepository.deleteAll() } + + void "test JOIN pagination xxx"() { + if (skipJoinPagination()) { + return + } + given: + Student denis = new Student("Denis") + Student josh = new Student("Josh") + Student kevin = new Student("Kevin") + def book1 = new Book(title: "The Stand", students: [denis, josh]) + def book2 = new Book(title: "Pet Cemetery", students: [kevin]) + def book3 = new Book(title: "Along Came a Spider", students: [kevin, josh]) + bookRepository.save(book1) + bookRepository.save(book2) + bookRepository.save(book3 + ) + List names = [denis.name, josh.name] + when: + io.micronaut.data.model.Page page = bookRepository.findAllByStudentsNameIn(names, Pageable.from(0, 10, Sort.of(Sort.Order.asc("title")))) + + then: + page.totalSize == page.content.size() + page.totalSize == 2 + page.content.collect { it.title }.sort() == ["Along Came a Spider", "The Stand"] + page.content[0].students.collect { it.name }.sort() == ["Josh", "Kevin"] + page.content[1].students.collect { it.name }.sort() == ["Denis", "Josh"] + + when: + def pageable = Pageable.from(0, 1, Sort.of(Sort.Order.asc("title"))) + page = bookRepository.findAllByStudentsNameIn(names, pageable) + + then: + page.totalSize == 2 + page.content.size() == 1 + page.content[0].title == "Along Came a Spider" + page.content[0].students.collect { it.name }.sort() == ["Josh", "Kevin"] + + when: + pageable = pageable.next() + page = bookRepository.findAllByStudentsNameIn(names, pageable) + + then: + page.totalSize == 2 + page.content.size() == 1 + page.content[0].title == "The Stand" + page.content[0].students.collect { it.name }.sort() == ["Denis", "Josh"] + + when: + pageable = pageable.next() + page = bookRepository.findAllByStudentsNameIn(names, pageable) + + then: + page.totalSize == 2 + page.content.size() == 0 + + when: + pageable = pageable.previous() + page = bookRepository.findAllByStudentsNameIn(names, pageable) + + then: + page.totalSize == 2 + page.content.size() == 1 + page.content[0].title == "The Stand" + page.content[0].students.collect { it.name }.sort() == ["Denis", "Josh"] + } } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Box.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Box.java new file mode 100644 index 0000000000..2a19611571 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Box.java @@ -0,0 +1,30 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.entity; + +import io.micronaut.core.annotation.Introspected; + +@jakarta.persistence.Entity +@Introspected(accessKind = Introspected.AccessKind.FIELD) +public class Box { + @jakarta.persistence.Id + public String boxIdentifier; + + public int length; + + public int width; + + public int height; + + public static Box of(String id, int length, int width, int height) { + Box box = new Box(); + box.boxIdentifier = id; + box.length = length; + box.width = width; + box.height = height; + return box; + } + + @Override + public String toString() { + return "Box@" + Integer.toHexString(hashCode()) + ":" + length + "x" + width + "x" + height + ":" + boxIdentifier; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Boxes.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Boxes.java new file mode 100644 index 0000000000..2f7978264c --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Boxes.java @@ -0,0 +1,13 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.entity; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Repository; + +/** + * A repository that inherits from the built-in BasicRepository and adds no methods. + */ +@JdbcRepository(dialect = Dialect.H2) +public interface Boxes extends BasicRepository { +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Coordinate.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Coordinate.java new file mode 100644 index 0000000000..1feb2fb998 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/Coordinate.java @@ -0,0 +1,32 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.entity; + +import io.micronaut.core.annotation.Introspected; + +import java.util.UUID; + +/** + * This entity includes some field types that aren't covered elsewhere in the TCK. + */ +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +@jakarta.persistence.Entity +public class Coordinate { + @jakarta.persistence.Id + public UUID id; + + public double x; + + public float y; + + public static Coordinate of(String id, double x, float y) { + Coordinate c = new Coordinate(); + c.id = UUID.nameUUIDFromBytes(id.getBytes()); + c.x = x; + c.y = y; + return c; + } + + @Override + public String toString() { + return "Coordinate@" + Integer.toHexString(hashCode()) + "(" + x + "," + y + ")" + ":" + id; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/EntityTests.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/EntityTests.java new file mode 100644 index 0000000000..504996dc3e --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/EntityTests.java @@ -0,0 +1,2456 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.entity; + +import io.micronaut.context.annotation.Property; +import io.micronaut.data.jdbc.h2.H2DBProperties; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.AsciiCharacter; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.AsciiCharacters; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.AsciiCharactersPopulator; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.CustomRepository; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumbers; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumbersPopulator; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.PositiveIntegers; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only._AsciiChar; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only._AsciiCharacter; +import io.micronaut.data.jdbc.h2.jakarta_data.utilities.DatabaseType; +import io.micronaut.data.jdbc.h2.jakarta_data.utilities.TestProperty; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.NonUniqueResultException; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.page.PageRequest.Cursor; +import jakarta.inject.Inject; +import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Execute a test with an entity that is dual annotated which means this test + * can run against a provider that supports any Entity type. + */ +@Property(name = "datasources.default.allowConnectionPerOperation", value = "true") +@H2DBProperties +@MicronautTest(transactional = false) +public class EntityTests { + + public static final Logger log = Logger.getLogger(EntityTests.class.getCanonicalName()); + + @Inject + Boxes boxes; + + @Inject + NaturalNumbers numbers; + + @Inject + PositiveIntegers positives; // shares same read-only data with NaturalNumbers + + @Inject + CustomRepository customRepo; // shares same read-only data with NaturalNumbers + + @Inject + AsciiCharacters characters; + + @Inject + MultipleEntityRepo shared; + + @BeforeEach + //Inject doesn't happen until after BeforeClass so this is necessary before each test + public void setup() { + assertNotNull(numbers); + NaturalNumbersPopulator.get().populate(numbers); + + assertNotNull(characters); + AsciiCharactersPopulator.get().populate(characters); + } + + private DatabaseType type = TestProperty.databaseType.getDatabaseType(); + + @Test + public void ensureNaturalNumberPrepopulation() { + assertEquals(100L, numbers.countAll()); + assertTrue(numbers.findById(0L).isEmpty(), "Zero should not have been in the set of natural numbers."); + assertFalse(numbers.findById(10L).get().isOdd()); + } + + @Test + public void ensureCharacterPrepopulation() { + try { + assertEquals(127L, characters.countByHexadecimalNotNull()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of the Null comparison + } else { + throw x; + } + } + + assertEquals('0', characters.findByNumericValue(48).get().getThisCharacter()); + assertTrue(characters.findByNumericValue(1).get().isControl()); + } + + @Test + public void testBasicRepository() { + + // custom method from NaturalNumbers: + try { + Stream found = numbers.findByIdBetweenOrderByNumTypeOrdinalAsc( + 50L, 59L, + Order.by(Sort.asc("id"))); + List list = found + .map(NaturalNumber::getId) + .collect(Collectors.toList()); + assertEquals(List.of(53L, 59L, // first 2 must be primes + 50L, 51L, 52L, 54L, 55L, 56L, 57L, 58L), // the remaining 8 are composite numbers + list); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + } else { + throw x; + } + } + + // built-in method from BasicRepository: + assertEquals(60L, numbers.findById(60L).orElseThrow().getId()); + } + + @Disabled // Pending feature + @Test + public void testBasicRepositoryBuiltInMethods() { + + // BasicRepository.saveAll + Iterable saved = boxes.saveAll(List.of(Box.of("TestBasicRepositoryMethods-01", 119, 120, 169), + Box.of("TestBasicRepositoryMethods-02", 20, 21, 29), + Box.of("TestBasicRepositoryMethods-03", 33, 56, 65), + Box.of("TestBasicRepositoryMethods-04", 45, 28, 53))); + Iterator savedIt = saved.iterator(); + assertEquals(true, savedIt.hasNext()); + Box box1 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-01", box1.boxIdentifier); + assertEquals(119, box1.length); + assertEquals(120, box1.width); + assertEquals(169, box1.height); + assertEquals(true, savedIt.hasNext()); + Box box2 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(20, box2.length); + assertEquals(21, box2.width); + assertEquals(29, box2.height); + assertEquals(true, savedIt.hasNext()); + Box box3 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + assertEquals(true, savedIt.hasNext()); + Box box4 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-04", box4.boxIdentifier); + assertEquals(45, box4.length); + assertEquals(28, box4.width); + assertEquals(53, box4.height); + assertEquals(false, savedIt.hasNext()); + + + // BasicRepository.save + box2.length = 21; + box2.width = 20; + box2 = boxes.save(box2); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(21, box2.length); + assertEquals(20, box2.width); + assertEquals(29, box2.height); + + Box box5 = boxes.save(Box.of("TestBasicRepositoryMethods-05", 153, 104, 185)); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + + + // BasicRepository.deleteAll(Iterable) + boxes.deleteAll(List.of(box1, box2)); + + + + assertEquals(3, boxes.findAll().count()); + + + // BasicRepository.delete + boxes.delete(box4); + + + + // BasicRepository.findAll + Stream stream = boxes.findAll(); + List list = stream.sorted(Comparator.comparing(b -> b.boxIdentifier)).collect(Collectors.toList()); + assertEquals(2, list.size()); + box4 = list.get(0); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + box5 = list.get(1); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-03"); + + + + // BasicRepository.findById + assertEquals(false, boxes.findById("TestBasicRepositoryMethods-03").isPresent()); + box5 = boxes.findById("TestBasicRepositoryMethods-05").orElseThrow(); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-05"); + + + assertEquals(0, boxes.findAll().count()); + } + + @Disabled // Pending feature + @Test + public void testBasicRepositoryMethods() { + + // BasicRepository.saveAll + Iterable saved = boxes.saveAll(List.of(Box.of("TestBasicRepositoryMethods-01", 119, 120, 169), + Box.of("TestBasicRepositoryMethods-02", 20, 21, 29), + Box.of("TestBasicRepositoryMethods-03", 33, 56, 65), + Box.of("TestBasicRepositoryMethods-04", 45, 28, 53))); + Iterator savedIt = saved.iterator(); + assertEquals(true, savedIt.hasNext()); + Box box1 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-01", box1.boxIdentifier); + assertEquals(119, box1.length); + assertEquals(120, box1.width); + assertEquals(169, box1.height); + assertEquals(true, savedIt.hasNext()); + Box box2 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(20, box2.length); + assertEquals(21, box2.width); + assertEquals(29, box2.height); + assertEquals(true, savedIt.hasNext()); + Box box3 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + assertEquals(true, savedIt.hasNext()); + Box box4 = savedIt.next(); + assertEquals("TestBasicRepositoryMethods-04", box4.boxIdentifier); + assertEquals(45, box4.length); + assertEquals(28, box4.width); + assertEquals(53, box4.height); + assertEquals(false, savedIt.hasNext()); + + + + + // BasicRepository.save + box2.length = 21; + box2.width = 20; + box2 = boxes.save(box2); + assertEquals("TestBasicRepositoryMethods-02", box2.boxIdentifier); + assertEquals(21, box2.length); + assertEquals(20, box2.width); + assertEquals(29, box2.height); + + Box box5 = boxes.save(Box.of("TestBasicRepositoryMethods-05", 153, 104, 185)); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + + + // BasicRepository.deleteAll(Iterable) + boxes.deleteAll(List.of(box1, box2)); + + + + assertEquals(3, boxes.findAll().count()); + + + // BasicRepository.delete + boxes.delete(box4); + + + + // BasicRepository.findAll + Stream stream = boxes.findAll(); + List list = stream.sorted(Comparator.comparing(b -> b.boxIdentifier)).collect(Collectors.toList()); + assertEquals(2, list.size()); + box4 = list.get(0); + assertEquals("TestBasicRepositoryMethods-03", box3.boxIdentifier); + assertEquals(33, box3.length); + assertEquals(56, box3.width); + assertEquals(65, box3.height); + box5 = list.get(1); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-03"); + + + + // BasicRepository.findById + assertEquals(false, boxes.findById("TestBasicRepositoryMethods-03").isPresent()); + box5 = boxes.findById("TestBasicRepositoryMethods-05").orElseThrow(); + assertEquals("TestBasicRepositoryMethods-05", box5.boxIdentifier); + assertEquals(153, box5.length); + assertEquals(104, box5.width); + assertEquals(185, box5.height); + + // BasicRepository.deleteById + boxes.deleteById("TestBasicRepositoryMethods-05"); + + + + assertEquals(0, boxes.findAll().count()); + } + + @Test + public void testBeyondFinalPage() { + PageRequest sixth = PageRequest.ofPage(6).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, sixth, Order.by(_AsciiCharacter.numericValue.asc())); + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testBeyondFinalSlice() { + PageRequest sixth = PageRequest.ofPage(6).size(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, + 8L, + sixth, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + } + + @Test + public void testBy() { + AsciiCharacter ch = characters.find('L', "4c").orElseThrow(); + assertEquals('L', ch.getThisCharacter()); + assertEquals("4c", ch.getHexadecimal()); + assertEquals(76L, ch.getId()); + assertEquals(false, ch.isControl()); + + assertEquals(true, characters.find('M', "4b").isEmpty()); + } + + @Test + public void testCommonInterfaceQueries() { + + try { + assertEquals(4L, numbers.countByIdBetween(87L, 90L)); + + assertEquals(5L, characters.countByIdBetween(86L, 90L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of Between + } else { + throw x; + } + } + + assertEquals(true, numbers.existsById(73L)); + + assertEquals(true, characters.existsById(74L)); + + assertEquals(false, numbers.existsById(-1L)); + + assertEquals(false, characters.existsById(-2L)); + + try { + assertEquals( + List.of(68L, 69L, 70L, 71L, 72L), + characters.withIdEqualOrAbove(68L, Limit.of(5))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of >= in JDQL + } else { + throw x; + } + } + + assertEquals(List.of(71L, 72L, 73L, 74L, 75L), + numbers.withIdEqualOrAbove(71L, Limit.of(5))); + } + + @Test + public void testContainsInString() { + Collection found; + try { + found = characters.findByHexadecimalContainsAndIsControlNot("4", true); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Contains. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + + assertEquals(List.of("24", "34", + "40", "41", "42", "43", + "44", "45", "46", "47", + "48", "49", "4a", "4b", + "4c", "4d", "4e", "4f", + "54", "64", "74"), + found.stream().map(AsciiCharacter::getHexadecimal).sorted().toList()); + } + + @Test + public void testDataRepository() { + try { + AsciiCharacter del = characters.findByIsControlTrueAndNumericValueBetween(33, 127); + assertEquals(127, del.getNumericValue()); + assertEquals("7f", del.getHexadecimal()); + assertEquals(true, del.isControl()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + } else { + throw x; + } + } + + try { + AsciiCharacter j = characters.findByHexadecimalIgnoreCase("6A"); + assertEquals("6a", j.getHexadecimal()); + assertEquals('j', j.getThisCharacter()); + assertEquals(106, j.getNumericValue()); + assertEquals(false, j.isControl()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + + AsciiCharacter d = characters.findByNumericValue(100).orElseThrow(); + assertEquals(100, d.getNumericValue()); + assertEquals('d', d.getThisCharacter()); + assertEquals("64", d.getHexadecimal()); + assertEquals(false, d.isControl()); + + assertEquals(true, characters.existsByThisCharacter('D')); + } + + @Test + public void testDefaultMethod() { + try { + assertEquals(List.of('W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd'), + characters.retrieveAlphaNumericIn(87L, 100L) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of Between + } else { + throw x; + } + } + } + + @Test + public void testDescendingSort() { + Stream stream; + try { + stream = characters.findByIdBetween( + 52L, 57L, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Character[]{'9', '8', '7', '6', '5', '4'}), + Arrays.toString(stream.map(AsciiCharacter::getThisCharacter).toArray())); + } + + @Test + public void testEmptyQuery() { + + try { + assertEquals(List.of('a', 'b', 'c', 'd', 'e', 'f'), + characters.all(Limit.range(97, 102), Sort.asc("id")) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + } + + @Test + public void testEmptyResultException() { + try { + AsciiCharacter ch = characters.findByHexadecimalIgnoreCase("2g"); + fail("Unexpected result of findByHexadecimalIgnoreCase(2g): " + ch.getHexadecimal()); + } catch (EmptyResultException x) { + log.info("testEmptyResultException expected to catch exception " + x + ". Printing its stack trace:"); + x.printStackTrace(System.out); + // test passes + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + return; // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + } + + @Test + public void testFalse() { + List even; + try { + even = positives.findByIsOddFalseAndIdBetween(50L, 60L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + + assertEquals(6L, even.stream().count()); + + assertEquals(List.of(50L, 52L, 54L, 56L, 58L, 60L), + even.stream().map(NaturalNumber::getId).sorted().collect(Collectors.toList())); + } + + @Test + public void testFinalPageOfUpTo10() { + PageRequest fifthPageRequest = PageRequest.ofPage(5).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, fifthPageRequest, + Order.by(_AsciiCharacter.numericValue.asc())); // 'X' to 'Z' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + AsciiCharacter ch = it.next(); + assertEquals('X', ch.getThisCharacter()); + assertEquals("58", ch.getHexadecimal()); + assertEquals(88L, ch.getId()); + assertEquals(88, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + // second result + ch = it.next(); + assertEquals('Y', ch.getThisCharacter()); + assertEquals("59", ch.getHexadecimal()); + assertEquals(89L, ch.getId()); + assertEquals(89, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + // third result + ch = it.next(); + assertEquals('Z', ch.getThisCharacter()); + assertEquals("5a", ch.getHexadecimal()); + assertEquals(90L, ch.getId()); + assertEquals(90, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + + assertEquals(false, it.hasNext()); + + assertEquals(5, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(3, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testFinalSliceOfUpTo5() { + PageRequest fifth = PageRequest.ofPage(5).size(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + PRIME, + 8L, + fifth, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(true, page.hasContent()); + assertEquals(5, page.pageRequest().page()); + assertEquals(2, page.numberOfElements()); + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + NaturalNumber number = it.next(); + assertEquals(3L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(1L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 2), number.getNumBitsRequired()); + + // second result + assertEquals(true, it.hasNext()); + number = it.next(); + assertEquals(2L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(1L, number.getFloorOfSquareRoot()); + assertEquals(false, number.isOdd()); + assertEquals(Short.valueOf((short) 2), number.getNumBitsRequired()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testFindAllWithPagination() { + PageRequest page2request = PageRequest.ofPage(2).size(12); + Page page2; + try { + page2 = positives.findAll(page2request, + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + + assertEquals(12, page2.numberOfElements()); + assertEquals(2, page2.pageRequest().page()); + + assertEquals(List.of(11L, 10L, 9L, // square root rounds down to 3 + 24L, 23L, 22L, 21L, 20L, 19L, 18L, 17L, 16L), // square root rounds down to 4 + page2.stream().map(n -> n.getId()).collect(Collectors.toList())); + } + + @Test + public void testFindFirst() { + Optional none; + try { + none = characters.findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc( + "h", false); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of StartsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + assertEquals(true, none.isEmpty()); + + AsciiCharacter ch = characters.findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc("4", false) + .orElseThrow(); + assertEquals('@', ch.getThisCharacter()); + assertEquals("40", ch.getHexadecimal()); + assertEquals(64, ch.getNumericValue()); + } + + @Test + public void testFindFirst3() { + AsciiCharacter[] found; + + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 40, "4", Sort.asc("numericValue")); + } catch (UnsupportedOperationException e) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } else { + throw e; + } + } + + assertEquals(3, found.length); + assertEquals('4', found[0].getThisCharacter()); + assertEquals('D', found[1].getThisCharacter()); + assertEquals('T', found[2].getThisCharacter()); + } + + @Test + public void testFindList() { + List oddCompositeNumbers; + try { + oddCompositeNumbers = positives.findOdd( + true, + NumberType.COMPOSITE, + Limit.of(10), + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("numBitsRequired"), + Sort.asc("id"))); + + + assertEquals(List.of(9L, 15L, // 3 <= sqrt < 4, 4 bits + 21L, // 4 <= sqrt < 5, 5 bits + 33L, 35L, // 5 <= sqrt < 6, 6 bits + 25L, 27L, // 5 <= sqrt < 6, 5 bits + 39L, 45L, // 6 <= sqrt < 7, 6 bits + 49L), // 7 <= sqrt < 8, 6 bits + oddCompositeNumbers + .stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + } else { + throw x; + } + } + + List evenPrimeNumbers = positives.findOdd(false, NumberType.PRIME, Limit.of(9), Order.by()); + + assertEquals(1, evenPrimeNumbers.size()); + NaturalNumber num = evenPrimeNumbers.get(0); + assertEquals(2L, num.getId()); + assertEquals(1L, num.getFloorOfSquareRoot()); + assertEquals(Short.valueOf((short) 2), num.getNumBitsRequired()); + assertEquals(NumberType.PRIME, num.getNumType()); + assertEquals(false, num.isOdd()); + } + + @Test + public void testFindOne() { + AsciiCharacter j = characters.find('j'); + + assertEquals("6a", j.getHexadecimal()); + assertEquals(106L, j.getId()); + assertEquals(106, j.getNumericValue()); + assertEquals('j', j.getThisCharacter()); + } + + @Test + public void testFindOptional() { + NaturalNumber num = positives.findNumber(67L).orElseThrow(); + + assertEquals(67L, num.getId()); + assertEquals(8L, num.getFloorOfSquareRoot()); + assertEquals(Short.valueOf((short) 7), num.getNumBitsRequired()); + assertEquals(NumberType.PRIME, num.getNumType()); + assertEquals(true, num.isOdd()); + + Optional opt = positives.findNumber(-40L); + + assertEquals(false, opt.isPresent()); + } + + @Test + public void testFindPage() { + PageRequest page1Request = PageRequest.ofSize(7); + + Page page1; + try { + page1 = positives.findMatching( + 9L, + Short.valueOf((short) 7), + NumberType.COMPOSITE, + page1Request, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + + assertEquals(List.of(99L, 98L, 96L, 95L, 94L, 93L, 92L), + page1.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(true, page1.hasNext()); + + Page page2 = positives.findMatching(9L, Short.valueOf((short) 7), NumberType.COMPOSITE, + page1.nextPageRequest(), Sort.desc("id")); + + assertEquals(List.of(91L, 90L, 88L, 87L, 86L, 85L, 84L), + page2.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(true, page2.hasNext()); + + Page page3 = positives.findMatching(9L, Short.valueOf((short) 7), NumberType.COMPOSITE, + page2.nextPageRequest(), Sort.desc("id")); + + assertEquals(List.of(82L, 81L), + page3.stream().map(NaturalNumber::getId).collect(Collectors.toList())); + + assertEquals(false, page3.hasNext()); + } + + @Test + public void testFirstCursoredPageOf8AndNextPages() { + // The query for this test returns 1-15,25-32 in the following order: + + // 32 requires 6 bits + // 25, 26, 27, 28, 29, 30, 31 requires 5 bits + // 8, 9, 10, 11, 12, 13, 14, 15 requires 4 bits + // 4, 5, 6, 7, 8 requires 3 bits + // 2, 3 requires 2 bits + // 1 requires 1 bit + + Order order = Order.by(Sort.asc("id")); + PageRequest first8 = PageRequest.ofSize(8); + CursoredPage page; + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, first8, order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(8, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{32L, 25L, 26L, 27L, 28L, 29L, 30L, 31L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, page.nextPageRequest(), order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(8, page.numberOfElements()); + + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(4L, 33L, page.nextPageRequest(), order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(7, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{4L, 5L, 6L, 7L, 2L, 3L, 1L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testFirstCursoredPageWithoutTotalOf6AndNextPages() { + PageRequest first6 = PageRequest.ofSize(6).withoutTotal(); + CursoredPage slice; + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, first6); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(Arrays.toString(new Long[]{49L, 50L, 51L, 52L, 53L, 54L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(6, slice.numberOfElements()); + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(6, slice.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{55L, 56L, 57L, 58L, 59L, 60L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(7L, slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{61L, 62L, 63L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(3, slice.numberOfElements()); + } + + @Test + public void testFirstPageOf10() { + PageRequest first10 = PageRequest.ofSize(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, first10, + Order.by(_AsciiCharacter.numericValue.asc())); // '0' to 'Z' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(1, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("30:0;31:1;32:2;33:3;34:4;35:5;36:6;37:7;38:8;39:9;", // '0' to '9' + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + } + + @Test + public void testFirstSliceOf5() { + PageRequest first5 = PageRequest.ofSize(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, + 8L, + first5, + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + assertEquals(5, page.numberOfElements()); + + Iterator it = page.iterator(); + + // first result + assertEquals(true, it.hasNext()); + NaturalNumber number = it.next(); + assertEquals(79L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(8L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 7), number.getNumBitsRequired()); + + // second result + assertEquals(true, it.hasNext()); + assertEquals(73L, it.next().getId()); + + // third result + assertEquals(true, it.hasNext()); + assertEquals(71L, it.next().getId()); + + // fourth result + assertEquals(true, it.hasNext()); + assertEquals(67L, it.next().getId()); + + // fifth result + assertEquals(true, it.hasNext()); + number = it.next(); + assertEquals(61L, number.getId()); + assertEquals(NumberType.PRIME, number.getNumType()); + assertEquals(7L, number.getFloorOfSquareRoot()); + assertEquals(true, number.isOdd()); + assertEquals(Short.valueOf((short) 6), number.getNumBitsRequired()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testGreaterThanEqualExists() { + try { + assertEquals(true, positives.existsByIdGreaterThan(0L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of GreaterThan + } else { + throw x; + } + } + assertEquals(true, positives.existsByIdGreaterThan(99L)); + assertEquals(false, positives.existsByIdGreaterThan(100L)); // doesn't exist because the table only has 1 to 100 + } + + @Test + public void testIn() { + Stream nonPrimes; + try { + nonPrimes = positives.findByNumTypeInOrderByIdAsc( + Set.of(NumberType.COMPOSITE, NumberType.ONE), + Limit.of(9)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of In + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + + assertEquals(List.of(1L, 4L, 6L, 8L, 9L, 10L, 12L, 14L, 15L), + nonPrimes.map(NaturalNumber::getId).collect(Collectors.toList())); + + Stream primes = positives.findByNumTypeInOrderByIdAsc(Collections.singleton(NumberType.PRIME), + Limit.of(6)); + assertEquals(List.of(2L, 3L, 5L, 7L, 11L, 13L), + primes.map(NaturalNumber::getId).collect(Collectors.toList())); + } + + @Test + public void testIgnoreCase() { + Stream found; + try { + found = characters.findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn( + "4c", "5A", Set.of("5"), + Order.by(Sort.asc("hexadecimal"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of IgnoreCase + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Column and Key-Value databases might not be capable of In + // Column and Key-Value databases might not be capable of sorting. + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + + assertEquals(List.of(Character.valueOf('L'), // 4c + Character.valueOf('M'), // 4d + Character.valueOf('N'), // 4e + Character.valueOf('O'), // 4f + Character.valueOf('P'), // 50 + Character.valueOf('Q'), // 51 + Character.valueOf('R'), // 52 + Character.valueOf('S'), // 53 + Character.valueOf('T'), // 54 + Character.valueOf('U'), // 55 + Character.valueOf('V'), // 56 + Character.valueOf('W'), // 57 + Character.valueOf('X'), // 58 + Character.valueOf('Y'), // 59 + Character.valueOf('Z')), // 5a + found.map(AsciiCharacter::getThisCharacter).collect(Collectors.toList())); + } + + @Test + public void testCursoredPageOf7FromCursor() { + // The query for this test returns 1-35 and 49 in the following order: + // + // 35 34 33 32 49 24 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 08 15 14 13 12 11 10 09 07 06 05 04 03 02 01 + // ^^^^^^ page 1 ^^^^^^ + // ^^^ previous page ^^ + // ^^^^^ next page ^^^^ + + Order order = Order.by(Sort.asc("floorOfSquareRoot"), Sort.desc("id")); + PageRequest middle7 = PageRequest.afterCursor( + Cursor.forKey((short) 5, 5L, 26L), // 20th result is 26; it requires 5 bits and its square root rounds down to 5.), + 4L, 7, true); + + CursoredPage page; + try { + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, middle7, order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(Arrays.toString(new Long[]{25L, // 5 bits required, square root rounds down to 5 + 8L, // 4 bits required, square root rounds down to 2 + 15L, 14L, 13L, 12L, 11L // 4 bits required, square root rounds down to 3 + }), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, page.numberOfElements()); + + assertEquals(true, page.hasPrevious()); + + CursoredPage previousPage; + try { + previousPage = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, + page.previousPageRequest(), + order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{16L, // 4 bits required, square root rounds down to 4 + 31L, 30L, 29L, 28L, 27L, 26L // 5 bits required, square root rounds down to 5 + }), + Arrays.toString(previousPage.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, previousPage.numberOfElements()); + + CursoredPage nextPage; + try { + nextPage = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(6L, 50L, + page.nextPageRequest(), + order); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{10L, 9L, // 4 bits required, square root rounds down to 3 + 7L, 6L, 5L, 4L, // 3 bits required, square root rounds down to 2 + 3L // 2 bits required, square root rounds down to 1 + }), + Arrays.toString(nextPage.stream().map(number -> number.getId()).toArray())); + + assertEquals(7, nextPage.numberOfElements()); + } + + @Test + public void testCursoredPageOfNothing() { + + CursoredPage page; + try { + // There are no positive integers less than 4 which have a square root that rounds down to something other than 1. + page = positives.findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(1L, 4L, PageRequest.ofPage(1L), Order.by()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(false, page.hasContent()); + assertEquals(false, page.hasNext()); + assertEquals(false, page.hasPrevious()); + assertEquals(0, page.content().size()); + assertEquals(0, page.numberOfElements()); + + try { + page.nextPageRequest(); + fail("nextPageRequest must raise NoSuchElementException when current page is empty."); + } catch (NoSuchElementException x) { + // expected + } + + try { + page.previousPageRequest(); + fail("previousPageRequest must raise NoSuchElementException when current page is empty."); + } catch (NoSuchElementException x) { + // expected + } + } + + @Test + public void testCursoredPageWithoutTotalOf9FromCursor() { + // The query for this test returns composite natural numbers under 64 in the following order: + // + // 49 50 51 52 54 55 56 57 58 60 62 63 36 38 39 40 42 44 45 46 48 25 26 27 28 30 32 33 34 35 16 18 20 21 22 24 09 10 12 14 15 04 06 08 + // ^^^^^^^^ slice 1 ^^^^^^^^^ + // ^^^^^^^^ slice 2 ^^^^^^^^^ + // ^^^^^^^^ slice 3 ^^^^^^^^^ + + PageRequest middle9 = PageRequest.afterCursor( + Cursor.forKey(6L, 46L), // 20th result is 46; its square root rounds down to 6. + 4L, 9, false); + Order order = Order.by(Sort.desc("floorOfSquareRoot"), Sort.asc("id")); + + CursoredPage slice; + try { + slice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, (short) 7, order, middle9); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + return; + } + + assertEquals(Arrays.toString(new Long[]{48L, 25L, 26L, 27L, 28L, 30L, 32L, 33L, 34L}), + Arrays.toString(slice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, slice.numberOfElements()); + + assertEquals(true, slice.hasPrevious()); + CursoredPage previousSlice; + try { + previousSlice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, + (short) 7, + order, + slice.previousPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{63L, 36L, 38L, 39L, 40L, 42L, 44L, 45L, 46L}), + Arrays.toString(previousSlice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, previousSlice.numberOfElements()); + + CursoredPage nextSlice; + try { + nextSlice = numbers.findByNumTypeAndNumBitsRequiredLessThan(NumberType.COMPOSITE, + (short) 7, + order, + slice.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(Arrays.toString(new Long[]{35L, 16L, 18L, 20L, 21L, 22L, 24L, 9L, 10L}), + Arrays.toString(nextSlice.stream().map(number -> number.getId()).toArray())); + + assertEquals(9, nextSlice.numberOfElements()); + } + + @Test + public void testCursoredPageWithoutTotalOfNothing() { + // There are no numbers larger than 30 which have a square root that rounds down to 3. + PageRequest pagination = PageRequest.ofSize(33).afterCursor(Cursor.forKey(30L)).withoutTotal(); + + CursoredPage slice; + try { + slice = numbers.findByFloorOfSquareRootOrderByIdAsc(3L, pagination); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(false, slice.hasContent()); + assertEquals(0, slice.content().size()); + assertEquals(0, slice.numberOfElements()); + } + + @Test + public void testLessThanWithCount() { + try { + assertEquals(91L, positives.countByIdLessThan(92L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of LessThan + } else { + throw x; + } + } + + assertEquals(0L, positives.countByIdLessThan(1L)); + } + + @Test + public void testLimit() { + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual( + 60L, + Limit.of(10), + Order.by( + Sort.asc("floorOfSquareRoot"), + Sort.desc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases are not capable of GreaterThanEqual + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{63L, 62L, 61L, 60L, // square root rounds down to 7 + 80L, 79L, 78L, 77L, 76L, 75L}), // square root rounds down to 8 + Arrays.toString(nums.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testLimitedRange() { + // Primes above 40 are: + // 41, 43, 47, 53, 59, + // 61, 67, 71, 73, 79, + // 83, 89, ... + + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual( + 40L, + Limit.range(6, 10), + Order.by( + Sort.asc("numTypeOrdinal"), // primes first + Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases are not capable of GreaterThanEqual + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{61L, 67L, 71L, 73L, 79L}), + Arrays.toString(nums.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testLimitToOneResult() { + Collection nums; + try { + nums = numbers.findByIdGreaterThanEqual(80L, Limit.of(1), Order.by()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of GreaterThanEqual + } else { + throw x; + } + } + + Iterator it = nums.iterator(); + assertEquals(true, it.hasNext()); + + NaturalNumber num = it.next(); + assertEquals(true, num.getId() >= 80L); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testLiteralEnumAndLiteralFalse() { + + NaturalNumber two; + try { + two = numbers.two().orElseThrow(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases are not capable of JDQL TRUE/FALSE + } else { + throw x; + } + } + + assertEquals(2L, two.getId()); + assertEquals(NumberType.PRIME, two.getNumType()); + assertEquals(Short.valueOf((short) 2), two.getNumBitsRequired()); + } + + @Test + public void testLiteralInteger() { + + try { + assertEquals(24, characters.twentyFour()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of <= in JDQL. + // Key-Value databases might not be capable of JDQL AND. + return; + } else { + throw x; + } + } + } + + @Test + public void testLiteralString() { + + try { + assertEquals(List.of('J', 'K', 'L', 'M'), + characters.jklOr("4d") + .map(AsciiCharacter::getThisCharacter) + .sorted() + .collect(Collectors.toList())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Key-Value databases might not be capable of JDQL AND. + // Column and Key-Value databases might not be capable of JDQL IN + // when used with entity attributes other than the Id. + return; + } else { + throw x; + } + } + } + + @Test + public void testLiteralTrue() { + Page page1; + try { + page1 = numbers.oddsFrom21To(40L, PageRequest.ofSize(5)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases are not capable of JDQL BETWEEN + // Key-Value databases are not capable of JDQL TRUE/FALSE + return; + } else { + throw x; + } + } + + try { + assertEquals(10L, page1.totalElements()); + assertEquals(2L, page1.totalPages()); + + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + assertEquals(List.of(21L, 23L, 25L, 27L, 29L), page1.content()); + + assertEquals(true, page1.hasNext()); + + Page page2 = numbers.oddsFrom21To(40L, page1.nextPageRequest()); + + assertEquals(List.of(31L, 33L, 35L, 37L, 39L), page2.content()); + + if (page2.hasNext()) { + Page page3 = numbers.oddsFrom21To(40L, page2.nextPageRequest()); + assertEquals(false, page3.hasContent()); + assertEquals(false, page3.hasNext()); + } + } + + @Test + public void testMixedSort() { + NaturalNumber[] nums; + try { + nums = numbers.findByIdLessThan( + 15L, + Sort.asc("numBitsRequired"), + Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThan. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{1L, // 1 bit + 3L, 2L, // 2 bits + 7L, 6L, 5L, 4L, // 3 bits + 14L, 13L, 12L, 11L, 10L, 9L, 8L}), // 4 bits + Arrays.toString(Stream.of(nums).map(number -> number.getId()).toArray())); + } + + @Disabled // Pending feature + @Test + public void testNonUniqueResultException() { + try { + AsciiCharacter ch = characters.findByIsControlTrueAndNumericValueBetween(10, 15); + fail("Unexpected result of findByIsControlTrueAndNumericValueBetween(10, 15): " + ch.getHexadecimal()); + } catch (NonUniqueResultException x) { + log.info("testNonUniqueResultException expected to catch exception " + x + ". Printing its stack trace:"); + x.printStackTrace(System.out); + // test passes + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + } + + @Test + public void testNot() { + NaturalNumber[] n; + try { + n = numbers.findByNumTypeNot( + NumberType.COMPOSITE, + Limit.of(8), + Order.by(Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + return; + } else { + throw x; + } + } + assertEquals(8, n.length); + assertEquals(1L, n[0].getId()); + assertEquals(2L, n[1].getId()); + assertEquals(3L, n[2].getId()); + assertEquals(5L, n[3].getId()); + assertEquals(7L, n[4].getId()); + assertEquals(11L, n[5].getId()); + assertEquals(13L, n[6].getId()); + assertEquals(17L, n[7].getId()); + } + + @Test + public void testOr() { + Stream found; + try { + found = positives.findByNumTypeOrFloorOfSquareRoot(NumberType.ONE, 2L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of Or. + return; + } else { + throw x; + } + } + + assertEquals(List.of(1L, 4L, 5L, 6L, 7L, 8L), + found.map(NaturalNumber::getId).sorted().collect(Collectors.toList())); + } + + @Test + public void testOrderByHasPrecedenceOverPageRequestSorts() { + PageRequest pagination = PageRequest.ofSize(8); + Order order = Order.by(Sort.asc("numTypeOrdinal"), Sort.desc("id")); + + Page page; + try { + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc( + 25L, pagination, order); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThan. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{23L, 19L, 17L, // square root rounds down to 4; prime + 24L, 22L, 21L, 20L, 18L}), // square root rounds down to 4; composite + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + + assertEquals(Arrays.toString(new Long[]{16L, // square root rounds down to 4; composite + 13L, 11L, // square root rounds down to 3; prime + 15L, 14L, 12L, 10L, 9L}), // square root rounds down to 3; composite + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + + assertEquals(Arrays.toString(new Long[]{7L, 5L, // square root rounds down to 2; prime + 8L, 6L, 4L, // square root rounds down to 2; composite + 1L, // square root rounds down to 1; one + 3L, 2L}), // square root rounds down to 1; prime + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + if (page.hasNext()) { + pagination = page.nextPageRequest(); + page = numbers.findByIdLessThanOrderByFloorOfSquareRootDesc(25L, pagination, order); + assertEquals(false, page.hasContent()); + } + } + + @Test + public void testOrderByHasPrecedenceOverSorts() { + Stream nums; + try { + nums = numbers.findByIdBetweenOrderByNumTypeOrdinalAsc( + 5L, 24L, + Order.by(Sort.desc("floorOfSquareRoot"), Sort.asc("id"))); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{17L, 19L, 23L, // prime; square root rounds down to 4 + 11L, 13L, // prime; square root rounds down to 3 + 5L, 7L, // prime; square root rounds down to 2 + 16L, 18L, 20L, 21L, 22L, 24L, // composite; square root rounds down to 4 + 9L, 10L, 12L, 14L, 15L, // composite; square root rounds down to 3 + 6L, 8L}), // composite; square root rounds down to 2 + Arrays.toString(nums.map(number -> number.getId()).toArray())); + } + + @Test + public void testPageOfNothing() { + PageRequest pagination = PageRequest.ofSize(6); + Page page; + try { + page = characters.findByNumericValueBetween(150, 160, pagination, + Order.by(_AsciiCharacter.id.asc())); + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(0, page.numberOfElements()); + assertEquals(0, page.stream().count()); + assertEquals(0, page.content().size()); + assertEquals(false, page.hasContent()); + assertEquals(false, page.iterator().hasNext()); + try { + assertEquals(0L, page.totalElements()); + assertEquals(0L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + } + + @Test + public void testPartialQueryOrderBy() { + + assertEquals(List.of('A', 'B', 'C', 'D', 'E', 'F'), + characters.alphabetic(Limit.range(65, 70)) + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testPartialQuerySelectAndOrderBy() { + + Character[] chars = characters.reverseAlphabetic(Limit.range(6, 13)); + for (int i = 0; i < chars.length; i++) { + assertEquals("zyxwvuts".charAt(i), chars[i]); + } + } + + @Test + public void testPrimaryEntityClassDeterminedByLifeCycleMethods() { + assertEquals(4L, customRepo.countByIdIn(Set.of(2L, 15L, 37L, -5L, 60L))); + + assertEquals(true, customRepo.existsByIdIn(Set.of(17L, 14L, -1L))); + + assertEquals(false, customRepo.existsByIdIn(Set.of(-10L, -12L, -14L))); + } + + @Test + public void testQueryWithNot() { + + // 'NOT LIKE' excludes '@' + // 'NOT IN' excludes 'E' and 'G' + // 'NOT BETWEEN' excludes 'H' through 'N'. + Character[] abcdfo; + try { + abcdfo = characters.getABCDFO(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Like + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(6, abcdfo.length); + for (int i = 0; i < abcdfo.length; i++) { + assertEquals("ABCDFO".charAt(i), abcdfo[i]); + } + } + + @Test + public void testQueryWithNull() { + try { + assertEquals("4a", characters.hex('J').orElseThrow()); + assertEquals("44", characters.hex('D').orElseThrow()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of Contains. + // Key-Value databases might not be capable of And. + return; + } else { + throw x; + } + } + } + + @Test + public void testQueryWithOr() { + PageRequest page1Request = PageRequest.ofSize(4); + CursoredPage page1; + + try { + page1 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page1Request); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + // Column and Key-Value databases might not be capable of JPQL OR. + // Column and Key-Value databases might not be capable of sorting. + return; + } + + assertEquals(List.of(16L, 18L, 8L, 9L), + page1.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + assertEquals(true, page1.hasTotals()); + assertEquals(true, page1.hasNext()); + try { + assertEquals(3L, page1.totalPages()); + assertEquals(12L, page1.totalElements()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + CursoredPage page2; + + try { + page2 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page1.nextPageRequest()); + } catch (UnsupportedOperationException x) { + // Test passes: Jakarta Data providers must raise UnsupportedOperationException when the database + // is not capable of cursor-based pagination. + return; + } + + assertEquals(List.of(10L, 11L, 12L, 13L), + page2.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + assertEquals(true, page2.hasNext()); + + CursoredPage page3 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page2.nextPageRequest()); + + assertEquals(List.of(14L, 15L, 4L, 6L), + page3.stream() + .map(NaturalNumber::getId) + .collect(Collectors.toList())); + + if (page3.hasNext()) { + CursoredPage page4 = positives.withBitCountOrOfTypeAndBelow((short) 4, + NumberType.COMPOSITE, 20L, + Sort.desc("numBitsRequired"), + Sort.asc("id"), + page3.nextPageRequest()); + assertEquals(false, page4.hasContent()); + } + } + + @Test + public void testQueryWithParenthesis() { + + try { + assertEquals( + List.of(15L, 7L, 5L, 3L, 1L), + positives.oddAndEqualToOrBelow(15L, 9L)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.DOCUMENT)) { + // Document, Column, and Key-Value databases might not be capable of parentheses. + // Column and Key-Value databases might not be capable of JDQL OR. + // Key-Value databases might not be capable of < in JDQL. + // Key-Value databases might not be capable of JDQL AND. + return; + } else { + throw x; + } + } + } + + @Test + public void testSingleEntity() { + AsciiCharacter ch; + try { + ch = characters.findByHexadecimalIgnoreCase("2B"); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + return; // NoSQL databases might not be capable of IgnoreCase + } else { + throw x; + } + } + + assertEquals('+', ch.getThisCharacter()); + assertEquals("2b", ch.getHexadecimal()); + assertEquals(43, ch.getNumericValue()); + assertEquals(false, ch.isControl()); + } + + @Test + public void testSliceOfNothing() { + PageRequest pagination = PageRequest.ofSize(5).withoutTotal(); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.COMPOSITE, 1L, pagination, Sort.desc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(false, page.hasContent()); + assertEquals(0, page.content().size()); + assertEquals(0, page.numberOfElements()); + } + + @Test + public void testStaticMetamodelAscendingSorts() { + assertEquals(Sort.asc("id"), _AsciiChar.id.asc()); + assertEquals(Sort.ascIgnoreCase(_AsciiChar.HEXADECIMAL), _AsciiChar.hexadecimal.ascIgnoreCase()); + assertEquals(Sort.ascIgnoreCase("thisCharacter"), _AsciiChar.thisCharacter.ascIgnoreCase()); + + PageRequest pageRequest = PageRequest.ofSize(6); + Page page1; + try { + page1 = characters.findByNumericValueBetween( + 68, 90, pageRequest, + Order.by(_AsciiChar.numericValue.asc())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(List.of('D', 'E', 'F', 'G', 'H', 'I'), + page1.stream() + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testStaticMetamodelAscendingSortsPreGenerated() { + assertEquals(Sort.asc("id"), _AsciiCharacter.id.asc()); + assertEquals(Sort.asc("isControl"), _AsciiCharacter.isControl.asc()); + assertEquals(Sort.ascIgnoreCase(_AsciiCharacter.HEXADECIMAL), _AsciiCharacter.hexadecimal.ascIgnoreCase()); + assertEquals(Sort.ascIgnoreCase("thisCharacter"), _AsciiCharacter.thisCharacter.ascIgnoreCase()); + + PageRequest pageRequest = PageRequest.ofSize(7); + Page page1; + try { + page1 = characters.findByNumericValueBetween( + 100, 122, pageRequest, + Order.by(_AsciiCharacter.numericValue.asc())); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } else { + throw x; + } + } + + assertEquals(List.of('d', 'e', 'f', 'g', 'h', 'i', 'j'), + page1.stream() + .map(AsciiCharacter::getThisCharacter) + .collect(Collectors.toList())); + } + + @Test + public void testStaticMetamodelAttributeNames() { + assertEquals(_AsciiChar.HEXADECIMAL, _AsciiChar.hexadecimal.name()); + assertEquals(_AsciiChar.ID, _AsciiChar.id.name()); + assertEquals("isControl", _AsciiChar.isControl.name()); + assertEquals(_AsciiChar.NUMERICVALUE, _AsciiChar.numericValue.name()); + assertEquals("thisCharacter", _AsciiChar.thisCharacter.name()); + } + + @Test + public void testStaticMetamodelAttributeNamesPreGenerated() { + assertEquals(_AsciiCharacter.HEXADECIMAL, _AsciiCharacter.hexadecimal.name()); + assertEquals(_AsciiCharacter.ID, _AsciiCharacter.id.name()); + assertEquals("isControl", _AsciiCharacter.isControl.name()); + assertEquals(_AsciiChar.NUMERICVALUE, _AsciiCharacter.numericValue.name()); + assertEquals("thisCharacter", _AsciiCharacter.thisCharacter.name()); + } + + @Test + public void testStaticMetamodelDescendingSorts() { + assertEquals(Sort.desc(_AsciiChar.ID), _AsciiChar.id.desc()); + assertEquals(Sort.descIgnoreCase("hexadecimal"), _AsciiChar.hexadecimal.descIgnoreCase()); + assertEquals(Sort.descIgnoreCase("thisCharacter"), _AsciiChar.thisCharacter.descIgnoreCase()); + + Sort sort = _AsciiChar.numericValue.desc(); + AsciiCharacter[] found; + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 30, "1", sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GreaterThanEqual. + return; + } else { + throw x; + } + } + assertEquals(3, found.length); + assertEquals('q', found[0].getThisCharacter()); + assertEquals('a', found[1].getThisCharacter()); + assertEquals('Q', found[2].getThisCharacter()); + } + + @Test + public void testStaticMetamodelDescendingSortsPreGenerated() { + assertEquals(Sort.desc(_AsciiCharacter.ID), _AsciiCharacter.id.desc()); + assertEquals(Sort.desc("isControl"), _AsciiCharacter.isControl.desc()); + assertEquals(Sort.descIgnoreCase("hexadecimal"), _AsciiCharacter.hexadecimal.descIgnoreCase()); + assertEquals(Sort.descIgnoreCase("thisCharacter"), _AsciiCharacter.thisCharacter.descIgnoreCase()); + + Sort sort = _AsciiCharacter.numericValue.desc(); + AsciiCharacter[] found; + try { + found = characters.findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith( + 30, "4", sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of EndsWith. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GreaterThanEqual. + return; + } else { + throw x; + } + } + assertEquals(3, found.length); + assertEquals('t', found[0].getThisCharacter()); + assertEquals('d', found[1].getThisCharacter()); + assertEquals('T', found[2].getThisCharacter()); + } + + @Test + public void testStreamsFromList() { + List chars; + try { + chars = characters.findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual( + 109, 101); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of GTE/LTE. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Character[]{Character.valueOf('e'), + Character.valueOf('f'), + Character.valueOf('g'), + Character.valueOf('h'), + Character.valueOf('i'), + Character.valueOf('j'), + Character.valueOf('k'), + Character.valueOf('l'), + Character.valueOf('m')}), + Arrays.toString(chars.stream().map(ch -> ch.getThisCharacter()).sorted().toArray())); + + assertEquals(101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109, + chars.stream().mapToInt(AsciiCharacter::getNumericValue).sum()); + + Set sorted = new TreeSet<>(); + chars.forEach(ch -> sorted.add(ch.getHexadecimal())); + assertEquals(new TreeSet<>(Set.of("65", "66", "67", "68", "69", "6a", "6b", "6c", "6d")), + sorted); + + List empty = characters.findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual(115, 120); + assertEquals(false, empty.iterator().hasNext()); + assertEquals(0L, empty.stream().count()); + } + + @Test + public void testThirdAndFourthPagesOf10() { + Order order = Order.by(_AsciiCharacter.numericValue.asc()); + PageRequest third10 = PageRequest.ofPage(3).size(10); + Page page; + try { + page = characters.findByNumericValueBetween(48, 90, third10, order); // 'D' to 'M' + } catch (UnsupportedOperationException x) { + // Some NoSQL databases lack the ability to count the total results + // and therefore cannot support a return type of Page. + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of Between. + return; + } + + assertEquals(3, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("44:D;45:E;46:F;47:G;48:H;49:I;4a:J;4b:K;4c:L;4d:M;", + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + + assertEquals(true, page.hasNext()); + PageRequest fourth10 = page.nextPageRequest(); + page = characters.findByNumericValueBetween(48, 90, fourth10, order); // 'N' to 'W' + + assertEquals(4, page.pageRequest().page()); + assertEquals(true, page.hasContent()); + assertEquals(10, page.numberOfElements()); + try { + assertEquals(43L, page.totalElements()); + assertEquals(5L, page.totalPages()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // Some NoSQL databases lack the ability to count the total results + } else { + throw x; + } + } + + assertEquals("4e:N;4f:O;50:P;51:Q;52:R;53:S;54:T;55:U;56:V;57:W;", + page.stream() + .map(c -> c.getHexadecimal() + ':' + c.getThisCharacter() + ';') + .reduce("", String::concat)); + } + + @Test + public void testThirdAndFourthSlicesOf5() { + PageRequest third5 = PageRequest.ofPage(3).size(5).withoutTotal(); + Sort sort = Sort.desc("id"); + Page page; + try { + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual( + NumberType.PRIME, 8L, third5, sort); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(3, page.pageRequest().page()); + assertEquals(5, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{37L, 31L, 29L, 23L, 19L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + + assertEquals(true, page.hasNext()); + PageRequest fourth5 = page.nextPageRequest(); + + page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType.PRIME, 8L, fourth5, sort); + + assertEquals(4, page.pageRequest().page()); + assertEquals(5, page.numberOfElements()); + + assertEquals(Arrays.toString(new Long[]{17L, 13L, 11L, 7L, 5L}), + Arrays.toString(page.stream().map(number -> number.getId()).toArray())); + } + + @Test + public void testTrue() { + Iterable odd; + try { + odd = positives.findByIsOddTrueAndIdLessThanEqualOrderByIdDesc(10L); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + // Key-Value databases might not be capable of And. + // Key-Value databases might not be capable of LessThanEqual. + // Key-Value databases might not be capable of True/False comparison. + return; + } else { + throw x; + } + } + + Iterator it = odd.iterator(); + + assertEquals(true, it.hasNext()); + assertEquals(9L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(7L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(5L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(3L, it.next().getId()); + + assertEquals(true, it.hasNext()); + assertEquals(1L, it.next().getId()); + + assertEquals(false, it.hasNext()); + } + + @Test + public void testUpdateQueryWithoutWhereClause() { + // Ensure there is no data left over from other tests: + + try { + shared.removeAll(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + boxes.deleteAll(boxes.findAll().toList()); + } else { + throw x; + } + } + + + + boxes.saveAll(List.of(Box.of("TestUpdateQueryWithoutWhereClause-01", 125, 117, 44), + Box.of("TestUpdateQueryWithoutWhereClause-02", 173, 165, 52), + Box.of("TestUpdateQueryWithoutWhereClause-03", 229, 221, 60))); + + + + boolean resized; + try { + // increases length by 12, decreases width by 12, and doubles the height + assertEquals(3L, shared.resizeAll(12, 2)); + resized = true; + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + resized = false; + } else { + throw x; + } + } + + + + if (resized) { + Box b1 = boxes.findById("TestUpdateQueryWithoutWhereClause-01").orElseThrow(); + assertEquals(137, b1.length); // increased by 12 + assertEquals(105, b1.width); // decreased by 12 + assertEquals(88, b1.height); // increased by factor of 2 + + Box b2 = boxes.findById("TestUpdateQueryWithoutWhereClause-02").orElseThrow(); + assertEquals(185, b2.length); // increased by 12 + assertEquals(153, b2.width); // decreased by 12 + assertEquals(104, b2.height); // increased by factor of 2 + + Box b3 = boxes.findById("TestUpdateQueryWithoutWhereClause-03").orElseThrow(); + assertEquals(241, b3.length); // increased by 12 + assertEquals(209, b3.width); // decreased by 12 + assertEquals(120, b3.height); // increased by factor of 2 + } + + try { + var removeAllResult = shared.removeAll(); + assertEquals(3, removeAllResult); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for removing entities. + boxes.deleteAll(boxes.findAll().toList()); + } else { + throw x; + } + } + + + + try { + assertEquals(0L, shared.resizeAll(2, 1)); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + } else { + throw x; + } + } + } + + @Test + public void testUpdateQueryWithWhereClause() { + try { + // Ensure there is no data left over from other tests: + shared.deleteIfPositive(); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of And. + } else if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + shared.deleteIfPositiveWithoutReturnRecords(); + } else { + throw x; + } + } + + UUID id1 = shared.create(Coordinate.of("first", 1.41d, 5.25f)).id; + UUID id2 = shared.create(Coordinate.of("second", 2.2d, 2.34f)).id; + + + + float c1yExpected; + double c1xExpected; + try { + assertEquals(true, shared.move(id1, 1.23d, 1.5f)); + c1yExpected = 3.5f; // 5.25 / 1.5 = 3.5 + c1xExpected = 1.23D; + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH)) { + // NoSQL databases might not be capable of arithmetic in updates. + c1yExpected = 5.25f; + c1xExpected = 1.41D;// no change + } else { + throw x; + } + } + + + + Coordinate c1 = shared.withUUID(id1).orElseThrow(); + assertEquals(c1xExpected, c1.x, 0.001d); + assertEquals(c1yExpected, c1.y, 0.001f); + + Coordinate c2 = shared.withUUID(id2).orElseThrow(); + assertEquals(2.2d, c2.x, 0.001d); + assertEquals(2.34f, c2.y, 0.001f); + + try { + assertEquals(2, shared.deleteIfPositive()); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.KEY_VALUE)) { + return; // Key-Value databases might not be capable of And. + } else if (type.isKeywordSupportAtOrBelow(DatabaseType.GRAPH) && TestProperty.delay.isSet()) { + // NoSQL databases with eventual consistency might not be capable + // of counting removed entities. + // Use alternative approach for ensuring no data is present: + shared.deleteIfPositiveWithoutReturnRecords(); + } else { + throw x; + } + } + + + assertEquals(false, shared.withUUID(id1).isPresent()); + assertEquals(false, shared.withUUID(id2).isPresent()); + } + + @Test + public void testVarargsSort() { + List list; + try { + list = numbers.findByIdLessThanEqual( + 12L, + Sort.asc("floorOfSquareRoot"), + Sort.desc("numBitsRequired"), + Sort.asc("id")); + } catch (UnsupportedOperationException x) { + if (type.isKeywordSupportAtOrBelow(DatabaseType.COLUMN)) { + // Column and Key-Value databases might not be capable of sorting. + // Key-Value databases might not be capable of LessThanEqual. + return; + } else { + throw x; + } + } + + assertEquals(Arrays.toString(new Long[]{2L, 3L, // square root rounds down to 1; 2 bits + 1L, // square root rounds down to 1; 1 bit + 8L, // square root rounds down to 2; 4 bits + 4L, 5L, 6L, 7L, // square root rounds down to 2; 3 bits + 9L, 10L, 11L, 12L}), // square root rounds down to 3; 4 bits + Arrays.toString(list.stream().map(number -> number.getId()).toArray())); + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/MultipleEntityRepo.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/MultipleEntityRepo.java new file mode 100644 index 0000000000..212132a369 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/entity/MultipleEntityRepo.java @@ -0,0 +1,44 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.entity; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.repository.Insert; +import jakarta.data.repository.Query; + +import java.util.Optional; +import java.util.UUID; + +/** + * A repository that performs operations on different types of entities. + */ +@JdbcRepository(dialect = Dialect.H2) +public interface MultipleEntityRepo { // Do not add a primary entity type. + + // Methods for Box entity: + + @Insert + Box[] addAll(Box... boxes); + + @Query("DELETE FROM Box") + long removeAll(); + + @Query("UPDATE Box SET length = length + ?1, width = width - ?1, height = height * ?2") + long resizeAll(int lengthIncrementWidthDecrement, int heightFactor); + + // Methods for Coordinate entity: + + @Insert + Coordinate create(Coordinate c); + + @Query("DELETE FROM Coordinate WHERE x > 0.0d AND y > 0.0f") + long deleteIfPositive(); + + @Query("DELETE FROM Coordinate WHERE x > 0.0d AND y > 0.0f") + void deleteIfPositiveWithoutReturnRecords(); + + @Query("UPDATE Coordinate SET x = :newX, y = y / :yDivisor WHERE id = :id") + boolean move(UUID id, double newX, float yDivisor); + + @Query("WHERE id = ?1") + Optional withUUID(UUID id); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/Catalog.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/Catalog.java new file mode 100644 index 0000000000..84014f0827 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/Catalog.java @@ -0,0 +1,122 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.persistence; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.Order; +import jakarta.data.repository.By; +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.Insert; +import jakarta.data.repository.OrderBy; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; +import jakarta.data.repository.Update; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static jakarta.data.repository.By.ID; + +@Repository +@JdbcRepository(dialect = Dialect.H2) +public interface Catalog extends DataRepository { + + @Insert + CatalogProduct add(CatalogProduct product); + + @Insert + CatalogProduct[] addMultiple(CatalogProduct... products); + + @Find + Optional get(String productNum); + + @Update + CatalogProduct modify(CatalogProduct product); + + @Update + CatalogProduct[] modifyMultiple(CatalogProduct... products); + + @Delete + void remove(CatalogProduct product); + + @Delete + void removeMultiple(CatalogProduct... products); + + @Save + void customSave(CatalogProduct product); + + @Delete + void deleteById(@By(ID) String productNum); + + long deleteByProductNumLike(String pattern); + + long countByPriceGreaterThanEqual(Double price); + + @Query("WHERE LENGTH(name) = ?1 AND price < ?2 ORDER BY name") + List findByNameLengthAndPriceBelow(int nameLength, double maxPrice); + + @OrderBy("name") + @Query("WHERE LENGTH(name) = ?1 AND price < ?2") + List findByNameLengthAndPriceBelowNameAsc(int nameLength, double maxPrice); + + @OrderBy(value = "name", descending = true) + @Query("WHERE LENGTH(name) = ?1 AND price < ?2") + List findByNameLengthAndPriceBelowNameDesc(int nameLength, double maxPrice); + + @Find + @OrderBy(value = "name") + List allSortedByNameAsc(); + + @Find + @OrderBy(value = "name", descending = true) + List allSortedByNameDesc(); + + @Find + @OrderBy(value = "name", ignoreCase = true) + List allSortedByNameAscIgnoreCase(); + + @Find + @OrderBy(value = "name", descending = true, ignoreCase = true) + List allSortedByNameDescIgnoreCase(); + + @Find + @OrderBy(value = "name", descending = true, ignoreCase = true) + List findAll(); + + List findByNameLike(String name); + + @OrderBy(value = "price", descending = true) + Stream findByPriceNotNullAndPriceLessThanEqual(double maxPrice); + + List findByPriceNull(); + + List findByProductNumBetween(String first, String last, Order sorts); + + List findByProductNumLike(String productNum); + +// EntityManager getEntityManager(); +// +// default double sumPrices(Department... departments) { +// StringBuilder jpql = new StringBuilder("SELECT SUM(o.price) FROM Product o"); +// for (int d = 1; d <= departments.length; d++) { +// jpql.append(d == 1 ? " WHERE " : " OR "); +// jpql.append('?').append(d).append(" MEMBER OF o.departments"); +// } +// +// EntityManager em = getEntityManager(); +// TypedQuery query = em.createQuery(jpql.toString(), Double.class); +// for (int d = 1; d <= departments.length; d++) { +// query.setParameter(d, departments[d - 1]); +// } +// return query.getSingleResult(); +// } + + @Query("FROM CatalogProduct WHERE (:rate * price <= :max AND :rate * price >= :min) ORDER BY name") + Stream withTaxBetween(@Param("min") double mininunTaxAmount, + @Param("max") double maximumTaxAmount, + @Param("rate") double taxRate); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/CatalogProduct.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/CatalogProduct.java new file mode 100644 index 0000000000..86dc4dfff9 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/CatalogProduct.java @@ -0,0 +1,111 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.persistence; + +import io.micronaut.core.annotation.Nullable; +import jakarta.persistence.Basic; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; +import org.testcontainers.shaded.org.checkerframework.checker.units.qual.N; + +import java.util.Collections; +import java.util.Set; + +@Entity +public class CatalogProduct { + public enum Department { + APPLIANCES, AUTOMOTIVE, CLOTHING, CRAFTS, ELECTRONICS, FURNITURE, GARDEN, GROCERY, OFFICE, PHARMACY, SPORTING_GOODS, TOOLS + } + +// @ElementCollection(fetch = FetchType.EAGER) + @Transient + private Set departments; + + @Basic(optional = false) + private String name; + + @Nullable + private Double price; + + @Basic(optional = false) + @Id + private String productNum; + + @Transient + private Double surgePrice; + + @Version + private long versionNum; + + public static CatalogProduct of(String name, Double price, String productNum, Department... departments) { + return new CatalogProduct(name, price, price, productNum, departments); + } + + private CatalogProduct(String name, Double price, Double surgePrice, String productNum, Department... departments) { + this.productNum = productNum; + this.name = name; + this.price = price; + this.surgePrice = surgePrice; + this.departments = departments == null ? Collections.emptySet() : Set.of(departments); + } + + public CatalogProduct() { + //do nothing + } + + public Set getDepartments() { + return departments; + } + + public void setDepartments(Set departments) { + this.departments = departments; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public String getProductNum() { + return productNum; + } + + public void setProductNum(String productNum) { + this.productNum = productNum; + } + + public long getVersionNum() { + return this.versionNum; + } + + public Double getSurgePrice() { + return surgePrice; + } + + public void setSurgePrice(Double surgePrice) { + this.surgePrice = surgePrice; + } + + public void setVersionNum(long versionNum) { + this.versionNum = versionNum; + } + + @Override + public String toString() { + return "Product [departments=" + departments + ", name=" + name + ", price=" + price + ", productNum=" + + productNum + ", surgePrice=" + surgePrice + ", versionNum=" + versionNum + "]"; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/PersistenceEntityTests.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/PersistenceEntityTests.java new file mode 100644 index 0000000000..68097d3665 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/persistence/PersistenceEntityTests.java @@ -0,0 +1,395 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.persistence; + +import io.micronaut.context.annotation.Property; +import io.micronaut.data.jdbc.h2.H2DBProperties; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.exceptions.EntityExistsException; +import jakarta.data.exceptions.OptimisticLockingFailureException; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.micronaut.data.jdbc.h2.jakarta_data.persistence.CatalogProduct.Department.GROCERY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Execute tests with a Persistence specific entity with a repository that requires read and writes (AKA not read-only) + */ +@Property(name = "jpa.default.properties.hibernate.show_sql", value = "true") +@H2DBProperties +@MicronautTest(transactional = false) +public class PersistenceEntityTests { + + @Inject + Catalog catalog; + + @Test + public void testEntityManager() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("bicycle", 359.98, "TEST-PROD-81", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("shin guards", 8.99, "TEST-PROD-83", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("dishwasher", 788.10, "TEST-PROD-86", CatalogProduct.Department.APPLIANCES)); + catalog.customSave(CatalogProduct.of("socks", 5.99, "TEST-PROD-87", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("volleyball", 10.99, "TEST-PROD-89", CatalogProduct.Department.SPORTING_GOODS)); + +// assertEquals(385.95, catalog.sumPrices(Department.CLOTHING, Department.SPORTING_GOODS), 0.001); +// assertEquals(794.09, catalog.sumPrices(Department.CLOTHING, Department.APPLIANCES), 0.001); + } + + @Test + public void testIdAttributeWithDifferentName() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("apple", 1.19, "TEST-PROD-12", GROCERY)); + catalog.customSave(CatalogProduct.of("pear", 0.99, "TEST-PROD-14", GROCERY)); + catalog.customSave(CatalogProduct.of("orange", 1.09, "TEST-PROD-16", GROCERY)); + catalog.customSave(CatalogProduct.of("banana", 0.49, "TEST-PROD-17", GROCERY)); + catalog.customSave(CatalogProduct.of("plum", 0.89, "TEST-PROD-18", GROCERY)); + + Iterable found = catalog.findByProductNumBetween("TEST-PROD-13", "TEST-PROD-17", Order.by(Sort.asc("name"))); + Iterator it = found.iterator(); + assertEquals(true, it.hasNext()); + assertEquals("banana", it.next().getName()); + assertEquals(true, it.hasNext()); + assertEquals("orange", it.next().getName()); + assertEquals(true, it.hasNext()); + assertEquals("pear", it.next().getName()); + assertEquals(false, it.hasNext()); + + assertEquals(5L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testInsertEntityThatAlreadyExists() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct prod1 = catalog.add(CatalogProduct.of("watermelon", 6.29, "TEST-PROD-94", GROCERY)); + + try { + catalog.add(CatalogProduct.of("pineapple", 1.99, "TEST-PROD-94", GROCERY)); + fail("Should not be able to insert an entity that has same Id as another entity."); + } catch (EntityExistsException x) { + // expected + } + + Optional result; + result = catalog.get("TEST-PROD-94"); + assertEquals(true, result.isPresent()); + + catalog.remove(prod1); + + result = catalog.get("TEST-PROD-94"); + assertEquals(false, result.isPresent()); + } + + @Test + public void testLike() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("celery", 1.57, "TEST-PROD-31", GROCERY)); + catalog.customSave(CatalogProduct.of("mushrooms", 1.89, "TEST-PROD-32", GROCERY)); + catalog.customSave(CatalogProduct.of("carrots", 1.39, "TEST-PROD-33", GROCERY)); + + List found = catalog.findByNameLike("%r_o%"); + assertEquals(List.of("carrots", "mushrooms"), + found.stream().map(CatalogProduct::getName).sorted().collect(Collectors.toList())); + + assertEquals(3L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testNotRunOnNOSQL() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + List products = new ArrayList<>(); + products.add(CatalogProduct.of("pen", 2.50, "TEST-PROD-01")); + products.add(CatalogProduct.of("pencil", 1.25, "TEST-PROD-02")); + products.add(CatalogProduct.of("marker", 3.00, "TEST-PROD-03")); + products.add(CatalogProduct.of("calculator", 15.00, "TEST-PROD-04")); + products.add(CatalogProduct.of("ruler", 2.00, "TEST-PROD-05")); + + products.forEach(product -> catalog.customSave(product)); + + long countExpensive = catalog.countByPriceGreaterThanEqual(2.99); + assertEquals(2L, countExpensive, "Expected two products to be more than 3.00"); + + assertEquals(5L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testMultipleInsertUpdateDelete() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct[] added = catalog.addMultiple(CatalogProduct.of("blueberries", 2.49, "TEST-PROD-95", GROCERY), + CatalogProduct.of("strawberries", 2.29, "TEST-PROD-96", GROCERY), + CatalogProduct.of("raspberries", 2.39, "TEST-PROD-97", GROCERY)); + + assertEquals(3, added.length); + + // The position of resulting entities must match the parameter + assertEquals("blueberries", added[0].getName()); + assertEquals("TEST-PROD-95", added[0].getProductNum()); + assertEquals(2.49, added[0].getPrice(), 0.001); +// assertEquals(Set.of(GROCERY), added[0].getDepartments()); + CatalogProduct blueberries = added[0]; + + assertEquals("strawberries", added[1].getName()); + assertEquals("TEST-PROD-96", added[1].getProductNum()); + assertEquals(2.29, added[1].getPrice(), 0.001); +// assertEquals(Set.of(GROCERY), added[1].getDepartments()); + CatalogProduct strawberries = added[1]; + + assertEquals("raspberries", added[2].getName()); + assertEquals("TEST-PROD-97", added[2].getProductNum()); + assertEquals(2.39, added[2].getPrice(), 0.001); +// assertEquals(Set.of(GROCERY), added[2].getDepartments()); + CatalogProduct raspberries = added[2]; + + strawberries.setPrice(1.99); + raspberries.setPrice(2.34); + CatalogProduct[] modified = catalog.modifyMultiple(raspberries, strawberries); + assertEquals(2, modified.length); + + assertEquals("raspberries", modified[0].getName()); + assertEquals("TEST-PROD-97", modified[0].getProductNum()); + assertEquals(2.34, modified[0].getPrice(), 0.001); +// assertEquals(Set.of(GROCERY), modified[0].getDepartments()); + raspberries = modified[0]; + + assertEquals("strawberries", modified[1].getName()); + assertEquals("TEST-PROD-96", modified[1].getProductNum()); + assertEquals(1.99, modified[1].getPrice(), 0.001); +// assertEquals(Set.of(GROCERY), modified[1].getDepartments()); + strawberries = modified[1]; + + // Attempt to remove entities that do not exist in the database + try { + catalog.removeMultiple(CatalogProduct.of("blackberries", 2.59, "TEST-PROD-98", GROCERY), + CatalogProduct.of("gooseberries", 2.79, "TEST-PROD-99", GROCERY)); + fail("OptimisticLockingFailureException must be raised because the entities are not found for deletion."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + // Remove only the entities that actually exist in the database + catalog.removeMultiple(strawberries, blueberries, raspberries); + + Iterable remaining = catalog.findByProductNumBetween("TEST-PROD-95", "TEST-PROD-99", Order.by()); + assertEquals(false, remaining.iterator().hasNext()); + } + + @Test + public void testNull() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("spinach", 2.28, "TEST-PROD-51", GROCERY)); + catalog.customSave(CatalogProduct.of("broccoli", 2.49, "TEST-PROD-52", GROCERY)); + catalog.customSave(CatalogProduct.of("rhubarb", null, "TEST-PROD-53", GROCERY)); + catalog.customSave(CatalogProduct.of("potato", 0.79, "TEST-PROD-54", GROCERY)); + + Collection found = catalog.findByPriceNull(); + + assertEquals(1, found.size()); + assertEquals("rhubarb", found.iterator().next().getName()); + + assertEquals(List.of("spinach", "potato"), + catalog.findByPriceNotNullAndPriceLessThanEqual(2.30) + .map(CatalogProduct::getName) + .collect(Collectors.toList())); + + assertEquals(4L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testSort() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, GROCERY)); + catalog.customSave(CatalogProduct.of("chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found = catalog.findByNameLengthAndPriceBelowNameAsc(10, 100.0); + + assertEquals(List.of("basketball", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findByNameLengthAndPriceBelowNameDesc(10, 100.0); + + assertEquals(List.of("toothpaste", "basketball"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameAsc(); + + assertEquals(List.of("baseball cap", "basketball", "chisel", "computer", "sunblock", "sweater", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDesc(); + + assertEquals(List.of("toothpaste", "sweater", "sunblock", "computer", "chisel", "basketball", "baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + } + + @Test + public void testSortIgnoreCase() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("Sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, GROCERY)); + catalog.customSave(CatalogProduct.of("Chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("Sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("Baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found; + + found = catalog.allSortedByNameAsc(); + + assertEquals(List.of("Baseball cap", "Chisel", "Sunblock", "Sweater", "basketball", "computer", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDesc(); + + assertEquals(List.of("toothpaste", "computer", "basketball", "Sweater", "Sunblock", "Chisel", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameAscIgnoreCase(); + + assertEquals(List.of("Baseball cap", "basketball", "Chisel", "computer", "Sunblock", "Sweater", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.allSortedByNameDescIgnoreCase(); + + assertEquals(List.of("toothpaste", "Sweater", "Sunblock", "computer", "Chisel", "basketball", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findAll(); + + assertEquals(List.of("toothpaste", "Sweater", "Sunblock", "computer", "Chisel", "basketball", "Baseball cap"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + } + + @Test + public void testQueryWithNamedParameters() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("tape measure", 7.29, "TEST-PROD-61", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("pry bar", 4.39, "TEST-PROD-62", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("hammer", 8.59, "TEST-PROD-63", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("adjustable wrench", 4.99, "TEST-PROD-64", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("framing square", 9.88, "TEST-PROD-65", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("rasp", 6.79, "TEST-PROD-66", CatalogProduct.Department.TOOLS)); + + Stream found = catalog.withTaxBetween(0.4, 0.6, 0.08125); + + assertEquals(List.of("adjustable wrench", "rasp", "tape measure"), + found.map(CatalogProduct::getName).collect(Collectors.toList())); + + assertEquals(6L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } + + @Test + public void testQueryWithPositionalParameters() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + catalog.customSave(CatalogProduct.of("sweater", 23.88, "TEST-PROD-71", CatalogProduct.Department.CLOTHING)); + catalog.customSave(CatalogProduct.of("toothpaste", 2.39, "TEST-PROD-72", CatalogProduct.Department.PHARMACY, GROCERY)); + catalog.customSave(CatalogProduct.of("chisel", 5.99, "TEST-PROD-73", CatalogProduct.Department.TOOLS)); + catalog.customSave(CatalogProduct.of("computer", 1299.50, "TEST-PROD-74", CatalogProduct.Department.ELECTRONICS, CatalogProduct.Department.OFFICE)); + catalog.customSave(CatalogProduct.of("sunblock", 5.98, "TEST-PROD-75", CatalogProduct.Department.PHARMACY, CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.GARDEN)); + catalog.customSave(CatalogProduct.of("basketball", 14.88, "TEST-PROD-76", CatalogProduct.Department.SPORTING_GOODS)); + catalog.customSave(CatalogProduct.of("baseball cap", 12.99, "TEST-PROD-77", CatalogProduct.Department.SPORTING_GOODS, CatalogProduct.Department.CLOTHING)); + + List found = catalog.findByNameLengthAndPriceBelow(10, 100.0); + + assertEquals(List.of("basketball", "toothpaste"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + found = catalog.findByNameLengthAndPriceBelow(8, 1000.0); + + assertEquals(List.of("sunblock"), + found.stream().map(CatalogProduct::getName).collect(Collectors.toList())); + + assertEquals(7L, catalog.deleteByProductNumLike("TEST-PROD-%")); + + } + + @Test + public void testVersionedInsertUpdateDelete() { + catalog.deleteByProductNumLike("TEST-PROD-%"); + + CatalogProduct prod1 = catalog.add(CatalogProduct.of("zucchini", 1.49, "TEST-PROD-91", GROCERY)); + CatalogProduct prod2 = catalog.add(CatalogProduct.of("cucumber", 1.29, "TEST-PROD-92", GROCERY)); + + long prod1InitialVersion = prod1.getVersionNum(); + long prod2InitialVersion = prod2.getVersionNum(); + + prod1.setPrice(1.59); + prod1 = catalog.modify(prod1); + + prod2.setPrice(1.39); + prod2 = catalog.modify(prod2); + + // Expect version number to change when modified + assertNotEquals(prod1InitialVersion, prod1.getVersionNum()); + assertNotEquals(prod2InitialVersion, prod2.getVersionNum()); + + long prod1SecondVersion = prod1.getVersionNum(); + + prod1.setPrice(1.54); + prod1 = catalog.modify(prod1); + + assertNotEquals(prod1SecondVersion, prod1.getVersionNum()); + assertNotEquals(prod1InitialVersion, prod1.getVersionNum()); + + // Update must not be made when the version does not match: + prod2.setVersionNum(prod2InitialVersion); + prod2.setPrice(1.34); + try { + catalog.modify(prod2); + fail("Must raise OptimisticLockingFailureException for entity instance with old version."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + catalog.remove(prod1); + + Optional found = catalog.get("TEST-PROD-91"); + assertEquals(false, found.isPresent()); + + try { + catalog.remove(prod1); // already removed + fail("Must raise OptimisticLockingFailureException for entity that was already removed from the database."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + prod2.setVersionNum(prod2InitialVersion); + try { + catalog.remove(prod2); // still at old version + fail("Must raise OptimisticLockingFailureException for entity with non-matching version."); + } catch (OptimisticLockingFailureException x) { + // expected + } + + assertEquals(1L, catalog.deleteByProductNumLike("TEST-PROD-%")); + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacter.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacter.java new file mode 100644 index 0000000000..1e104e6c28 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacter.java @@ -0,0 +1,63 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +public class AsciiCharacter implements Serializable { + private static final long serialVersionUID = 1L; + + @jakarta.persistence.Id + private long id; + + private int numericValue; + + private String hexadecimal; + + private char thisCharacter; + + private boolean isControl; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getNumericValue() { + return numericValue; + } + + public void setNumericValue(int numericValue) { + this.numericValue = numericValue; + } + + public String getHexadecimal() { + return hexadecimal; + } + + public void setHexadecimal(String hexadecimal) { + this.hexadecimal = hexadecimal; + } + + public char getThisCharacter() { + return thisCharacter; + } + + public void setThisCharacter(char thisCharacter) { + this.thisCharacter = thisCharacter; + } + + public boolean isControl() { + return isControl; + } + + public void setControl(boolean isControl) { + this.isControl = isControl; + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacters.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacters.java new file mode 100644 index 0000000000..9bcbdc1c7e --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharacters.java @@ -0,0 +1,100 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.By; +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * This is a read only repository that represents the set of AsciiCharacters from 0-256. + * This repository will be pre-populated at test startup and verified prior to running tests. + * This interface is required to inherit only from DataRepository in order to satisfy a TCK scenario. + */ +@Repository +@JdbcRepository(dialect = Dialect.H2) +public interface AsciiCharacters extends DataRepository, IdOperations { + + @Query(" ") // it is valid to have a query with no clauses + Stream all(Limit limit, Sort... sort); + + @Query("ORDER BY id ASC") + Stream alphabetic(Limit limit); + + long countByHexadecimalNotNull(); + + boolean existsByThisCharacter(char ch); + + @Find + AsciiCharacter find(char thisCharacter); + + @Find + List findAll(); + + @Find + Optional find(@By("thisCharacter") char ch, + @By("hexadecimal") String hex); + + List findByHexadecimalContainsAndIsControlNot(String substring, boolean isPrintable); + + Stream findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn(String minHex, + String maxHex, + Set excludeHex, + Order sorts); + + AsciiCharacter findByHexadecimalIgnoreCase(String hex); + + Stream findByIdBetween(long minimum, long maximum, Sort sort); + + AsciiCharacter findByIsControlTrueAndNumericValueBetween(int min, int max); + + Optional findByNumericValue(int id); + + Page findByNumericValueBetween(int min, int max, PageRequest pagination, Order order); + + List findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual(int max, int min); + + AsciiCharacter[] findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith(int minValue, String lastHexDigit, Sort sort); + + Optional findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc(String firstHexDigit, boolean isControlChar); + + @Query("select thisCharacter where hexadecimal like '4_'" + + " and hexadecimal not like '%0'" + + " and thisCharacter not in ('E', 'G')" + + " and id not between 72 and 78" + + " order by id asc") + Character[] getABCDFO(); + + @Query("SELECT hexadecimal WHERE hexadecimal IS NOT NULL AND thisCharacter = ?1") + Optional hex(char ch); + + @Query("WHERE hexadecimal <> ' ORDER BY isn''t a keyword when inside a literal' AND hexadecimal IN ('4a', '4b', '4c', ?1)") + Stream jklOr(String hex); + + default Stream retrieveAlphaNumericIn(long minId, long maxId) { + return findByIdBetween(minId, maxId, Sort.asc("id")) + .filter(c -> Character.isLetterOrDigit(c.getThisCharacter())); + } + + @Query("SELECT thisCharacter ORDER BY id DESC") + Character[] reverseAlphabetic(Limit limit); + + @Save + List saveAll(List characters); + + @Query("SELECT COUNT(THIS) WHERE numericValue <= 97 AND numericValue >= 74") + long twentyFour(); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharactersPopulator.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharactersPopulator.java new file mode 100644 index 0000000000..5b328d940c --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/AsciiCharactersPopulator.java @@ -0,0 +1,38 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public class AsciiCharactersPopulator implements Populator { + + public static AsciiCharactersPopulator get() { + return new AsciiCharactersPopulator(); + } + + @Override + public void populationLogic(AsciiCharacters repo) { + List dictonary = new ArrayList<>(); + + IntStream.range(1, 128) // Some databases don't support ASCII NULL character (0) + .forEach(value -> { + AsciiCharacter inst = new AsciiCharacter(); + + inst.setId(value); + inst.setNumericValue(value); + inst.setHexadecimal(Integer.toHexString(value)); + inst.setThisCharacter((char) value); + inst.setControl(Character.isISOControl((char) value)); + + dictonary.add(inst); + }); + + repo.saveAll(dictonary); + } + + @Override + public boolean isPopulated(AsciiCharacters repo) { + return repo.countByHexadecimalNotNull() == 127L; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/CustomRepository.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/CustomRepository.java new file mode 100644 index 0000000000..20ba592253 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/CustomRepository.java @@ -0,0 +1,29 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Insert; + +import java.util.List; +import java.util.Set; + +/** + * Do not add methods or inheritance to this interface. + * Its purpose is to test that without inheriting from a built-in repository, + * the lifecycle methods with the same entity class are what identifies the + * primary entity class to use for the count and exist methods. + */ +@JdbcRepository(dialect = Dialect.H2) +public interface CustomRepository { + + @Insert + void add(List list); + + long countByIdIn(Set ids); + + boolean existsByIdIn(Set ids); + + @Delete + void remove(List list); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/IdOperations.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/IdOperations.java new file mode 100644 index 0000000000..4139edd4b4 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/IdOperations.java @@ -0,0 +1,16 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import jakarta.data.Limit; +import jakarta.data.repository.Query; + +import java.util.List; +/** + * This interface contains common operations for the NaturalNumbers and AsciiCharacters repositories. + */ +public interface IdOperations { + long countByIdBetween(long minimum, long maximum); + + boolean existsById(long id); + @Query("SELECT id WHERE id >= :inclusiveMin ORDER BY id ASC") + List withIdEqualOrAbove(long inclusiveMin, Limit limit); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumber.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumber.java new file mode 100644 index 0000000000..05186455cd --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumber.java @@ -0,0 +1,79 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +public class NaturalNumber implements Serializable { + private static final long serialVersionUID = 1L; + + public enum NumberType { + ONE, PRIME, COMPOSITE + } + + @jakarta.persistence.Id + private long id; //AKA the value + + private boolean isOdd; + + private Short numBitsRequired; + + // Sorting on enum types is vendor-specific in Jakarta Data. + // Use numTypeOrdinal for sorting instead. + @jakarta.persistence.Enumerated(jakarta.persistence.EnumType.STRING) + private NumberType numType; // enum of ONE | PRIME | COMPOSITE + + private int numTypeOrdinal; // ordinal value of numType + + private long floorOfSquareRoot; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public boolean isOdd() { + return isOdd; + } + + public void setOdd(boolean isOdd) { + this.isOdd = isOdd; + } + + public Short getNumBitsRequired() { + return numBitsRequired; + } + + public void setNumBitsRequired(Short numBitsRequired) { + this.numBitsRequired = numBitsRequired; + } + + public NumberType getNumType() { + return numType; + } + + public void setNumType(NumberType numType) { + this.numType = numType; + } + + public int getNumTypeOrdinal() { + return numTypeOrdinal; + } + + public void setNumTypeOrdinal(int value) { + numTypeOrdinal = value; + } + + public long getFloorOfSquareRoot() { + return floorOfSquareRoot; + } + + public void setFloorOfSquareRoot(long floorOfSquareRoot) { + this.floorOfSquareRoot = floorOfSquareRoot; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbers.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbers.java new file mode 100644 index 0000000000..de300c98e8 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbers.java @@ -0,0 +1,68 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * This is a read only repository that represents the set of Natural Numbers from 1-100. + * This repository will be pre-populated at test startup and verified prior to running tests. + * + * TODO figure out a way to make this a ReadOnlyRepository instead. + */ +@Repository +@JdbcRepository(dialect = Dialect.H2) +public interface NaturalNumbers extends BasicRepository, IdOperations { + + long countAll(); + + CursoredPage findByFloorOfSquareRootOrderByIdAsc(long sqrtFloor, + PageRequest pagination); + + Stream findByIdBetweenOrderByNumTypeOrdinalAsc(long minimum, + long maximum, + Order sorts); + + List findByIdGreaterThanEqual(long minimum, + Limit limit, + Order sorts); + + NaturalNumber[] findByIdLessThan(long exclusiveMax, Sort primarySort, Sort secondarySort); + + List findByIdLessThanEqual(long maximum, Sort... sorts); + + Page findByIdLessThanOrderByFloorOfSquareRootDesc(long exclusiveMax, + PageRequest pagination, + Order order); + + CursoredPage findByNumTypeAndNumBitsRequiredLessThan(NumberType type, + short bitsUnder, + Order order, + PageRequest pagination); + + NaturalNumber[] findByNumTypeNot(NumberType notThisType, Limit limit, Order sorts); + + Page findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType type, + long maxSqrtFloor, + PageRequest pagination, + Sort sort); + + @Query("SELECT id WHERE isOdd = true AND id BETWEEN 21 AND ?1 ORDER BY id ASC") + Page oddsFrom21To(long max, PageRequest pageRequest); + + @Query("WHERE isOdd = false AND numType = io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType.PRIME") + Optional two(); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbersPopulator.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbersPopulator.java new file mode 100644 index 0000000000..4ada3fecb1 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/NaturalNumbersPopulator.java @@ -0,0 +1,66 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + + +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public class NaturalNumbersPopulator implements Populator { + + public static NaturalNumbersPopulator get() { + return new NaturalNumbersPopulator(); + } + + @Override + public boolean isPopulated(NaturalNumbers repo) { + return repo.countAll() == 100L; + } + + @Override + public void populationLogic(NaturalNumbers repo) { + List dictonary = new ArrayList<>(); + + IntStream.range(1, 101) + .forEach(id -> { + NaturalNumber inst = new NaturalNumber(); + + boolean isOne = id == 1; + boolean isOdd = id % 2 == 1; + long sqrRoot = squareRoot(id); + boolean isPrime = isOdd ? isPrime(id, sqrRoot) : (id == 2); + NumberType numType = isOne ? NumberType.ONE : isPrime ? NumberType.PRIME : NumberType.COMPOSITE; + + inst.setId(id); + inst.setOdd(isOdd); + inst.setNumBitsRequired(bitsRequired(id)); + inst.setNumType(numType); + inst.setNumTypeOrdinal(numType.ordinal()); + inst.setFloorOfSquareRoot(sqrRoot); + + dictonary.add(inst); + }); + + repo.saveAll(dictonary); + } + + private static Short bitsRequired(int value) { + return (short) (Math.floor(Math.log(value) / Math.log(2)) + 1); + } + + private static long squareRoot(int value) { + return (long) Math.floor(Math.sqrt(value)); + } + + private static boolean isPrime(int value, long largestPossibleFactor) { + if(value == 1) + return false; + + for(int i = 2; i <= largestPossibleFactor; i++) { + if( value % i == 0 ) + return false; + } + return true; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/Populator.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/Populator.java new file mode 100644 index 0000000000..edc5ace1e1 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/Populator.java @@ -0,0 +1,60 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import java.util.logging.Logger; + +/** + * Aids in the population of repositories with entities for read-only testing. + * + * @param A repository + */ +public interface Populator { + // INTERFACE METHODS + + /** + * The logic that adds one or more entities to this repository. + * + * @param repo - this repository + */ + void populationLogic(T repo); + + /** + * A logical test that can verify if a repository is already populated or not. + * Typically, this is by verifying the count of entities saved in the repository. + * + * @param repo - this repository + * + * @return true if the repository is populated, false otherwise. + */ + boolean isPopulated(T repo); + + //DEFAULT METHODS + + public static final Logger log = Logger.getLogger(Populator.class.getCanonicalName()); + + /** + * Short circuiting method to to populate a repository that is not already populated. + * Uses the isPopulated() method to determine if a repository is populated or not. + * + * @param repo - this repository + */ + public default void populate(T repo) { + if(isPopulated(repo)) { + return; + } + + final String repoName = repo.getClass().getSimpleName(); + + log.info(repoName + " populating"); + populationLogic(repo); + + log.info(repoName + " waiting for eventual consistency"); + + log.info(repoName + " verifying"); + if(! isPopulated(repo)) { + throw new RuntimeException("Repository " + repoName + " was not populated"); + } + + log.info(repoName + " populated"); + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/PositiveIntegers.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/PositiveIntegers.java new file mode 100644 index 0000000000..65b1b3cd92 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/PositiveIntegers.java @@ -0,0 +1,67 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.jdbc.h2.jakarta_data.read.only.NaturalNumber.NumberType; +import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.BasicRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * This is a read only repository that shares the same data (and entity type) + * as the NaturalNumbers repository: the positive integers 1-100. + * This repository is pre-populated at test startup and verified prior to running tests. + */ +@JdbcRepository(dialect = Dialect.H2) +public interface PositiveIntegers extends BasicRepository { + long countByIdLessThan(long number); + + boolean existsByIdGreaterThan(Long number); + + CursoredPage findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc(long excludeSqrt, + long eclusiveMax, + PageRequest pagination, + Order order); + + List findByIsOddTrueAndIdLessThanEqualOrderByIdDesc(long max); + + List findByIsOddFalseAndIdBetween(long min, long max); + + Stream findByNumTypeInOrderByIdAsc(Set types, Limit limit); + + Stream findByNumTypeOrFloorOfSquareRoot(NumberType type, long floor); + + @Find + Page findMatching(long floorOfSquareRoot, Short numBitsRequired, NumberType numType, + PageRequest pagination, Sort... sorts); + + @Find + Optional findNumber(long id); + + @Find + List findOdd(boolean isOdd, NumberType numType, Limit limit, Order sorts); + + @Query("Select id Where isOdd = true and (id = :id or id < :exclusiveMax) Order by id Desc") + List oddAndEqualToOrBelow(long id, long exclusiveMax); + + // Per the spec: The 'and' operator has higher precedence than 'or'. + @Query("WHERE numBitsRequired = :bits OR numType = :type AND id < :xmax") + CursoredPage withBitCountOrOfTypeAndBelow(@Param("bits") short bitsRequired, + @Param("type") NumberType numberType, + @Param("xmax") long exclusiveMax, + Sort sort1, + Sort sort2, + PageRequest pageRequest); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/ReadOnlyRepository.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/ReadOnlyRepository.java new file mode 100644 index 0000000000..be3abb9235 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/ReadOnlyRepository.java @@ -0,0 +1,33 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Save; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +//FIXME - Are user defined repository interfaces like this allowed via the Specification? +// Currently failing in test environment +// java.lang.IllegalArgumentException: @Repository ee.jakarta.tck.data.framework.readonly.NaturalNumbers does not specify an entity class. +// To correct this, have the repository interface extend DataRepository or another built-in repository interface and supply the entity class as the first parameter. +@Deprecated //Not currently in use +public interface ReadOnlyRepository extends DataRepository{ + + // WRITE - default method + // Necessary for pre-population + @Save + List saveAll(List entities); + + // READ - default methods + Optional findById(K id); + + boolean existsById(K id); + + Stream findAll(); + + Stream findByIdIn(List ids); + + long count(); + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiChar.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiChar.java new file mode 100644 index 0000000000..ccdaf448b5 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiChar.java @@ -0,0 +1,31 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import jakarta.data.metamodel.Attribute; +import jakarta.data.metamodel.SortableAttribute; +import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; +import jakarta.data.metamodel.impl.AttributeRecord; +import jakarta.data.metamodel.impl.SortableAttributeRecord; +import jakarta.data.metamodel.impl.TextAttributeRecord; + +/** + * This static metamodel class tests what a user might explicitly provide, + * in which case the Jakarta Data provider will need to initialize the attributes. + */ +@StaticMetamodel(AsciiCharacter.class) +public class _AsciiChar { + public static final String ID = "id"; + public static final String HEXADECIMAL = "hexadecimal"; + public static final String NUMERICVALUE = "numericValue"; + + public static final SortableAttribute id = new SortableAttributeRecord<>("id"); + public static final TextAttribute hexadecimal = new TextAttributeRecord<>("hexadecimal"); + public static final Attribute isControl = new AttributeRecord<>("isControl"); // user decided it didn't care about sorting for this one + public static final SortableAttribute numericValue = new SortableAttributeRecord<>("numericValue"); + public static final TextAttribute thisCharacter = new TextAttributeRecord<>("thisCharacter"); + + // Avoids the checkstyle error, + // HideUtilityClassConstructor: Utility classes should not have a public or default constructor + private _AsciiChar() { + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiCharacter.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiCharacter.java new file mode 100644 index 0000000000..0234abf9a1 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/read/only/_AsciiCharacter.java @@ -0,0 +1,51 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.read.only; + +import jakarta.annotation.Generated; +import jakarta.data.Sort; +import jakarta.data.metamodel.SortableAttribute; +import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; + +/** + * This static metamodel class represents what an annotation processor-based approach + * might generate. + */ +@Generated("ee.jakarta.tck.data.mock.generator") +@StaticMetamodel(AsciiCharacter.class) +public class _AsciiCharacter { + public static final String ID = "id"; + public static final String HEXADECIMAL = "hexadecimal"; + public static final String NUMERICVALUE = "numericValue"; + + public static final SortableAttribute id = new NumericAttr("id"); + public static final TextAttribute hexadecimal = new TextAttr("hexadecimal"); + public static final SortableAttribute isControl = new BooleanAttr("isControl"); + public static final SortableAttribute numericValue = new NumericAttr("numericValue"); + public static final TextAttribute thisCharacter = new TextAttr("thisCharacter"); + + private static record BooleanAttr(String name, Sort asc, Sort desc) + implements SortableAttribute { + private BooleanAttr(String name) { + this(name, Sort.asc(name), Sort.desc(name)); + } + }; + + private static record NumericAttr(String name, Sort asc, Sort desc) + implements SortableAttribute { + private NumericAttr(String name) { + this(name, Sort.asc(name), Sort.desc(name)); + } + }; + + private static record TextAttr(String name, Sort asc, Sort ascIgnoreCase, + Sort desc, Sort descIgnoreCase) implements TextAttribute { + private TextAttr(String name) { + this(name, Sort.asc(name), Sort.ascIgnoreCase(name), Sort.desc(name), Sort.descIgnoreCase(name)); + } + }; + + // Avoids the checkstyle error, + // HideUtilityClassConstructor: Utility classes should not have a public or default constructor + private _AsciiCharacter() { + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/DatabaseType.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/DatabaseType.java new file mode 100644 index 0000000000..66e6989e6e --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/DatabaseType.java @@ -0,0 +1,30 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.utilities; + +import java.util.Arrays; + +/** + * This enum represents the configured DatabaseType based on the {@link TestProperty} databaseType + */ +public enum DatabaseType { + OTHER(Integer.MAX_VALUE), //No database type was configured + RELATIONAL(100), + GRAPH(50), + DOCUMENT(40), + TIME_SERIES(30), + COLUMN(20), + KEY_VALUE(10); + + private int flexibility; + + private DatabaseType(int flexibility) { + this.flexibility = flexibility; + } + + public static DatabaseType valueOfIgnoreCase(String value) { + return Arrays.stream(DatabaseType.values()).filter(type -> type.name().equalsIgnoreCase(value)).findAny().orElse(DatabaseType.OTHER); + } + + public boolean isKeywordSupportAtOrBelow(DatabaseType benchmark) { + return this.flexibility <= benchmark.flexibility; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestProperty.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestProperty.java new file mode 100644 index 0000000000..fd7120a848 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestProperty.java @@ -0,0 +1,157 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.utilities; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + *

This enum represents the different test properties used within this TCK. + * Each one is given a description and documentation will automatically be created in the TCK distribution.

+ * + *

When a test property is requested from the client, we expect these properties to be available from the system.

+ */ +public enum TestProperty { + //Java properties that should always be set by the JVM + javaHome (true, "java.home", + "Path to the java executable used to create the current JVM"), + javaSpecVer (true, "java.specification.version", + "Specification version of the java executable"), + javaTempDir (true, "java.io.tmpdir", + "The path to a temporary directory where a copy of the signature file will be created"), + javaVer (true, "java.version", + "Full version of the java executable"), + + //TCK specific properties + skipDeployment (false, "jakarta.tck.skip.deployment", + "If true, run in SE mode and do not use Arquillian deployment, if false run in EE mode and use Arquillian deployments. " + + "Default: false", "false"), + pollFrequency (false, "jakarta.tck.poll.frequency", + "Time in seconds between polls of the repository to verify read-only data was successfully written. " + + "Default: 1 second", "1"), + pollTimeout (false, "jakarta.tck.poll.timeout", + "Time in seconds when we will stop polling to verify read-only data was successfully written. " + + "Default: 60 seconds", "60"), + delay (false, "jakarta.tck.consistency.delay", + "Time in seconds after verifying read-only data was successfully written to respository " + + "for repository to have consistency. " + + "Default: none"), + databaseType (false, "jakarta.tck.database.type", + "The type of database being used. Valid values are " + Arrays.asList(DatabaseType.values()).toString() + " (case insensitive). " + + "The database type is used to make assertions based on the keywords supported by the underlying database. " + + "Default: OTHER", "OTHER"), + databaseName (false, "jakarta.tck.database.name", + "The name of database being used. The database name is used to make assertions based on the underlying database. " + + "Default: none"), + + //Signature testing properties + signatureClasspath (false, "signature.sigTestClasspath", "The path to the Jakarta Data API JAR used by your implementation. " + + "Required for standalone testing, but optional when testing on a Jakarta EE profile. " + + "Default: none"), + signatureImageDir (true, "jimage.dir", "The path to a directory that is readable and writable that " + + "the signature test will cache Java SE modules as classes. " + + "Default: none"); + + private boolean required; + private String key; + private String value; + private String description; + + // CONSTRUCTORS + private TestProperty(boolean required, String key, String description) { + this(required, key, description, null); + } + + private TestProperty(boolean required, String key, String description, String defaultValue) { + this.required = required; + this.key = key; + this.description = description; + this.value = getValue(defaultValue); + } + + // GETTERS + public boolean isRequired() { + return required; + } + + public String getKey() { + return key; + } + + public String getDescription() { + return description; + } + + // COMPARISONS + public boolean equals(String expectedValue) { + return getValue().equalsIgnoreCase(expectedValue); + } + + public boolean isSet() { + if(value == null) + return false; + if(value.isBlank() || value.isEmpty()) { + return false; + } + return true; + } + + // CONVERTERS + public long getLong() throws IllegalStateException, NumberFormatException { + return Long.parseLong(value); + } + + public int getInt() throws IllegalStateException, NumberFormatException { + return Integer.parseInt(value); + } + + public boolean getBoolean() { + return Boolean.parseBoolean(value); + } + + public DatabaseType getDatabaseType() { + return DatabaseType.valueOfIgnoreCase(value); + } + + /** + * Get the test property value. + * + * @return the property value + * @throws IllegalStateException if required and no property was found + */ + public String getValue() { + if(required && value == null) + throw new IllegalStateException("Could not obtain a value for system property: " + key); + + return value; + } + + private String getValue(String defaultVal) throws IllegalStateException { + final Logger log = Logger.getLogger(TestProperty.class.getCanonicalName()); + + String valueLocal = null; + log.fine("Searching for property: " + key); + + // Client: get property from system + if(valueLocal == null) { + valueLocal = System.getProperty(key); + log.fine("Value from system: " + valueLocal); + } + + //Container: get property from properties file + if(valueLocal == null) { + valueLocal = TestPropertyHandler.loadProperties().getProperty(key); + log.fine("Value from resource file: " + valueLocal); + } + + //Default: get default property + if(valueLocal == null) { + valueLocal = defaultVal; + log.fine("Value set to default: " + valueLocal); + } + + if (valueLocal == null) { + log.fine("Property was not set, value: " + null); + } + + return valueLocal; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestPropertyHandler.java b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestPropertyHandler.java new file mode 100644 index 0000000000..74e684511f --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/jakarta_data/utilities/TestPropertyHandler.java @@ -0,0 +1,47 @@ +package io.micronaut.data.jdbc.h2.jakarta_data.utilities; + +import java.io.InputStream; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * This uitlity class handles the caching and loading of test properties between the + * client and container when tests are run inside an Arquillian container. + */ +public class TestPropertyHandler { + + private static final Logger log = Logger.getLogger(TestPropertyHandler.class.getCanonicalName()); + + private static final String PROP_FILE = "tck.properties"; + private static Properties foundProperties; + + private TestPropertyHandler() { + //UTILITY CLASS + } + + /** + * Container: Load properties from the TestProperty cache file, and return a properties object. + * If any error occurs in finding the cache file, or loading the properties, + * then an empty properties object is returned. + * + * @return - the cached properties, or an empty properties object. + */ + static Properties loadProperties() { + if (foundProperties != null) { + return foundProperties; + } + + //Try to load property file + foundProperties = new Properties(); + InputStream propsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROP_FILE); + if (propsStream != null) { + try { + foundProperties.load(propsStream); + } catch (Exception e) { + log.info("Attempted to load properties from resource " + PROP_FILE + " but failed. Because: " + e.getLocalizedMessage()); + } + } + + return foundProperties; + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXERepositorySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXERepositorySpec.groovy index fa998609f9..a2d712dbe1 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXERepositorySpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXERepositorySpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.data.jdbc.oraclexe import groovy.transform.Memoized +import io.micronaut.data.model.Pageable import io.micronaut.data.tck.entities.Book import io.micronaut.data.tck.entities.Face import io.micronaut.data.tck.repositories.* diff --git a/data-model/src/main/java/io/micronaut/data/annotation/By.java b/data-model/src/main/java/io/micronaut/data/annotation/By.java new file mode 100644 index 0000000000..72224ef2df --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/By.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017-2024 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + *

Annotates a parameter of a repository method, specifying a mapping to + * a persistent field:

+ *
    + *
  • if a {@linkplain #value field name} is specified, the parameter maps + * to the persistent field with the specified name, or + *
  • if the special value {@value #ID} is specified, the parameter maps + * to the unique identifier field or property. + *
+ *

Arguments to the annotated parameter are compared to values of the + * mapped persistent field.

+ *

The field name may be a compound name like {@code address.city}.

+ * + *

For example, for a {@code Person} entity with attributes {@code ssn}, + * {@code firstName}, {@code lastName}, and {@code address} we might have:

+ * + *
+ * @Repository
+ * public interface People {
+ *
+ *     @Find
+ *     Person findById(@By(ID) String id); // maps to Person.ssn
+ *
+ *     @Find
+ *     List<Person> findNamed(@By("firstName") String first,
+ *                            @By("lastName") String last);
+ *
+ *     @Find
+ *     Person findByCity(@By("address.city") String city);
+ * }
+ * 
+ * + *

The {@code By} annotation is unnecessary when the method parameter name + * matches the entity attribute name and the application is compiled with the + * {@code -parameters} compiler option that makes parameter names available + * at runtime.

+ * + *

Thus, when this compiler option is enabled, the previous example may be + * written without the use of {@code By}:

+ * + *
+ * @Repository
+ * public interface People {
+ *
+ *     @Find
+ *     Person findById(String ssn);
+ *
+ *     @Find
+ *     List<Person> findNamed(String firstName,
+ *                            String lastname);
+ *
+ *     @Find
+ *     Person findByCity(String address_city);
+ * }
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface By { + + /** + * The name of the persistent field mapped by the annotated parameter, + * or {@value #ID} to indicate the unique identifier field or property + * of the entity. + * + * @return the persistent field name, or {@value #ID} to indicate the + * unique identifier field. + */ + String value(); + + /** + * The special value which indicates the unique identifier field or + * property. The annotation {@code By(ID)} maps a parameter to the + * identifier. + *

+ * Note that {@code id(this)} is the expression in JPQL for the + * unique identifier of an entity with an implicit identification + * variable. + */ + String ID = "id(this)"; +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/ConvertException.java b/data-model/src/main/java/io/micronaut/data/annotation/ConvertException.java new file mode 100644 index 0000000000..b47da2f15e --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/ConvertException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 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.annotation; + +import io.micronaut.aop.Introduction; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.data.exceptions.ExceptionConverter; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The exception converter definition for the data method. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Introduction +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Documented +@Experimental +public @interface ConvertException { + + /** + * The exception converter class that will be retried from the bean context. + * + * @return The exception converter class + */ + Class value(); +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/Delete.java b/data-model/src/main/java/io/micronaut/data/annotation/Delete.java new file mode 100644 index 0000000000..701685700c --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/Delete.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Lifecycle annotation for repository methods which perform delete operations; alternatively, annotates a repository + * method as a parameter-based automatic query method which deletes entities.

+ * + *

The {@code Delete} annotation indicates that the annotated repository method deletes the state of one or more + * entities from the database. It may be used in one of two ways: as a lifecycle annotation, to delete a given entity + * instance or instances, or as an automatic query annotation, to delete all entities satisfying parameter-based + * conditions. + *

+ * + *

A {@code Delete} method might accept an instance or instances of an entity class. In this case, the method must + * have exactly one parameter whose type is either: + *

+ *
    + *
  • the class of the entity to be deleted, or
  • + *
  • {@code List} or {@code E[]} where {@code E} is the class of the entities to be deleted.
  • + *
+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Delete { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/Find.java b/data-model/src/main/java/io/micronaut/data/annotation/Find.java new file mode 100644 index 0000000000..4bdbf10e87 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/Find.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Annotates a repository method returning entities as a parameter-based automatic query method.

+ * + *

The {@code Find} annotation indicates that the annotated repository method executes a query to retrieve entities + * based on its parameters and on the arguments assigned to its parameters. The method return type identifies the entity + * type returned by the query. Each parameter of the annotated method must either: + *

+ *
    + *
  • have exactly the same type and name (the parameter name in the Java source, or a name assigned by {@link By @By}) + * as a persistent field or property of the entity class, or
  • + *
+ *

The query is inferred from the method parameters which match persistent fields of the entity. + *

+ *

There is no specific naming convention for methods annotated with {@code @Find}; they may be named arbitrarily, + * and their names do not carry any semantic meaning defined by the Jakarta Data specification. + *

+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Find { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/Insert.java b/data-model/src/main/java/io/micronaut/data/annotation/Insert.java new file mode 100644 index 0000000000..7a155922f3 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/Insert.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Lifecycle annotation for repository methods which perform insert operations.

+ * + *

The {@code Insert} annotation indicates that the annotated repository method adds the state of one or more + * entities to the database. + *

+ *

An {@code Insert} method accepts an instance or instances of an entity class. The method must have exactly one + * parameter whose type is either: + *

+ *
    + *
  • the class of the entity to be inserted, or
  • + *
  • {@code List} or {@code E[]} where {@code E} is the class of the entities to be inserted.
  • + *
+ *

The annotated method must either be declared {@code void}, or have a return type that is the same as the type of + * its parameter. + *

+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Insert { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/OrderBy.java b/data-model/src/main/java/io/micronaut/data/annotation/OrderBy.java new file mode 100644 index 0000000000..7530594c5d --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/OrderBy.java @@ -0,0 +1,121 @@ +/* + * Copyright 2017-2025 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Annotates a repository method to request sorting of results.

+ * + *

When multiple {@code OrderBy} annotations are specified on a + * repository method, the precedence for sorting follows the order + * in which the {@code OrderBy} annotations are specified, + * and after that follows any sort criteria that are supplied + * dynamically by {@link io.micronaut.data.model.Sort} parameters or by any {@link io.micronaut.data.model.Sort.Order} parameter.

+ * + *

For example, the following sorts first by the + * {@code lastName} attribute in ascending order, + * and secondly, for entities with the same {@code lastName}, + * it then sorts by the {@code firstName} attribute, + * also in ascending order. For entities with the same + * {@code lastName} and {@code firstName}.

+ * + *
+ * @OrderBy("lastName")
+ * @OrderBy("firstName")
+ * @OrderBy("id")
+ * Person[] findByZipCode(int zipCode, Pageable pageable);
+ * 
+ * + *

The interpretation of ascending and descending order is determined + * by the database, but, in general: + *

    + *
  • ascending order for numeric values is the natural order with + * smaller numbers before larger numbers,
  • + *
  • ascending order for string values is lexicographic order with + * {@code A} before {@code Z}, and
  • + *
  • ascending order for boolean values places {@code false} before + * {@code true}.
  • + *
+ * + *

A repository method with an {@code @OrderBy} annotation must not + * have:

+ *
    + *
  • the Query by Method Name {@code OrderBy} keyword in its + * name, nor
  • + *
  • a {@link Query @Query} annotation specifying a JDQL or JPQL query + * with an {@code ORDER BY} clause.
  • + *
+ */ +@Repeatable(OrderBy.List.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OrderBy { + /** + *

Indicate whether to use descending order + * when sorting by this attribute.

+ * + *

The default value of {@code false} means ascending sort.

+ * + * @return whether to use descending (versus ascending) order. + */ + boolean descending() default false; + + /** + *

Indicates whether or not to request case insensitive ordering + * from a database with case sensitive collation. + * A database with case insensitive collation performs case insensitive + * ordering regardless of the requested {@code ignoreCase} value.

+ * + *

The default value is {@code false}.

+ * + * @return whether or not to request case insensitive sorting for the property. + */ + boolean ignoreCase() default false; + + /** + *

Entity attribute name to sort by.

+ * + *

For example,

+ * + *
+     * @OrderBy("age")
+     * Stream<Person> findByLastName(String lastName);
+     * 
+ * + * @return entity attribute name. + */ + String value(); + + /** + * Enables multiple {@code OrderBy} annotations on the method. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface List { + /** + * Returns a list of annotations with the first taking precedence, + * followed by the second, and so forth. + * + * @return list of annotations. + */ + OrderBy[] value(); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/Save.java b/data-model/src/main/java/io/micronaut/data/annotation/Save.java new file mode 100644 index 0000000000..d7a0535723 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/Save.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Lifecycle annotation for repository methods which conditionally perform insert or update operations.

+ * + *

The {@code Save} annotation indicates that the annotated repository method accepts one or more entities and, for + * each entity, either adds its state to the database, or updates state already held in the database. + *

+ *

A {@code Save} method accepts an instance or instances of an entity class. The method must have exactly one + * parameter whose type is either: + *

+ *
    + *
  • the class of the entity to be inserted or updated, or
  • + *
  • {@code List} or {@code E[]} where {@code E} is the class of the entities to be inserted or updated.
  • + *
+ *

The annotated method must either be declared {@code void}, or have a return type that is the same as the type of + * its parameter. + *

+ *

For example, consider an interface representing a garage:

+ *
+ * @Repository
+ * interface Garage {
+ *     @Save
+ *     Car park(Car car);
+ * }
+ * 
+ *

The operation performed by the annotated method depends on whether the database already holds an entity with the + * unique identifier of an entity passed as an argument: + *

+ *
    + *
  • If there is such an entity already held in the database, the annotated method must behave as if it were annotated + * {@link Update @Update}. + *
  • Otherwise, if there is no such entity in the database, the annotated method must behave as if it were annotated + * {@link Insert @Insert}. + *
+ *

Annotations such as {@code @Find}, {@code @Query}, {@code @Insert}, {@code @Update}, {@code @Delete}, and + * {@code @Save} are mutually-exclusive. A given method of a repository interface may have at most one {@code @Find} + * annotation, lifecycle annotation, or query annotation. + *

+ * + * @see Insert + * @see Update + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Save { +} diff --git a/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java b/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java index f03aaa90fe..f54311d902 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java @@ -48,6 +48,11 @@ */ String PAGEABLE = "pageable"; + /** + * The parameter that is used for limit. + */ + String LIMIT = "querylimit"; + /** * The parameter that is used for sorting. */ diff --git a/data-model/src/main/java/io/micronaut/data/annotation/Update.java b/data-model/src/main/java/io/micronaut/data/annotation/Update.java new file mode 100644 index 0000000000..da53bc3062 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/annotation/Update.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Lifecycle annotation for repository methods which perform update operations.

+ * + *

The {@code Update} annotation indicates that the annotated repository method updates the state of one or more + * entities already held in the database. + *

+ *

An {@code Update} method accepts an instance or instances of an entity class. The method must + * have exactly one parameter whose type is either: + *

+ *
    + *
  • the class of the entity to be updated, or
  • + *
  • {@code List} or {@code E[]} where {@code E} is the class of the entities to be updated.
  • + *
+ *

The annotated method must either be declared {@code void}, or have a return type that is the same as the type of + * its parameter. + *

+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Update { +} diff --git a/data-model/src/main/java/io/micronaut/data/exceptions/ExceptionConverter.java b/data-model/src/main/java/io/micronaut/data/exceptions/ExceptionConverter.java new file mode 100644 index 0000000000..2dd51bd983 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/exceptions/ExceptionConverter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2025 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.exceptions; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; + +/** + * The exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Experimental +public interface ExceptionConverter { + + /** + * Converts the exception. + * @param exception The exception to convert + * @return The converted exception + */ + @NonNull + Exception convert(@NonNull Exception exception); + +} diff --git a/data-model/src/main/java/io/micronaut/data/intercept/FindAllInterceptor.java b/data-model/src/main/java/io/micronaut/data/intercept/FindAllInterceptor.java index 8f2ce82c8a..243e8a547a 100644 --- a/data-model/src/main/java/io/micronaut/data/intercept/FindAllInterceptor.java +++ b/data-model/src/main/java/io/micronaut/data/intercept/FindAllInterceptor.java @@ -23,5 +23,5 @@ * @since 1.0 * @author graemerocher */ -public interface FindAllInterceptor extends IterableResultInterceptor { +public interface FindAllInterceptor extends DataInterceptor { } diff --git a/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQuery.java b/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQuery.java index 0538367f78..2bad619655 100644 --- a/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQuery.java +++ b/data-model/src/main/java/io/micronaut/data/intercept/annotation/DataMethodQuery.java @@ -86,6 +86,11 @@ */ String META_MEMBER_LIMIT = "limit"; + /** + * The parameter that holds the order value. + */ + String META_MEMBER_SORT = "sorts"; + /** * Does the query result in a DTO object. */ @@ -106,6 +111,16 @@ */ String META_MEMBER_OPERATION_TYPE = "opType"; + /** + * The member name that holds the type roles if parameters. + */ + String META_MEMBER_PARAMETERS_TYPE_ROLES = "parametersTypeRoles"; + + /** + * The member name that holds the type role of the return type. + */ + String META_MEMBER_RETURN_TYPE_ROLE = "returnTypeRole"; + /** * The computed result type. This represents the type that is to be read from the database. For example for a {@link java.util.List} * this would return the value of the generic type parameter {@code E}. Or for an entity result the return type itself. diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java index eecab0aa96..58d585b50d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java @@ -97,7 +97,7 @@ default boolean hasPrevious() { if (pageable.getMode() == Mode.CURSOR_PREVIOUS) { return getContent().size() == pageable.getSize(); } else { - return true; + return getPageNumber() != 0; } } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java index 423eca438e..a11db729c0 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.data.model.Pageable.Cursor; import io.micronaut.serde.annotation.Serdeable; @@ -34,7 +35,8 @@ * @param The generic type */ @Serdeable -class DefaultCursoredPage extends DefaultPage implements CursoredPage { +@Internal +final class DefaultCursoredPage extends DefaultPage implements CursoredPage { private final List cursors; diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java index 9d33f2270e..cf0e3e5038 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java @@ -34,7 +34,7 @@ @Serdeable class DefaultPage extends DefaultSlice implements Page { - private final Long totalSize; + private final long totalSize; /** * Default constructor. @@ -58,16 +58,12 @@ class DefaultPage extends DefaultSlice implements Page { @Override public boolean hasTotalSize() { - return totalSize != null && totalSize != -1L; + return totalSize != -1L; } @Override @ReflectiveAccess public long getTotalSize() { - if (totalSize == null) { - throw new IllegalStateException("Page does not contain total count. " + - "It is likely that the Pageable needs to be modified to request this information."); - } return totalSize; } @@ -79,7 +75,7 @@ public boolean equals(Object o) { if (!(o instanceof DefaultPage that)) { return false; } - return totalSize == that.totalSize && super.equals(o); + return Objects.equals(totalSize, that.totalSize) && super.equals(o); } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/Limit.java b/data-model/src/main/java/io/micronaut/data/model/Limit.java new file mode 100644 index 0000000000..1825aab9fa --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/Limit.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2025 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; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; + +/** + * The query limit. + * + * @since 4.12 + */ +@Experimental +public interface Limit { + + Limit UNLIMITED = Limit.of(-1, 0); + + /** + * @return The max results of the query or -1 if none + */ + default int maxResults() { + return -1; + } + + /** + * @return The offset of the query or 0 if none + */ + default long offset() { + return 0; + } + + /** + * @return Is the limit present + */ + default boolean isLimited() { + return maxResults() != -1 || offset() > 0; + } + + /** + * Creates a new limit. + * + * @param maxResults The max results + * @param offset The offset + * @return the limit + */ + static Limit of(int maxResults, long offset) { + return new DefaultLimit(maxResults, offset); + } + + /** + * The default implementation. + * + * @param maxResults The max results + * @param offset The offset + */ + @Internal + record DefaultLimit(int maxResults, long offset) implements Limit { + } + +} diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 844b5d2823..f4be6f7b42 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -70,14 +70,17 @@ public interface Page extends Slice { /** * Get the total count of pages that can be given by this query. - * The method may produce a {@link IllegalStateException} if the {@link Pageable} request - * did not ask for total size. + * The method will return -1 if the total size is not available. * * @return The total page of pages */ default int getTotalPages() { int size = getSize(); - return size == 0 ? 1 : (int) Math.ceil((double) getTotalSize() / (double) size); + long totalSize = getTotalSize(); + if (totalSize == -1) { + return -1; + } + return size == 0 ? 1 : (int) Math.ceil((double) totalSize / (double) size); } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 2a4e9f4678..f186e6be30 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -112,6 +112,16 @@ default Sort getSort() { return Sort.unsorted(); } + /** + * @return The limit + * @see 4.12 + */ + @NonNull + @JsonIgnore + default Limit getLimit() { + return Limit.of(getSize(), getOffset()); + } + /** * @return The next pageable. */ @@ -132,19 +142,14 @@ private Pageable getPageable(int newNumber) { int size = getSize(); if (size < 0) { // unpaged - return Pageable.from(0, size, getSort()); + return Pageable.from(0, size, getSort(), requestTotal()); } Pageable newPageable; // handle overflow if (newNumber < 0) { - newPageable = Pageable.from(0, size, getSort()); - } else { - newPageable = Pageable.from(newNumber, size, getSort()); - } - if (!requestTotal()) { - newPageable = newPageable.withoutTotal(); + return Pageable.from(0, size, getSort(), requestTotal()); } - return newPageable; + return Pageable.from(newNumber, size, getSort(), requestTotal()); } /** @@ -159,7 +164,7 @@ default boolean isUnpaged() { @Override default Pageable order(@NonNull String propertyName) { Sort newSort = getSort().order(propertyName); - return Pageable.from(getNumber(), getSize(), newSort); + return Pageable.from(getNumber(), getSize(), newSort, requestTotal()); } @Override @@ -172,24 +177,20 @@ default boolean isSorted() { @Override default Pageable order(@NonNull Order order) { Sort newSort = getSort().order(order); - return Pageable.from(getNumber(), getSize(), newSort); + return Pageable.from(getNumber(), getSize(), newSort, requestTotal()); } @NonNull @Override default Pageable order(@NonNull String propertyName, @NonNull Order.Direction direction) { Sort newSort = getSort().order(propertyName, direction); - return Pageable.from(getNumber(), getSize(), newSort); + return Pageable.from(getNumber(), getSize(), newSort, requestTotal()); } @NonNull @Override default Pageable orders(@NonNull List orders) { - Sort newSort = getSort(); - for (Order order : orders) { - newSort = newSort.order(order); - } - return Pageable.from(getNumber(), getSize(), newSort); + return Pageable.from(getNumber(), getSize(), getSort().orders(orders), requestTotal()); } /** @@ -201,7 +202,7 @@ default Pageable orders(@NonNull List orders) { @NonNull default Pageable withoutSort() { if (isSorted()) { - return Pageable.from(getNumber(), getSize()); + return Pageable.from(getNumber(), getSize(), null, requestTotal()); } return this; } @@ -225,7 +226,7 @@ default Pageable withoutPaging() { * @return A pageable instance with a new sort */ default Pageable withSort(@NonNull Sort sort) { - return Pageable.from(getNumber(), getSize(), sort); + return Pageable.from(getNumber(), getSize(), sort, requestTotal()); } @NonNull @@ -298,6 +299,24 @@ default Pageable withoutTotal() { return new DefaultPageable(page, size, sort, true); } + /** + * Creates a new {@link Pageable} with the given offset. + * + * @param page The page + * @param size the size + * @param sort the sort + * @param requestTotal The request total + * @return The pageable + * @since 4.12 + */ + static @NonNull Pageable from( + int page, + int size, + @Nullable Sort sort, + boolean requestTotal) { + return new DefaultPageable(page, size, sort, requestTotal); + } + /** * Creates a new {@link Pageable} with the given parameters. * The method is used for deserialization and most likely should not be used as an API. diff --git a/data-model/src/main/java/io/micronaut/data/model/PersistentProperty.java b/data-model/src/main/java/io/micronaut/data/model/PersistentProperty.java index c526cb0991..c26e520884 100644 --- a/data-model/src/main/java/io/micronaut/data/model/PersistentProperty.java +++ b/data-model/src/main/java/io/micronaut/data/model/PersistentProperty.java @@ -15,8 +15,8 @@ */ package io.micronaut.data.model; -import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.naming.NameUtils; import io.micronaut.data.annotation.AutoPopulated; @@ -26,6 +26,8 @@ import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import java.util.List; + /** * Models a persistent property. That is a property that is saved and retrieved from the database. @@ -37,13 +39,16 @@ public interface PersistentProperty extends PersistentElement { /** * The name of the property. + * * @return The property name */ @Override - @NonNull String getName(); + @NonNull + String getName(); /** * The name with the first letter in upper case as per Java bean conventions. + * * @return The capitilized name * @deprecated The method with a type replaced with {@link #getCapitalizedName()}. */ @@ -54,6 +59,7 @@ public interface PersistentProperty extends PersistentElement { /** * The name with the first letter in upper case as per Java bean conventions. + * * @return The capitalized name * @since 4.2.0 */ @@ -63,16 +69,19 @@ public interface PersistentProperty extends PersistentElement { /** * The type of the property. + * * @return The property type */ - @NonNull String getTypeName(); + @NonNull + String getTypeName(); /** * Obtains the owner of this persistent property. * * @return The owner */ - @NonNull PersistentEntity getOwner(); + @NonNull + PersistentEntity getOwner(); /** * Whether the property can be set to null. @@ -87,18 +96,19 @@ default boolean isOptional() { * Whether a property is required to be specified. This returns * false if the property is both not nullable and not generated. * + * @return True if the property is required * @see #isOptional() * @see #isGenerated() - * @return True if the property is required */ default boolean isRequired() { - return !isOptional() && - !isGenerated() && - !getAnnotationMetadata().hasStereotype(AutoPopulated.class); + return !isOptional() && + !isGenerated() && + !getAnnotationMetadata().hasStereotype(AutoPopulated.class); } /** * Whether the property is read-only, for example for generated values. + * * @return True if it is read-only */ default boolean isReadOnly() { @@ -130,6 +140,7 @@ default boolean isAutoPopulated() { /** * Is the property assignable to the given type name. + * * @param type The type name * @return True if it is */ @@ -137,6 +148,7 @@ default boolean isAutoPopulated() { /** * Is the property assignable to the given type. + * * @param type The type * @return True it is */ @@ -153,18 +165,18 @@ default DataType getDataType() { } else { AnnotationMetadata annotationMetadata = getAnnotationMetadata(); return annotationMetadata.enumValue(MappedProperty.class, "type", DataType.class) - .orElseGet(() -> { - DataType dt = annotationMetadata.enumValue(TypeDef.class, "type", DataType.class).orElse(null); - if (dt != null) { - return dt; + .orElseGet(() -> { + DataType dt = annotationMetadata.enumValue(TypeDef.class, "type", DataType.class).orElse(null); + if (dt != null) { + return dt; + } else { + if (isEnum()) { + return DataType.STRING; } else { - if (isEnum()) { - return DataType.STRING; - } else { - return DataType.OBJECT; - } + return DataType.OBJECT; } - }); + } + }); } } @@ -184,6 +196,13 @@ default boolean isEnum() { return false; } + /** + * @return Returns the enum constants if the property type is an enum. + */ + default List getEnumConstants() { + return List.of(); + } + /** * @return Returns possible property convertor. */ @@ -194,14 +213,15 @@ default AttributeConverter getConverter() { /** * Return whether the metadata indicates the instance is nullable. + * * @param metadata The metadata * @return True if it is nullable */ static boolean isNullableMetadata(@NonNull AnnotationMetadata metadata) { return metadata - .getDeclaredAnnotationNames() - .stream() - .anyMatch(n -> NameUtils.getSimpleName(n).equalsIgnoreCase("nullable")); + .getDeclaredAnnotationNames() + .stream() + .anyMatch(n -> NameUtils.getSimpleName(n).equalsIgnoreCase("nullable")); } /** @@ -224,4 +244,17 @@ default String getAlias() { default boolean isEmbedded() { return false; } + + /** + * The enum constant. + * + * @since 4.12 + */ + interface EnumConstant { + + String name(); + + int ordinal(); + + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/Sort.java b/data-model/src/main/java/io/micronaut/data/model/Sort.java index 89a81e95c2..771b84ecd9 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Sort.java +++ b/data-model/src/main/java/io/micronaut/data/model/Sort.java @@ -80,10 +80,11 @@ public interface Sort { */ @NonNull default Sort orders(@NonNull List orders) { + Sort theSort = this; for (Order order : orders) { - order(order); + theSort = theSort.order(order); } - return this; + return theSort; } /** @@ -248,6 +249,17 @@ public static Order asc(String property, boolean ignoreCase) { return new Order(property, Direction.ASC, ignoreCase); } + /** + * Creates a new order for the given property in ascending order. + * + * @param property The property + * @param ignoreCase Whether to ignore case + * @return The order instance + */ + public static Order of(String property, boolean ignoreCase) { + return new Order(property, Direction.ASC, ignoreCase); + } + /** * @return Is the order ascending */ diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java index 830884c0f2..f8a276d6d8 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java @@ -16,6 +16,7 @@ package io.micronaut.data.model.jpa.criteria; import io.micronaut.core.annotation.Experimental; +import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.jpa.criteria.impl.expression.ClassExpressionType; import jakarta.persistence.criteria.CommonAbstractCriteria; @@ -42,4 +43,6 @@ default PersistentEntitySubquery subquery(Class type) { return subquery(new ClassExpressionType<>(type)); } + PersistentEntity getPersistentEntity(); + } 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 09b67e60a6..0444e94eef 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 @@ -19,6 +19,7 @@ import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Order; import jakarta.persistence.criteria.Predicate; /** @@ -45,6 +46,16 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { @Override PersistentEntityCriteriaDelete createCriteriaDelete(Class targetEntity); + /** + * Create an ordering. + * + * @param x expression used to define the ordering + * @param ascending If ascending should be use + * @param ignoreCase If ignore case should be used + * @return ascending ordering corresponding to the expression + */ + Order sort(Expression x, boolean ascending, boolean ignoreCase); + /** * OR restriction predicate. * 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 70b0a32c72..fab60efe0d 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 @@ -73,7 +73,6 @@ import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.notSupportedOperation; import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireBoolExpression; import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireBoolExpressions; -import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireProperty; /** * Abstract {@link jakarta.persistence.criteria.CriteriaBuilder} implementation. @@ -144,13 +143,18 @@ public CompoundSelection array(@NonNull Selection... selections) { @Override @NonNull public Order asc(@NonNull Expression x) { - return new PersistentPropertyOrder<>(requireProperty(x), true); + return sort(x, true, false); } @Override @NonNull public Order desc(@NonNull Expression x) { - return new PersistentPropertyOrder<>(requireProperty(x), false); + return sort(x, false, false); + } + + @Override + public Order sort(Expression x, boolean ascending, boolean ignoreCase) { + return new DefaultOrder<>(x, ascending, ignoreCase); } @Override @@ -552,125 +556,70 @@ public Expression sum(@NonNull Expression x, return new BinaryExpression<>(x, y, BinaryExpressionType.SUM, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression sum(@NonNull Expression x, @NonNull N y) { return new BinaryExpression<>(x, literal(y), BinaryExpressionType.SUM, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression sum(@NonNull N x, @NonNull Expression y) { return new BinaryExpression<>(literal(x), y, BinaryExpressionType.SUM, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression prod(@NonNull Expression x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, y, BinaryExpressionType.PROD, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression prod(@NonNull Expression x, @NonNull N y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, literal(y), BinaryExpressionType.PROD, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression prod(@NonNull N x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(literal(x), y, BinaryExpressionType.PROD, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression diff(@NonNull Expression x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, y, BinaryExpressionType.DIFF, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression diff(@NonNull Expression x, @NonNull N y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, literal(y), BinaryExpressionType.DIFF, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression diff(@NonNull N x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(literal(y), y, BinaryExpressionType.DIFF, (Class) Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression quot(@NonNull Expression x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, y, BinaryExpressionType.QUOT, Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression quot(@NonNull Expression x, @NonNull Number y) { - throw notSupportedOperation(); + return new BinaryExpression<>(x, literal(y), BinaryExpressionType.QUOT, Number.class); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression quot(@NonNull Number x, @NonNull Expression y) { - throw notSupportedOperation(); + return new BinaryExpression<>(literal(x), y, BinaryExpressionType.QUOT, Number.class); } /** @@ -1158,15 +1107,10 @@ public Expression upper(@NonNull Expression x) { return new UnaryExpression<>(x, UnaryExpressionType.UPPER); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Expression length(@NonNull Expression x) { - throw notSupportedOperation(); + return new UnaryExpression<>(x, UnaryExpressionType.LENGTH); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaDelete.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaDelete.java index 6a65293871..e06bb5d0e7 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaDelete.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaDelete.java @@ -56,6 +56,11 @@ public abstract class AbstractPersistentEntityCriteriaDelete implements Persi protected PersistentEntityRoot entityRoot; protected Selection returning; + @Override + public PersistentEntity getPersistentEntity() { + return entityRoot.getPersistentEntity(); + } + @Override public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder2 queryBuilder) { return queryBuilder.buildDelete( diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaQuery.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaQuery.java index 8fad84dd20..a2773c1b64 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaQuery.java @@ -92,7 +92,7 @@ public PersistentEntityCriteriaQuery orderBy(List orders) { @Override public List getOrderList() { - throw notSupportedOperation(); + return orders; } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaUpdate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaUpdate.java index 20ded5b4b1..53a11b7739 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaUpdate.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityCriteriaUpdate.java @@ -65,6 +65,11 @@ public abstract class AbstractPersistentEntityCriteriaUpdate implements Persi protected Map updateValues = new LinkedHashMap<>(); protected Selection returning; + @Override + public PersistentEntity getPersistentEntity() { + return entityRoot.getPersistentEntity(); + } + @Override public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder2 queryBuilder) { return queryBuilder.buildUpdate( diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityQuery.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityQuery.java index 484e5e2400..8f55243618 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityQuery.java @@ -73,7 +73,7 @@ public abstract class AbstractPersistentEntityQuery> implements AbstractQuery, QueryResultPersistentEntityCriteriaQuery, PersistentEntityQuery { - protected Map parametersInRole = new LinkedHashMap<>(); + protected Map parametersInRole = new LinkedHashMap<>(); protected final CriteriaBuilder criteriaBuilder; protected final ExpressionType resultType; protected Predicate predicate; @@ -90,7 +90,12 @@ protected AbstractPersistentEntityQuery(ExpressionType resultType, CriteriaBu this.criteriaBuilder = criteriaBuilder; } - public final Map getParametersInRole() { + @Override + public PersistentEntity getPersistentEntity() { + return entityRoot.getPersistentEntity(); + } + + public final Map getParametersInRole() { return parametersInRole; } @@ -386,7 +391,7 @@ private static final class SelectQueryDefinitionImpl extends BaseQueryDefinition private final List order; private final int limit; private final int offset; - private final Map parametersInRole; + private final Map parametersInRole; public SelectQueryDefinitionImpl(Root root, PersistentEntity persistentEntity, @@ -398,7 +403,7 @@ public SelectQueryDefinitionImpl(Root root, List order, int limit, int offset, - Map parametersInRole) { + Map parametersInRole) { super(persistentEntity, predicate, joinPaths); this.root = root; this.selection = selection; @@ -411,7 +416,7 @@ public SelectQueryDefinitionImpl(Root root, } @Override - public Map parametersInRole() { + public Map parametersInRole() { return parametersInRole; } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/BoundPathParameterExpression.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/BoundPathParameterExpression.java new file mode 100644 index 0000000000..8edaca6741 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/BoundPathParameterExpression.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2024 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.core.annotation.Internal; +import io.micronaut.data.model.PersistentPropertyPath; +import io.micronaut.data.model.query.builder.QueryParameterBinding; + +/** + * The parameter value with a bound path. + * + * @param The parameter type + * @author Denis Stepanov + * @since 4.12.0 + */ +@Internal +public final class BoundPathParameterExpression extends IParameterExpression { + + private final IParameterExpression originalParameterExpression; + private final PersistentPropertyPath propertyPath; + + public BoundPathParameterExpression(IParameterExpression originalParameterExpression, PersistentPropertyPath propertyPath) { + super(originalParameterExpression.getExpressionType(), originalParameterExpression.getName()); + this.originalParameterExpression = originalParameterExpression; + this.propertyPath = propertyPath; + } + + @Override + public QueryParameterBinding bind(BindingContext bindingContext) { + return originalParameterExpression.bind(bindingContext.parameterBindingPath(propertyPath)); + } +} 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 ca99c06e8d..b3c8f1f248 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 @@ -88,7 +88,7 @@ public static PersistentEntitySubquery requirePersistentEntitySubquery(Su public static IExpression requireNumericExpression(Expression exp) { IExpression expression = requireIExpression(exp); - if (expression.getExpressionType().isNumeric()) { + if (expression instanceof ParameterExpression || expression.getExpressionType().isNumeric()) { return expression; } throw new IllegalStateException("Expected a numeric expression! Got: " + expression.getExpressionType().getName()); @@ -96,7 +96,7 @@ public static IExpression requireNumericExpression(Expression exp) { public static IExpression requireStringExpression(Expression exp) { IExpression expression = requireIExpression(exp); - if (expression.getExpressionType().isTextual()) { + if (expression instanceof ParameterExpression || expression.getExpressionType().isTextual()) { return expression; } throw new IllegalStateException("Expected a string expression! Got: " + expression.getExpressionType().getName()); @@ -104,7 +104,7 @@ public static IExpression requireStringExpression(Expression exp) { public static Expression requireComparableExpression(Expression exp) { IExpression expression = requireIExpression(exp); - if (expression.getExpressionType().isComparable()) { + if (expression instanceof ParameterExpression || expression.getExpressionType().isComparable()) { return expression; } throw new IllegalStateException("Expected a comparable expression! Got: " + expression.getExpressionType().getName()); @@ -112,7 +112,7 @@ public static Expression requireComparableExpression(Expression exp) { public static IExpression requireBoolExpression(Expression exp) { IExpression expression = requireIExpression(exp); - if (expression.getExpressionType().isBoolean()) { + if (expression instanceof ParameterExpression || expression.getExpressionType().isBoolean()) { return expression; } throw new IllegalStateException("Expected a boolean expression! Got: " + expression.getExpressionType().getName()); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PersistentPropertyOrder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultOrder.java similarity index 71% rename from data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PersistentPropertyOrder.java rename to data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultOrder.java index 581a6979cd..02842f3cec 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PersistentPropertyOrder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultOrder.java @@ -16,7 +16,6 @@ package io.micronaut.data.model.jpa.criteria.impl; import io.micronaut.core.annotation.Internal; -import io.micronaut.data.model.jpa.criteria.PersistentPropertyPath; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Order; @@ -28,19 +27,25 @@ * @since 3.2 */ @Internal -public final class PersistentPropertyOrder implements Order { +public final class DefaultOrder implements Order { - private final PersistentPropertyPath persistentPropertyPath; + private final Expression expression; private final boolean ascending; + private final boolean ignoreCase; - public PersistentPropertyOrder(PersistentPropertyPath persistentPropertyPath, boolean ascending) { - this.persistentPropertyPath = persistentPropertyPath; + public DefaultOrder(Expression expression, boolean ascending, boolean ignoreCase) { + this.expression = expression; this.ascending = ascending; + this.ignoreCase = ignoreCase; } @Override public Order reverse() { - return new PersistentPropertyOrder<>(persistentPropertyPath, !ascending); + return new DefaultOrder<>(expression, !ascending, ignoreCase); + } + + public boolean isIgnoreCase() { + return ignoreCase; } @Override @@ -50,6 +55,6 @@ public boolean isAscending() { @Override public Expression getExpression() { - return persistentPropertyPath; + return expression; } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/LegacyQueryModelQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/LegacyQueryModelQueryBuilder.java index 5c79acc186..8124b3679e 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/LegacyQueryModelQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/LegacyQueryModelQueryBuilder.java @@ -23,7 +23,6 @@ import io.micronaut.data.model.jpa.criteria.IPredicate; import io.micronaut.data.model.jpa.criteria.ISelection; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; -import io.micronaut.data.model.jpa.criteria.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.impl.query.QueryModelPredicateVisitor; import io.micronaut.data.model.jpa.criteria.impl.query.QueryModelSelectionVisitor; import io.micronaut.data.model.jpa.criteria.impl.util.Joiner; @@ -31,14 +30,10 @@ import io.micronaut.data.model.query.builder.QueryBuilder; import io.micronaut.data.model.query.builder.QueryBuilder2; import io.micronaut.data.model.query.builder.QueryResult; -import jakarta.persistence.criteria.Order; -import java.util.List; import java.util.Map; import java.util.Optional; -import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireProperty; - /** * The Legacy query builder wrapper. * @@ -81,18 +76,9 @@ public QueryResult buildSelect(AnnotationMetadata annotationMetadata, SelectQuer entityRoot.visitSelection(new QueryModelSelectionVisitor(qm, definition.isDistinct())); entityRoot.visitSelection(joiner); } - List orders = definition.order(); - if (orders != null && !orders.isEmpty()) { - List sortOrders = orders.stream().map(o -> { - PersistentPropertyPath propertyPath = requireProperty(o.getExpression()); - joiner.joinIfNeeded(propertyPath); - String name = propertyPath.getPathAsString(); - if (o.isAscending()) { - return Sort.Order.asc(name); - } - return Sort.Order.desc(name); - }).toList(); - qm.sort(Sort.of(sortOrders)); + Sort sort = definition.asSort(); + if (sort.isSorted()) { + qm.sort(sort); } for (Map.Entry e : joiner.getJoins().entrySet()) { qm.join(e.getKey(), Optional.ofNullable(e.getValue().getType()).orElse(Join.Type.DEFAULT), e.getValue().getAlias()); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/QueryResultPersistentEntityCriteriaQuery.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/QueryResultPersistentEntityCriteriaQuery.java index 9459fbaabd..34cf4e10a0 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/QueryResultPersistentEntityCriteriaQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/QueryResultPersistentEntityCriteriaQuery.java @@ -42,7 +42,7 @@ default QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuild QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder2 queryBuilder); @NonNull - private static QueryBuilder2 asQueryBuilder2(QueryBuilder queryBuilder) { + static QueryBuilder2 asQueryBuilder2(QueryBuilder queryBuilder) { Class queryBuilderClass = queryBuilder.getClass(); if (queryBuilderClass.getSimpleName().equals("CosmosSqlQueryBuilder")) { // Use new implementation diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/BinaryExpressionType.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/BinaryExpressionType.java index e96fa4ea6d..179f15fb1d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/BinaryExpressionType.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/BinaryExpressionType.java @@ -25,5 +25,9 @@ */ @Internal public enum BinaryExpressionType { - CONCAT, SUM + CONCAT, // "foo" + "bar" + SUM, // + + PROD, // * + QUOT, // / + DIFF // - } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/UnaryExpressionType.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/UnaryExpressionType.java index d6f50917d7..acbffe6b56 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/UnaryExpressionType.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/expression/UnaryExpressionType.java @@ -27,13 +27,13 @@ */ @Internal public enum UnaryExpressionType { - AVG, SUM, MAX, MIN, COUNT, COUNT_DISTINCT, UPPER, LOWER; + AVG, SUM, MAX, MIN, COUNT, COUNT_DISTINCT, UPPER, LOWER, LENGTH; void validate(Expression expression) { switch (this) { case AVG, SUM -> CriteriaUtils.requireNumericExpression(expression); case MAX, MIN -> CriteriaUtils.requireComparableExpression(expression); - case UPPER, LOWER -> CriteriaUtils.requireStringExpression(expression); + case UPPER, LOWER, LENGTH -> CriteriaUtils.requireStringExpression(expression); case COUNT, COUNT_DISTINCT -> CriteriaUtils.requirePropertyOrRoot(expression); default -> throw new IllegalStateException("Unexpected value: " + this); } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/BindingContextImpl.java b/data-model/src/main/java/io/micronaut/data/model/query/BindingContextImpl.java index 0fc320b37f..3f597c1e70 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/BindingContextImpl.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/BindingContextImpl.java @@ -29,6 +29,7 @@ final class BindingContextImpl implements BindingParameter.BindingContext { private String name; private PersistentPropertyPath incomingMethodParameterProperty; private PersistentPropertyPath outgoingQueryParameterProperty; + private PersistentPropertyPath parameterBindingPath; private boolean expandable; @Override @@ -55,6 +56,12 @@ public BindingParameter.BindingContext outgoingQueryParameterProperty(Persistent return this; } + @Override + public BindingParameter.BindingContext parameterBindingPath(PersistentPropertyPath propertyPath) { + this.parameterBindingPath = propertyPath; + return this; + } + @Override public BindingParameter.BindingContext expandable() { this.expandable = true; @@ -81,6 +88,11 @@ public PersistentPropertyPath getOutgoingQueryParameterProperty() { return outgoingQueryParameterProperty; } + @Override + public PersistentPropertyPath getParameterBindingPath() { + return parameterBindingPath; + } + @Override public boolean isExpandable() { return expandable; diff --git a/data-model/src/main/java/io/micronaut/data/model/query/BindingParameter.java b/data-model/src/main/java/io/micronaut/data/model/query/BindingParameter.java index 1af7c07285..a3ee6f7f75 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/BindingParameter.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/BindingParameter.java @@ -92,6 +92,16 @@ static BindingContext create() { @NonNull BindingContext outgoingQueryParameterProperty(@Nullable PersistentPropertyPath propertyPath); + /** + * The binding path of the parameter. + * Parameter value needs to be resolved before it can be set. + * + * @param propertyPath The property path + * @return this context + */ + @NonNull + BindingContext parameterBindingPath(@Nullable PersistentPropertyPath propertyPath); + /** * Mark the property as expandable. * @@ -125,6 +135,12 @@ static BindingContext create() { @Nullable PersistentPropertyPath getOutgoingQueryParameterProperty(); + /** + * @return The parameter binding path + */ + @Nullable + PersistentPropertyPath getParameterBindingPath(); + /** * @return Is expandable */ diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryBuilder2.java index 9eef6bb055..5b717b82bd 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryBuilder2.java @@ -21,6 +21,9 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.PersistentEntity; +import io.micronaut.data.model.Sort; +import io.micronaut.data.model.jpa.criteria.PersistentPropertyPath; +import io.micronaut.data.model.jpa.criteria.impl.DefaultOrder; import io.micronaut.data.model.query.JoinPath; import jakarta.persistence.criteria.Order; import jakarta.persistence.criteria.Predicate; @@ -32,6 +35,8 @@ import java.util.Map; import java.util.Optional; +import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireProperty; + /** * An interface capable of encoding a query into a string and a set of named parameters. * @@ -114,6 +119,28 @@ interface SelectQueryDefinition extends BaseQueryDefinition { @NonNull List order(); + /** + * @return Return the order as sort + */ + default Sort asSort() { + List orders = order(); + if (orders == null || orders.isEmpty()) { + return Sort.unsorted(); + } + List sortOrders = orders.stream().map(o -> { + PersistentPropertyPath propertyPath = requireProperty(o.getExpression()); + String name = propertyPath.getPathAsString(); + if (o instanceof DefaultOrder order) { + return new Sort.Order(name, order.isAscending() ? Sort.Order.Direction.ASC : Sort.Order.Direction.DESC, order.isIgnoreCase()); + } + if (o.isAscending()) { + return Sort.Order.asc(name); + } + return Sort.Order.desc(name); + }).toList(); + return Sort.of(sortOrders); + } + /** * @return Is the query marked for update */ @@ -131,7 +158,7 @@ default boolean isDistinct() { /** * @return The parameters in role */ - default Map parametersInRole() { + default Map parametersInRole() { return Map.of(); } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryResult.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryResult.java index c2ed67c020..bb92ffd31d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryResult.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/QueryResult.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.JoinPath; import java.util.Collection; @@ -108,6 +109,11 @@ default long getOffset() { return 0; } + @NonNull + default Sort getSort() { + return Sort.UNSORTED; + } + /** * Gets the join paths. * @@ -356,6 +362,72 @@ public Collection getJoinPaths() { }; } + /** + * Creates a new encoded query. + * + * @param query The query + * @param queryParts The queryParts + * @param parameterBindings The parameters binding + * @param max The query limit + * @param offset The query offset + * @param sort The sort + * @param joinPaths The join paths + * @return The query + */ + @NonNull + static QueryResult of( + @NonNull String query, + @NonNull List queryParts, + @NonNull List parameterBindings, + int max, + long offset, + @NonNull + Sort sort, + @Nullable + Collection joinPaths) { + ArgumentUtils.requireNonNull("query", query); + ArgumentUtils.requireNonNull("parameterBindings", parameterBindings); + + return new QueryResult() { + + @Override + public int getMax() { + return max; + } + + @Override + public long getOffset() { + return offset; + } + + @Override + public Sort getSort() { + return sort; + } + + @NonNull + @Override + public String getQuery() { + return query; + } + + @Override + public List getQueryParts() { + return queryParts; + } + + @Override + public List getParameterBindings() { + return parameterBindings; + } + + @Override + public Collection getJoinPaths() { + return joinPaths; + } + }; + } + /** * Creates a new encoded query. * diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/jpa/JpaQueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/jpa/JpaQueryBuilder2.java index 5fe7d8632c..8c8051432f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/jpa/JpaQueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/jpa/JpaQueryBuilder2.java @@ -29,11 +29,13 @@ import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; +import io.micronaut.data.model.jpa.criteria.impl.DefaultOrder; import io.micronaut.data.model.naming.NamingStrategy; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.builder.QueryResult; import io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2; import io.micronaut.data.model.query.builder.sql.Dialect; +import jakarta.persistence.criteria.Order; import java.util.HashSet; import java.util.List; @@ -240,8 +242,29 @@ protected void appendLimitAndOffset(Dialect dialect, long limit, long offset, St } @Override - protected void appendPaginationAndOrder(AnnotationMetadata annotationMetadata, SelectQueryDefinition definition, boolean pagination, QueryState queryState) { - appendOrder(annotationMetadata, definition.order(), queryState); + protected boolean supportsLimitQuery() { + return false; + } + + @Override + protected void appendLimitAndOrder(AnnotationMetadata annotationMetadata, SelectQueryDefinition definition, boolean appendLimit, boolean appendOrder, QueryState queryState) { + if (appendOrder) { + appendOrder(annotationMetadata, definition.order(), queryState); + } + } + + @Override + protected boolean shouldAppendOrder(SelectQueryDefinition definition) { + for (Order order : definition.order()) { + if (order instanceof DefaultOrder defaultOrder) { + if (defaultOrder.isIgnoreCase()) { + // JPA Query doesn't support ignore case order + // Append order at the runtime + return false; + } + } + } + return super.shouldAppendOrder(definition); } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java index 3680201375..7f8787c52b 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java @@ -50,9 +50,11 @@ import io.micronaut.data.model.jpa.criteria.PersistentEntitySubquery; import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityQuery; import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils; +import io.micronaut.data.model.jpa.criteria.impl.DefaultOrder; import io.micronaut.data.model.jpa.criteria.impl.DefaultPersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.impl.ExpressionVisitor; import io.micronaut.data.model.jpa.criteria.impl.IParameterExpression; +import io.micronaut.data.model.jpa.criteria.impl.BoundPathParameterExpression; import io.micronaut.data.model.jpa.criteria.impl.SelectionVisitor; import io.micronaut.data.model.jpa.criteria.impl.expression.BinaryExpression; import io.micronaut.data.model.jpa.criteria.impl.expression.FunctionExpression; @@ -192,24 +194,47 @@ private PersistentPropertyPath asPersistentPropertyPath(PersistentProperty persi @Override public QueryResult buildSelect(AnnotationMetadata annotationMetadata, SelectQueryDefinition definition) { QueryBuilder queryBuilder = new QueryBuilder(); - QueryState queryState = buildQuery(annotationMetadata, definition, queryBuilder, false, null); + boolean appendOrder = shouldAppendOrder(definition); + // We cannot append limit if order can come at the runtime + boolean appendLimit = supportsLimitQuery() && appendOrder && !parameterInRoleModifiesLimit(definition.parametersInRole()); + QueryState queryState = buildQuery(annotationMetadata, definition, queryBuilder, appendLimit, appendOrder, null); return QueryResult.of( queryState.getFinalQuery(), queryState.getQueryParts(), queryState.getParameterBindings(), - definition.limit(), - definition.offset(), + appendLimit ? -1 : definition.limit(), + appendLimit ? 0 : definition.offset(), + appendOrder ? Sort.UNSORTED : definition.asSort(), queryState.getJoinPaths() ); } + /** + * Should append order. + * + * @param definition The definition + * @return true if should + */ + protected boolean shouldAppendOrder(SelectQueryDefinition definition) { + return !parameterInRoleModifiesOrder(definition.parametersInRole()); + } + + protected static boolean parameterInRoleModifiesOrder(Map parametersInRole) { + return parametersInRole.containsValue(TypeRole.SORT) || parametersInRole.containsValue(TypeRole.PAGEABLE) || parametersInRole.containsValue(TypeRole.PAGEABLE_REQUIRED); + } + + protected static boolean parameterInRoleModifiesLimit(Map parametersInRole) { + return parametersInRole.containsValue(TypeRole.PAGEABLE) || parametersInRole.containsValue(TypeRole.PAGEABLE_REQUIRED) || parametersInRole.containsValue(TypeRole.LIMIT); + } + @NonNull protected final QueryState buildQuery(AnnotationMetadata annotationMetadata, - SelectQueryDefinition definition, - QueryBuilder queryBuilder, - boolean supportsQueryPagination, - @Nullable String tableAliasPrefix) { + SelectQueryDefinition definition, + QueryBuilder queryBuilder, + boolean appendLimit, + boolean appendOrder, + @Nullable String tableAliasPrefix) { QueryState queryState = new QueryState(queryBuilder, definition, true, true, tableAliasPrefix); Predicate predicate = definition.predicate(); @@ -232,15 +257,23 @@ protected final QueryState buildQuery(AnnotationMetadata annotationMetadata, if (predicate != null || annotationMetadata.hasStereotype(WhereSpecifications.class) || queryState.getEntity().getAnnotationMetadata().hasStereotype(WhereSpecifications.class)) { buildWhereClause(annotationMetadata, predicate, queryState); } - appendPaginationAndOrder(annotationMetadata, definition, supportsQueryPagination, queryState); + appendLimitAndOrder(annotationMetadata, definition, appendLimit, appendOrder, queryState); appendForUpdate(QueryPosition.END_OF_QUERY, definition, queryState.getQuery()); return queryState; } - protected void appendPaginationAndOrder(AnnotationMetadata annotationMetadata, - SelectQueryDefinition definition, - boolean pagination, - QueryState queryState) { + /** + * @return True if limit is supported in the query + */ + protected boolean supportsLimitQuery() { + return true; + } + + protected void appendLimitAndOrder(AnnotationMetadata annotationMetadata, + SelectQueryDefinition definition, + boolean appendLimit, + boolean appendOrder, + QueryState queryState) { } /** @@ -634,42 +667,49 @@ protected final String resolveWhereForAnnotationMetadata(String alias, Annotatio * @param queryState the query state */ protected void appendOrder(AnnotationMetadata annotationMetadata, List orders, QueryState queryState) { - if (!orders.isEmpty()) { - StringBuilder buff = queryState.getQuery(); - buff.append(ORDER_BY_CLAUSE); + if (orders.isEmpty()) { + return; + } + StringBuilder buff = queryState.getQuery(); + buff.append(ORDER_BY_CLAUSE); - String jsonEntityColumn = getJsonEntityColumn(annotationMetadata); + String jsonEntityColumn = getJsonEntityColumn(annotationMetadata); - Iterator i = orders.iterator(); - while (i.hasNext()) { - Order order = i.next(); - QueryPropertyPath propertyPath = queryState.findProperty(requireProperty(order.getExpression()).getPropertyPath()); - String currentAlias = propertyPath.getTableAlias(); - if (currentAlias != null) { - buff.append(currentAlias).append(DOT); - } + Iterator i = orders.iterator(); + while (i.hasNext()) { + Order order = i.next(); + QueryPropertyPath propertyPath = queryState.findProperty(requireProperty(order.getExpression()).getPropertyPath()); + String currentAlias = propertyPath.getTableAlias(); + boolean ignoreCase = order instanceof DefaultOrder defaultOrder && defaultOrder.isIgnoreCase(); + if (ignoreCase) { + buff.append("LOWER("); + } + if (currentAlias != null) { + buff.append(currentAlias).append(DOT); + } + if (jsonEntityColumn != null) { + buff.append(jsonEntityColumn).append(DOT); + } + if (computePropertyPaths() && jsonEntityColumn == null) { + buff.append(propertyPath.getColumnName()); + } else { + buff.append(propertyPath.getPath()); if (jsonEntityColumn != null) { - buff.append(jsonEntityColumn).append(DOT); - } - String direction; - if (order.isAscending()) { - direction = "ASC"; - } else { - direction = "DESC"; - } - if (computePropertyPaths() && jsonEntityColumn == null) { - buff.append(propertyPath.getColumnName()).append(SPACE).append(direction); - } else { - buff.append(propertyPath.getPath()); - if (jsonEntityColumn != null) { - appendJsonProjection(buff, propertyPath.getProperty().getDataType()); - } - buff.append(SPACE).append(direction); - } - if (i.hasNext()) { - buff.append(","); + appendJsonProjection(buff, propertyPath.getProperty().getDataType()); } } + if (ignoreCase) { + buff.append(")"); + } + buff.append(SPACE); + if (order.isAscending()) { + buff.append("ASC"); + } else { + buff.append("DESC"); + } + if (i.hasNext()) { + buff.append(","); + } } } @@ -770,12 +810,16 @@ public JsonDataType getJsonDataType() { } queryString.append(propertyPath.getPath()).append('='); } - if (entry.getValue() instanceof BindingParameter bindingParameter) { + Object value = entry.getValue(); + if (value instanceof BindingParameter bindingParameter) { appendUpdateSetParameter(queryString, tableAlias, prop, () -> { queryState.pushParameter(bindingParameter, newBindingContext(propertyPath.propertyPath)); }); + } else if (value instanceof IExpression expression) { + new ExpressionAppender(queryState, annotationMetadata) + .appendExpression(expression, new DefaultPersistentPropertyPath<>(propertyPath.propertyPath, null)); } else { - queryString.append(asLiteral(entry.getValue())); + queryString.append(asLiteral(value)); } if (jsonViewColumnName == null) { queryString.append(COMMA); @@ -825,7 +869,13 @@ public JsonDataType getJsonDataType() { queryString.append(tableAlias).append(DOT); } queryString.append(propertyPath.getColumnName()).append('='); - queryString.append(asLiteral(entry.getValue())); + Object value = entry.getValue(); + if (value instanceof IExpression expression) { + new ExpressionAppender(queryState, annotationMetadata) + .appendExpression(expression, new DefaultPersistentPropertyPath<>(propertyPath.propertyPath, null)); + } else { + queryString.append(asLiteral(value)); + } queryString.append(COMMA); needsTrimming[0] = true; } @@ -1592,7 +1642,7 @@ private String getAliasName(JoinPath joinPath) { if (rootAlias == null) { ownerAlias = AbstractSqlLikeQueryBuilder2.this.getAliasName(owner); } else { - ownerAlias = rootAlias; + ownerAlias = rootAlias; } } else { ownerAlias = AbstractSqlLikeQueryBuilder2.this.getAliasName(owner); @@ -1901,25 +1951,10 @@ protected enum QueryPosition { /** * The predicate visitor to construct the query. */ - protected class SqlPredicateVisitor implements AdvancedPredicateVisitor { - - protected final PersistentEntity persistentEntity; - protected final String tableAlias; - protected final StringBuilder query; - protected final QueryState queryState; - protected final AnnotationMetadata annotationMetadata; + protected class SqlPredicateVisitor extends ExpressionAppender implements AdvancedPredicateVisitor { protected SqlPredicateVisitor(QueryState queryState, AnnotationMetadata annotationMetadata) { - this.queryState = queryState; - this.annotationMetadata = annotationMetadata; - persistentEntity = queryState.getEntity(); - tableAlias = queryState.getRootAlias(); - query = queryState.getQuery(); - } - - @Override - public PersistentPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - return persistentPropertyPath.getPropertyPath(); + super(queryState, annotationMetadata); } private void visitPredicate(IExpression expression) { @@ -2066,10 +2101,11 @@ public void visitEquals(Expression leftExpression, Expression rightExpress PersistentProperty property = propertyPath.getProperty(); if (computePropertyPaths() && property instanceof Association) { List predicates = new ArrayList<>(); + Expression finalRightExpression = rightExpression; PersistentEntityUtils.traverse(propertyPath, pp -> predicates.add(new BinaryPredicate( new DefaultPersistentPropertyPath<>(pp, null), - rightExpression, + finalRightExpression, ignoreCase ? PredicateBinaryOp.EQUALS_IGNORE_CASE : PredicateBinaryOp.EQUALS )) ); @@ -2080,6 +2116,23 @@ public void visitEquals(Expression leftExpression, Expression rightExpress } return; } + if (property.isEnum() && rightExpression instanceof LiteralExpression literalExpression + && literalExpression.getValue() instanceof String stringValue) { + String typeName = property.getTypeName().replace("$", "."); + if (stringValue.startsWith(typeName)) { + for (PersistentProperty.EnumConstant enumConstant : property.getEnumConstants()) { + if (stringValue.equals(typeName + "." + enumConstant.name())) { + if (property.getDataType() == DataType.STRING) { + rightExpression = new LiteralExpression(enumConstant.name()); + } + if (property.getDataType() == DataType.INTEGER) { + rightExpression = new LiteralExpression(enumConstant.ordinal()); + } + break; + } + } + } + } } if (ignoreCase) { appendCaseInsensitiveOp(leftExpression, rightExpression, " = "); @@ -2185,13 +2238,16 @@ private void appendLikeConcatComparison(Expression leftExpression, Expression @Override public void visitIdEquals(Expression expression) { if (persistentEntity.hasCompositeIdentity()) { + if (!(expression instanceof IParameterExpression parameterExpression)) { + throw new IllegalStateException("Composite identity expressions can only be used with parameters"); + } new ConjunctionPredicate( Arrays.stream(persistentEntity.getCompositeIdentity()) .map(prop -> { PersistentPropertyPath propertyPath = asPersistentPropertyPath(prop); return new BinaryPredicate( new DefaultPersistentPropertyPath<>(propertyPath, null), - expression, + new BoundPathParameterExpression<>(parameterExpression, propertyPath), PredicateBinaryOp.EQUALS ); } @@ -2209,166 +2265,6 @@ public void visitIdEquals(Expression expression) { } } - protected final void appendPropertyRef(PersistentPropertyPath propertyPath) { - AbstractSqlLikeQueryBuilder2.this.appendPropertyRef(annotationMetadata, query, queryState, propertyPath, false); - } - - private void appendBinaryOperation(@NonNull String operator, @NonNull Expression leftExpression, @NonNull Expression rightExpression) { - appendExpression(leftExpression, null); - query.append(operator); - appendExpression(rightExpression, leftExpression); - } - - private void appendExpression(Expression expression) { - appendExpression(expression, null); - } - - protected final void appendExpression(Expression expression, @Nullable Expression boundedExpression) { - CriteriaUtils.requireIExpression(expression).visitExpression(new ExpressionVisitor() { - - @Override - public void visit(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - appendPropertyRef(persistentPropertyPath.getPropertyPath()); - } - - @Override - public void visit(PersistentEntityRoot entityRoot) { - visit(new IdExpression<>(entityRoot)); - } - - @Override - public void visit(LiteralExpression literalExpression) { - query.append(asLiteral(literalExpression)); - } - - @Override - public void visit(UnaryExpression unaryExpression) { - Expression expression = unaryExpression.getExpression(); - switch (unaryExpression.getType()) { - case SUM, AVG, MAX, MIN, UPPER, LOWER -> - appendFunction(unaryExpression.getType().name(), expression); - default -> - throw new IllegalStateException(UNSUPPORTED_EXPRESSION + unaryExpression.getType()); - } - } - - @Override - public void visit(BinaryExpression binaryExpression) { - Expression left = binaryExpression.getLeft(); - Expression right = binaryExpression.getRight(); - switch (binaryExpression.getType()) { - case SUM -> { - appendExpression(left); - query.append(" + "); - appendExpression(right); - } - case CONCAT -> appendFunction("CONCAT", List.of(left, right)); - default -> - throw new IllegalStateException(UNSUPPORTED_EXPRESSION + binaryExpression.getType()); - } - } - - @Override - public void visit(IdExpression idExpression) { - PersistentEntity persistentEntity = idExpression.getRoot().getPersistentEntity(); - if (persistentEntity.hasCompositeIdentity()) { - throw new IllegalStateException("ID expression with composite IDs not allowed"); - } - if (persistentEntity.getIdentityProperties().size() > 1) { - throw new IllegalStateException("ID expression with multiple IDs not allowed"); - } - PersistentProperty identity = persistentEntity.getIdentity(); - appendPropertyRef(new PersistentPropertyPath(identity)); - } - - @Override - public void visit(FunctionExpression functionExpression) { - appendFunction(functionExpression.getName(), functionExpression.getExpressions()); - } - - @Override - public void visit(IParameterExpression parameterExpression) { - appendBindingParameter(parameterExpression, findParameterBoundProperty(boundedExpression)); - } - - @Override - public void visit(SubqueryExpression subqueryExpression) { - query.append(subqueryExpression.getType().name()); - visit(subqueryExpression.getSubquery()); - } - - @Override - public void visit(PersistentEntitySubquery subquery) { - AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) subquery; - SelectQueryDefinition selectQueryDefinition = abstractPersistentEntityQuery.toSelectQueryDefinition(); - String outerAlias = queryState.getRootAlias(); - if (outerAlias == null) { - outerAlias = getAliasName(queryState.getEntity()); - } - boolean requiresBrackets = query.charAt(query.length() - 1) != '('; - if (requiresBrackets) { - query.append("("); - } - buildQuery(AnnotationMetadata.EMPTY_METADATA, selectQueryDefinition, queryState.queryBuilder, false, outerAlias); - if (requiresBrackets) { - query.append(")"); - } - } - }); - } - - private PersistentPropertyPath findParameterBoundProperty(Expression binaryOpExpression) { - // We want to find the property bound to the parameter - if (binaryOpExpression == null) { - return null; - } - if (binaryOpExpression instanceof UnaryExpression unaryExpression) { - return findParameterBoundProperty(unaryExpression.getExpression()); - } - if (binaryOpExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - return persistentPropertyPath.getPropertyPath(); - } - return null; - } - - private void appendFunction(String functionName, Expression expression) { - appendFunction(functionName, List.of(expression)); - } - - private void appendFunction(String functionName, List> expressions) { - query.append(functionName) - .append(OPEN_BRACKET); - for (Iterator> iterator = expressions.iterator(); iterator.hasNext(); ) { - Expression expression = iterator.next(); - appendExpression(expression); - if (iterator.hasNext()) { - query.append(COMMA); - } - } - query.append(CLOSE_BRACKET); - } - - private void appendBindingParameter(BindingParameter bindingParameter, - @Nullable PersistentPropertyPath entityPropertyPath) { - Runnable pushParameter = () -> { - queryState.pushParameter( - bindingParameter, - newBindingContext(null, entityPropertyPath) - ); - }; - if (entityPropertyPath == null) { - pushParameter.run(); - } else { - QueryPropertyPath qpp = queryState.findProperty(entityPropertyPath); - String writeTransformer = getDataTransformerWriteValue(qpp.tableAlias, entityPropertyPath.getProperty()).orElse(null); - if (writeTransformer != null) { - appendTransformed(query, writeTransformer, pushParameter); - } else { - pushParameter.run(); - } - } - } - private void appendCaseInsensitiveOp(Expression leftExpression, Expression expression, String operator) { query.append("LOWER("); appendExpression(leftExpression); @@ -2442,7 +2338,11 @@ private void appendUnaryCondition(String sqlOp, Expression expression) { } @Override - public void visitInBetween(Expression value, Expression from, Expression to) { + public void visitInBetween(Expression value, Expression from, Expression to, boolean negated) { + if (negated) { + query.append(NOT); + query.append(" "); + } query.append(OPEN_BRACKET); appendExpression(value); query.append(" >= "); @@ -2484,6 +2384,212 @@ public void visitIn(Expression expression, Collection values, boolean nega } + protected class ExpressionAppender implements ExpressionVisitor { + + protected final PersistentEntity persistentEntity; + protected final String tableAlias; + protected final StringBuilder query; + protected final QueryState queryState; + protected final AnnotationMetadata annotationMetadata; + + private @Nullable Expression boundedExpression; + + protected ExpressionAppender(QueryState queryState, AnnotationMetadata annotationMetadata) { + this.queryState = queryState; + this.annotationMetadata = annotationMetadata; + persistentEntity = queryState.getEntity(); + tableAlias = queryState.getRootAlias(); + query = queryState.getQuery(); + } + + public final PersistentPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { + return persistentPropertyPath.getPropertyPath(); + } + + protected final void appendPropertyRef(PersistentPropertyPath propertyPath) { + AbstractSqlLikeQueryBuilder2.this.appendPropertyRef(annotationMetadata, query, queryState, propertyPath, false); + } + + protected final void appendBinaryOperation(@NonNull String operator, @NonNull Expression leftExpression, @NonNull Expression rightExpression) { + appendExpression(leftExpression, null); + query.append(operator); + appendExpression(rightExpression, leftExpression); + } + + protected final void appendExpression(Expression expression) { + appendExpression(expression, null); + } + + protected final void appendExpression(Expression expression, @Nullable Expression boundedExpression) { + this.boundedExpression = boundedExpression; + CriteriaUtils.requireIExpression(expression).visitExpression(this); + this.boundedExpression = null; + } + + protected final PersistentPropertyPath findParameterBoundProperty(Expression binaryOpExpression) { + // We want to find the property bound to the parameter + if (binaryOpExpression == null) { + return null; + } + if (binaryOpExpression instanceof UnaryExpression unaryExpression) { + return findParameterBoundProperty(unaryExpression.getExpression()); + } + if (binaryOpExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { + return persistentPropertyPath.getPropertyPath(); + } + return null; + } + + protected final void appendFunction(String functionName, Expression expression) { + appendFunction(functionName, List.of(expression)); + } + + protected final void appendFunction(String functionName, List> expressions) { + query.append(functionName) + .append(OPEN_BRACKET); + for (Iterator> iterator = expressions.iterator(); iterator.hasNext(); ) { + Expression expression = iterator.next(); + appendExpression(expression); + if (iterator.hasNext()) { + query.append(COMMA); + } + } + query.append(CLOSE_BRACKET); + } + + protected final void appendBindingParameter(BindingParameter bindingParameter, + @Nullable PersistentPropertyPath entityPropertyPath) { + Runnable pushParameter = () -> { + queryState.pushParameter( + bindingParameter, + newBindingContext(null, entityPropertyPath) + ); + }; + if (entityPropertyPath == null) { + pushParameter.run(); + } else { + QueryPropertyPath qpp = queryState.findProperty(entityPropertyPath); + String writeTransformer = getDataTransformerWriteValue(qpp.tableAlias, entityPropertyPath.getProperty()).orElse(null); + if (writeTransformer != null) { + appendTransformed(query, writeTransformer, pushParameter); + } else { + pushParameter.run(); + } + } + } + + @Override + public void visit(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { + appendPropertyRef(persistentPropertyPath.getPropertyPath()); + } + + @Override + public void visit(PersistentEntityRoot entityRoot) { + visit(new IdExpression<>(entityRoot)); + } + + @Override + public void visit(LiteralExpression literalExpression) { + query.append(asLiteral(literalExpression)); + } + + @Override + public void visit(UnaryExpression unaryExpression) { + Expression expression = unaryExpression.getExpression(); + switch (unaryExpression.getType()) { + case LENGTH -> { + if (getDialect() == Dialect.SQL_SERVER) { + appendFunction("LEN", expression); + } else { + appendFunction("LENGTH", expression); + } + } + case SUM, AVG, MAX, MIN, UPPER, LOWER -> + appendFunction(unaryExpression.getType().name(), expression); + default -> + throw new IllegalStateException(UNSUPPORTED_EXPRESSION + unaryExpression.getType()); + } + } + + @Override + public void visit(BinaryExpression binaryExpression) { + Expression left = binaryExpression.getLeft(); + Expression right = binaryExpression.getRight(); + switch (binaryExpression.getType()) { + case SUM -> { + appendExpression(left); + query.append(" + "); + appendExpression(right); + } + case DIFF -> { + appendExpression(left); + query.append(" - "); + appendExpression(right); + } + case QUOT -> { + appendExpression(left); + query.append(" / "); + appendExpression(right); + } + case PROD -> { + appendExpression(left); + query.append(" * "); + appendExpression(right); + } + case CONCAT -> appendFunction("CONCAT", List.of(left, right)); + default -> + throw new IllegalStateException(UNSUPPORTED_EXPRESSION + binaryExpression.getType()); + } + } + + @Override + public void visit(IdExpression idExpression) { + PersistentEntity persistentEntity = idExpression.getRoot().getPersistentEntity(); + if (persistentEntity.hasCompositeIdentity()) { + throw new IllegalStateException("ID expression with composite IDs not allowed"); + } + if (persistentEntity.getIdentityProperties().size() > 1) { + throw new IllegalStateException("ID expression with multiple IDs not allowed"); + } + PersistentProperty identity = persistentEntity.getIdentity(); + appendPropertyRef(new PersistentPropertyPath(identity)); + } + + @Override + public void visit(FunctionExpression functionExpression) { + appendFunction(functionExpression.getName(), functionExpression.getExpressions()); + } + + @Override + public void visit(IParameterExpression parameterExpression) { + appendBindingParameter(parameterExpression, findParameterBoundProperty(boundedExpression)); + } + + @Override + public void visit(SubqueryExpression subqueryExpression) { + query.append(subqueryExpression.getType().name()); + visit(subqueryExpression.getSubquery()); + } + + @Override + public void visit(PersistentEntitySubquery subquery) { + AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) subquery; + SelectQueryDefinition selectQueryDefinition = abstractPersistentEntityQuery.toSelectQueryDefinition(); + String outerAlias = queryState.getRootAlias(); + if (outerAlias == null) { + outerAlias = getAliasName(queryState.getEntity()); + } + boolean requiresBrackets = query.charAt(query.length() - 1) != '('; + if (requiresBrackets) { + query.append("("); + } + buildQuery(AnnotationMetadata.EMPTY_METADATA, selectQueryDefinition, queryState.queryBuilder, false, true, outerAlias); + if (requiresBrackets) { + query.append(")"); + } + } + } + /** * The selection visitor to construct the query. */ @@ -2587,6 +2693,13 @@ public void visit(LiteralExpression literalExpression) { public void visit(UnaryExpression unaryExpression) { Expression expression = unaryExpression.getExpression(); switch (unaryExpression.getType()) { + case LENGTH -> { + if (getDialect() == Dialect.SQL_SERVER) { + appendFunction("LEN", expression); + } else { + appendFunction("LENGTH", expression); + } + } case SUM, AVG, MAX, MIN, UPPER, LOWER -> appendFunction(unaryExpression.getType().name(), expression); case COUNT -> { @@ -2623,6 +2736,21 @@ public void visit(BinaryExpression binaryExpression) { query.append(" + "); appendExpression(right); } + case DIFF -> { + appendExpression(left); + query.append(" - "); + appendExpression(right); + } + case QUOT -> { + appendExpression(left); + query.append(" / "); + appendExpression(right); + } + case PROD -> { + appendExpression(left); + query.append(" * "); + appendExpression(right); + } case CONCAT -> appendFunction("CONCAT", List.of(left, right)); default -> throw new IllegalStateException(UNSUPPORTED_EXPRESSION + binaryExpression.getType()); diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java index 9bdb896d12..cad168d2f2 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java @@ -33,7 +33,6 @@ import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.Relation; import io.micronaut.data.annotation.Repository; -import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.annotation.sql.JoinColumn; import io.micronaut.data.annotation.sql.JoinColumns; import io.micronaut.data.annotation.sql.SqlMembers; @@ -49,7 +48,7 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.impl.DefaultPersistentPropertyPath; -import io.micronaut.data.model.jpa.criteria.impl.PersistentPropertyOrder; +import io.micronaut.data.model.jpa.criteria.impl.DefaultOrder; import io.micronaut.data.model.naming.NamingStrategy; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.builder.QueryParameterBinding; @@ -207,7 +206,7 @@ protected String asLiteral(Object value) { @NonNull public String buildBatchCreateTableStatement(@NonNull PersistentEntity... entities) { return Arrays.stream(entities).flatMap(entity -> Stream.of(buildCreateTableStatements(entity))) - .collect(Collectors.joining(System.getProperty("line.separator"))); + .collect(Collectors.joining(System.lineSeparator())); } /** @@ -1476,40 +1475,24 @@ public final String positionalParameterFormat() { } @Override - public QueryResult buildSelect(@NonNull AnnotationMetadata annotationMetadata, @NonNull SelectQueryDefinition definition) { - if (definition.parametersInRole().isEmpty()) { - // We can directly generate the query with limit and offset and omit the runtime modification - QueryState queryState = buildQuery(annotationMetadata, definition, new QueryBuilder(), true, null); - - return QueryResult.of( - queryState.getFinalQuery(), - queryState.getQueryParts(), - queryState.getParameterBindings(), - queryState.getJoinPaths() - ); - } - - return super.buildSelect(annotationMetadata, definition); - } - - @Override - protected void appendPaginationAndOrder(AnnotationMetadata annotationMetadata, - SelectQueryDefinition definition, - boolean pagination, - QueryState queryState) { - Map parametersInRole = definition.parametersInRole(); - if (parametersInRole.isEmpty()) { - // Directly create a query with LIMIT and ORDER + protected void appendLimitAndOrder(AnnotationMetadata annotationMetadata, + SelectQueryDefinition definition, + boolean appendLimit, + boolean appendOrder, + QueryState queryState) { + if (appendOrder) { appendOrder(annotationMetadata, definition, queryState); - if (pagination) { - appendLimitAndOffset(getDialect(), definition.limit(), definition.offset(), queryState.getQuery()); - } - } else if (parametersInRole.containsKey(TypeRole.SORT) || parametersInRole.containsKey(TypeRole.PAGEABLE) || parametersInRole.containsKey(TypeRole.PAGEABLE_REQUIRED)) { - Map.Entry e = parametersInRole.entrySet().iterator().next(); + } + if (appendLimit) { + appendLimitAndOffset(getDialect(), definition.limit(), definition.offset(), queryState.getQuery()); + } + Map parametersInRole = definition.parametersInRole(); + if (parameterInRoleModifiesOrder(parametersInRole) || parameterInRoleModifiesLimit(parametersInRole)) { + Map.Entry e = parametersInRole.entrySet().iterator().next(); queryState.pushParameter(new QueryParameterBinding() { @Override public String getName() { - return e.getKey(); + return e.getValue(); } @Override @@ -1519,7 +1502,7 @@ public String getKey() { @Override public int getParameterIndex() { - return e.getValue(); + return e.getKey(); } @Override @@ -1534,7 +1517,7 @@ public boolean isExpandable() { @Override public String getRole() { - return e.getKey(); + return e.getValue(); } @Override @@ -1554,7 +1537,7 @@ private void appendOrder(AnnotationMetadata annotationMetadata, SelectQueryDefin if (identity == null) { throw new DataAccessException("Pagination requires an entity ID on SQL Server"); } - orders = List.of(new PersistentPropertyOrder<>(new DefaultPersistentPropertyPath<>(identity, List.of(), null), true)); + orders = List.of(new DefaultOrder<>(new DefaultPersistentPropertyPath<>(identity, List.of(), null), true, false)); } appendOrder(annotationMetadata, orders, queryState); } 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 a65ddf79f8..998a802655 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 @@ -62,7 +62,7 @@ default void visit(UnaryPredicate unaryPredicate) { @Override default void visit(BetweenPredicate betweenPredicate) { - visitInBetween(betweenPredicate.getValue(), betweenPredicate.getFrom(), betweenPredicate.getTo()); + visitInBetween(betweenPredicate.getValue(), betweenPredicate.getFrom(), betweenPredicate.getTo(), false); } @Override @@ -126,7 +126,7 @@ default void visitArrayContains(Expression leftExpression, Expression expr void visitLessThanOrEquals(Expression leftExpression, Expression rightExpression); - void visitInBetween(Expression value, Expression from, Expression to); + void visitInBetween(Expression value, Expression from, Expression to, boolean negated); void visitIsFalse(Expression expression); diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java b/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java index 4674f7360c..cf27be457e 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java @@ -18,7 +18,9 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.naming.Named; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; import java.util.Collections; import java.util.Map; @@ -44,6 +46,24 @@ public interface PagedQuery extends Named, AnnotationMetadataProvider { @NonNull Pageable getPageable(); + /** + * @return The limit + * @see 4.12 + */ + @NonNull + default Limit getQueryLimit() { + return getPageable().getLimit(); + } + + /** + * @return The sort + * @see 4.12 + */ + @NonNull + default Sort getSort() { + return getPageable().getSort(); + } + /** * The parameter binding. That is the mapping between named query parameters and parameters of the method. * diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedDataOperation.java b/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedDataOperation.java index ca82584a1b..4a6a696a11 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedDataOperation.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedDataOperation.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.attr.AttributeHolder; +import java.util.List; import java.util.Optional; /** @@ -39,4 +40,15 @@ public interface PreparedDataOperation extends StoredDataOperation, Attrib default Optional getParameterInRole(@NonNull String role, @NonNull Class type) { return Optional.empty(); } + + /** + * Return the values of the given parameter if the given role. + * @param role The role + * @param type The type + * @param The generic type + * @return An optional value. + */ + default List getParametersInRole(@NonNull String role, @NonNull Class type) { + return getParameterInRole(role, type).stream().toList(); + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedQuery.java b/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedQuery.java index 76aca6a299..e59dfdd88a 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/PreparedQuery.java @@ -16,7 +16,11 @@ package io.micronaut.data.model.runtime; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.ConversionServiceProvider; import io.micronaut.core.type.Argument; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Sort; import java.util.Collections; import java.util.Map; @@ -29,7 +33,7 @@ * @param The entity type * @param The result type */ -public interface PreparedQuery extends PagedQuery, StoredQuery, PreparedDataOperation { +public interface PreparedQuery extends PagedQuery, StoredQuery, PreparedDataOperation, ConversionServiceProvider { /** * @return The repository type. @@ -59,4 +63,19 @@ default Map getQueryHints() { */ @Override boolean isRawQuery(); + + @Override + default Sort getSort() { + return PagedQuery.super.getSort(); + } + + @Override + default Limit getQueryLimit() { + return PagedQuery.super.getQueryLimit(); + } + + @Override + default @NonNull ConversionService getConversionService() { + return ConversionService.SHARED; + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentProperty.java b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentProperty.java index f790994868..55aaac8114 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentProperty.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentProperty.java @@ -27,6 +27,8 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.runtime.convert.AttributeConverter; +import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -112,6 +114,27 @@ public boolean isEnum() { return type.isEnum(); } + @Override + public List getEnumConstants() { + if (type.isEnum()) { + return Arrays.stream(type.getEnumConstants()).map(it -> new EnumConstant() { + + final Enum e = (Enum) it; + + @Override + public String name() { + return e.name(); + } + + @Override + public int ordinal() { + return e.ordinal(); + } + }).toList(); + } + return List.of(); + } + @Override public DataType getDataType() { return dataType; diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java b/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java index a463ee925f..46d31d4717 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java @@ -22,6 +22,8 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.JoinPath; import java.util.Collections; @@ -251,17 +253,39 @@ default Map> getParameterExpressions() { /** * @return The limit of the query or -1 if none * @since 4.10 + * @deprecated Replaced by {@link #getQueryLimit()} ()} */ + @Deprecated(forRemoval = true, since = "4.12") default int getLimit() { - return -1; + return getQueryLimit().maxResults(); } /** * @return The offset of the query or 0 if none * @since 4.10 + * @deprecated Replaced by {@link #getQueryLimit()} ()} */ + @Deprecated(forRemoval = true, since = "4.12") default int getOffset() { - return 0; + return (int) getQueryLimit().offset(); + } + + /** + * @return The query limit + * @since 4.12 + */ + @NonNull + default Limit getQueryLimit() { + return Limit.of(getLimit(), getOffset()); + } + + /** + * @return The runtime sort + * @since 4.12 + */ + @NonNull + default Sort getSort() { + return Sort.UNSORTED; } /** diff --git a/data-mongodb/build.gradle b/data-mongodb/build.gradle index 6da5bde414..24364773c2 100644 --- a/data-mongodb/build.gradle +++ b/data-mongodb/build.gradle @@ -35,6 +35,7 @@ dependencies { compileOnly mnMongo.mongo.driver compileOnly mnMongo.mongo.reactive compileOnly mnReactor.micronaut.reactor + compileOnly libs.managed.jakarta.data.api testAnnotationProcessor projects.micronautDataDocumentProcessor testAnnotationProcessor mn.micronaut.inject.java diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/exceptions/jakarta/data/MongoJakartaDataExceptionConverter.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/exceptions/jakarta/data/MongoJakartaDataExceptionConverter.java new file mode 100644 index 0000000000..885af9ace3 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/exceptions/jakarta/data/MongoJakartaDataExceptionConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2025 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.exceptions.jakarta.data; + +import com.mongodb.MongoWriteException; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.order.Ordered; +import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataDeleteExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataInsertExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataUpdateExceptionConverter; +import jakarta.data.exceptions.EntityExistsException; +import jakarta.inject.Singleton; + +/** + * The Micronaut Data to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@Singleton +@Requires(classes = jakarta.data.exceptions.OptimisticLockingFailureException.class) +final class MongoJakartaDataExceptionConverter implements JakartaDataExceptionConverter, JakartaDataUpdateExceptionConverter, + JakartaDataDeleteExceptionConverter, JakartaDataInsertExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof DataAccessException) { + if (exception.getCause() instanceof MongoWriteException e && e.getError().getCode() == 11000) { + throw new EntityExistsException(exception.getMessage(), e); + } + } + return exception; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/AbstractMongoRepositoryOperations.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/AbstractMongoRepositoryOperations.java index bd0fb0670b..ee57f0ccfb 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/AbstractMongoRepositoryOperations.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/AbstractMongoRepositoryOperations.java @@ -263,48 +263,53 @@ protected final Bson createFilterIdAndVersion(RuntimePersistentEntity per filter.put(MongoUtils.ID, bsonDocument.get(MongoUtils.ID)); RuntimePersistentProperty version = persistentEntity.getVersion(); if (version != null) { - filter.put(version.getPersistedName(), bsonDocument.get(version.getPersistedName())); + // We don't support naming strategy for Mongo entity properties + String versionPropertyName = version.getName(); + BsonValue value = bsonDocument.get(versionPropertyName); + if (value != null) { + filter.put(versionPropertyName, value); + } } return filter; } protected void logFind(MongoFind find) { - StringBuilder sb = new StringBuilder("Executing Mongo 'find'"); - MongoFindOptions options = find.getOptions(); - if (options != null) { - sb.append(" with"); - Bson filter = options.getFilter(); - sb.append(" filter: ").append(filter == null ? "{}" : filter.toBsonDocument().toJson()); - Bson sort = options.getSort(); - if (sort != null) { - sb.append(" sort: ").append(sort.toBsonDocument().toJson()); - } - Bson projection = options.getProjection(); - if (projection != null) { - sb.append(" projection: ").append(projection.toBsonDocument().toJson()); - } - Collation collation = options.getCollation(); - if (collation != null) { - sb.append(" collation: ").append(collation); - } - } if (QUERY_LOG.isDebugEnabled()) { + StringBuilder sb = new StringBuilder("Executing Mongo 'find'"); + MongoFindOptions options = find.getOptions(); + if (options != null) { + sb.append(" with"); + Bson filter = options.getFilter(); + sb.append(" filter: ").append(filter == null ? "{}" : filter.toBsonDocument().toJson()); + Bson sort = options.getSort(); + if (sort != null) { + sb.append(" sort: ").append(sort.toBsonDocument().toJson()); + } + Bson projection = options.getProjection(); + if (projection != null) { + sb.append(" projection: ").append(projection.toBsonDocument().toJson()); + } + Collation collation = options.getCollation(); + if (collation != null) { + sb.append(" collation: ").append(collation); + } + } QUERY_LOG.debug(sb.toString()); } } protected void logAggregate(MongoAggregation aggregation) { - MongoAggregationOptions options = aggregation.getOptions(); - StringBuilder sb = new StringBuilder("Executing Mongo 'aggregate'"); - if (options != null) { + if (QUERY_LOG.isDebugEnabled()) { + StringBuilder sb = new StringBuilder("Executing Mongo 'aggregate'"); sb.append(" with"); sb.append(" pipeline: ").append(aggregation.getPipeline().stream().map(e -> e.toBsonDocument().toJson()).toList()); - Collation collation = options.getCollation(); - if (collation != null) { - sb.append(" collation: ").append(collation); + MongoAggregationOptions options = aggregation.getOptions(); + if (options != null) { + Collation collation = options.getCollation(); + if (collation != null) { + sb.append(" collation: ").append(collation); + } } - } - if (QUERY_LOG.isDebugEnabled()) { QUERY_LOG.debug(sb.toString()); } } diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java index a434d25bc6..13dc6d5cf7 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java @@ -17,11 +17,14 @@ import com.mongodb.client.model.Sorts; import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.TypeRole; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.model.runtime.RuntimePersistentEntity; +import io.micronaut.data.model.runtime.RuntimePersistentProperty; import io.micronaut.data.mongodb.operations.options.MongoFindOptions; import io.micronaut.data.runtime.operations.internal.query.DefaultBindableParametersPreparedQuery; import io.micronaut.data.runtime.query.internal.DefaultPreparedQuery; @@ -29,6 +32,7 @@ import io.micronaut.data.runtime.query.internal.DelegateStoredQuery; import org.bson.BsonDocument; import org.bson.BsonInt32; +import org.bson.BsonValue; import org.bson.conversions.Bson; import java.util.ArrayList; @@ -65,15 +69,36 @@ public boolean isAggregate() { return mongoStoredQuery.isAggregate(); } + @Override + public Limit getQueryLimit() { + Limit queryLimit = super.getQueryLimit(); + if (queryLimit.isLimited()) { + return queryLimit; + } + return getParameterInRole(TypeRole.LIMIT, Limit.class).orElse(queryLimit); + } + + @Override + public Sort getSort() { + return super.getSort(); + } + @Override public MongoAggregation getAggregation() { MongoAggregation aggregation = mongoStoredQuery.getAggregation(defaultPreparedQuery.getContext()); Pageable pageable = getPageable(); - if (pageable != Pageable.UNPAGED) { + if (!pageable.isUnpaged()) { List pipeline = new ArrayList<>(aggregation.getPipeline()); applyPageable(pageable, pipeline); return new MongoAggregation(pipeline, aggregation.getOptions()); } + Limit limit = getQueryLimit(); + Sort sort = getSort(); + if (limit.isLimited() || sort.isSorted()) { + List pipeline = new ArrayList<>(aggregation.getPipeline()); + applyPageable(limit, sort, pipeline); + return new MongoAggregation(pipeline, aggregation.getOptions()); + } return aggregation; } @@ -81,24 +106,48 @@ public MongoAggregation getAggregation() { public MongoFind getFind() { MongoFind find = mongoStoredQuery.getFind(defaultPreparedQuery.getContext()); Pageable pageable = defaultPreparedQuery.getPageable(); - if (pageable != Pageable.UNPAGED) { + if (!pageable.isUnpaged()) { if (pageable.getMode() != Mode.OFFSET) { throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); } - MongoFindOptions findOptions = find.getOptions(); - MongoFindOptions options = findOptions == null ? new MongoFindOptions() : new MongoFindOptions(findOptions); - options.limit(pageable.getSize()).skip((int) pageable.getOffset()); - Sort pageableSort = pageable.getSort(); - if (pageableSort.isSorted()) { - Bson sort = pageableSort.getOrderBy().stream().map(order -> order.isAscending() ? Sorts.ascending(order.getProperty()) : Sorts.descending(order.getProperty())) - .collect(Collectors.collectingAndThen(Collectors.toList(), Sorts::orderBy)); - options.sort(sort); - } - return new MongoFind(options); + Limit limit = pageable.getLimit(); + Sort sort = pageable.getSort(); + return applyLimitAndSort(find, limit, sort); + } + Limit limit = getQueryLimit(); + Sort sort = getSort(); + if (limit.isLimited() || sort.isSorted()) { + return applyLimitAndSort(find, limit, sort); } return find; } + private MongoFind applyLimitAndSort(MongoFind find, Limit limit, Sort sort) { + MongoFindOptions findOptions = find.getOptions(); + MongoFindOptions options = findOptions == null ? new MongoFindOptions() : new MongoFindOptions(findOptions); + options.limit(limit.maxResults()).skip((int) limit.offset()); + if (sort.isSorted()) { + Bson sortBson = getSort(sort); + options.sort(sortBson); + } + return new MongoFind(options); + } + + private Bson getSort(Sort sort) { + RuntimePersistentEntity persistentEntity = getPersistentEntity(); + RuntimePersistentProperty identity = persistentEntity.getIdentity(); + return sort.getOrderBy() + .stream() + .map(order -> { + String property = order.getProperty(); + if (identity != null && identity.getName().contains(property)) { + property = MongoUtils.ID; + } + return order.isAscending() ? Sorts.ascending(property) : Sorts.descending(property); + }) + .collect(Collectors.collectingAndThen(Collectors.toList(), Sorts::orderBy)); + } + @Override public MongoUpdate getUpdateMany() { return mongoStoredQuery.getUpdateMany(defaultPreparedQuery.getContext()); @@ -114,28 +163,46 @@ public PreparedQuery getPreparedQueryDelegate() { return defaultPreparedQuery; } - private int applyPageable(Pageable pageable, List pipeline) { - int limit = 0; - if (pageable != Pageable.UNPAGED) { - if (pageable.getMode() != Mode.OFFSET) { - throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + private void applyPageable(Pageable pageable, List pipeline) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + } + applyPageable(pageable.getLimit(), pageable.getSort(), pipeline); + } + + private void applyPageable(Limit queryLimit, Sort sort, List pipeline) { + if (sort.isSorted()) { + BsonDocument existingSortBson = null; + for (Bson p : pipeline) { + BsonDocument sortBsonDocument = p.toBsonDocument(); + if (sortBsonDocument != null) { + BsonValue bsonValue = sortBsonDocument.get("$sort"); + if (bsonValue != null) { + existingSortBson = bsonValue.asDocument(); + if (existingSortBson != null) { + break; + } + } + } } - int skip = (int) pageable.getOffset(); - limit = pageable.getSize(); - Sort pageableSort = pageable.getSort(); - if (pageableSort.isSorted()) { - Bson sort = pageableSort.getOrderBy().stream().map(order -> order.isAscending() ? Sorts.ascending(order.getProperty()) : Sorts.descending(order.getProperty())).collect(Collectors.collectingAndThen(Collectors.toList(), Sorts::orderBy)); - BsonDocument sortStage = new BsonDocument().append("$sort", sort.toBsonDocument()); + Bson sortBson = getSort(sort); + if (existingSortBson != null) { + existingSortBson.putAll(sortBson.toBsonDocument()); + } else { + BsonDocument sortStage = new BsonDocument().append("$sort", sortBson.toBsonDocument()); addStageToPipelineBefore(pipeline, sortStage, "$limit", "$skip"); } - if (skip > 0) { - pipeline.add(new BsonDocument().append("$skip", new BsonInt32(skip))); + } + if (queryLimit.isLimited()) { + int offset = (int) queryLimit.offset(); + if (offset > 0) { + pipeline.add(new BsonDocument().append("$skip", new BsonInt32(offset))); } - if (limit > 0) { - pipeline.add(new BsonDocument().append("$limit", new BsonInt32(limit))); + int maxResults = queryLimit.maxResults(); + if (maxResults > 0) { + pipeline.add(new BsonDocument().append("$limit", new BsonInt32(maxResults))); } } - return limit; } private void addStageToPipelineBefore(List pipeline, BsonDocument stageToAdd, String... beforeStages) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java index 6c581812a3..cfea192000 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java @@ -42,8 +42,11 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanProperty; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.data.connection.ConnectionDefinition; +import io.micronaut.data.connection.ConnectionStatus; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.exceptions.NonUniqueResultException; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Mode; @@ -246,6 +249,14 @@ public Stream findStream(PagedQuery query) { @Override public Page findPage(PagedQuery query) { + if (query instanceof PreparedQuery pg) { + PreparedQuery preparedQuery = (PreparedQuery) pg; + return Page.of( + CollectionUtils.iterableToList(findAll(preparedQuery)), + query.getPageable(), + -1L + ); + } throw new DataAccessException("Not supported!"); } @@ -256,23 +267,27 @@ public Iterable findAll(PreparedQuery preparedQuery) { @Override public Stream findStream(PreparedQuery preparedQuery) { - return withClientSession(clientSession -> { - MongoIterable iterable = (MongoIterable) findAll(clientSession, getMongoPreparedQuery(preparedQuery), true); - MongoCursor iterator = iterable.iterator(); - Spliterators.AbstractSpliterator spliterator = new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, - Spliterator.ORDERED | Spliterator.IMMUTABLE) { - @Override - public boolean tryAdvance(Consumer action) { - if (iterator.hasNext()) { - action.accept(iterator.next()); - return true; - } - iterator.close(); - return false; + Optional> connectionStatus = connectionOperations.findConnectionStatus(); + if (connectionStatus.isEmpty()) { + return withClientSession(clientSession -> CollectionUtils.iterableToList( + findAll(clientSession, getMongoPreparedQuery(preparedQuery), false) + ).stream()); + } + MongoIterable iterable = (MongoIterable) findAll(connectionStatus.get().getConnection(), getMongoPreparedQuery(preparedQuery), true); + MongoCursor iterator = iterable.iterator(); + Spliterators.AbstractSpliterator spliterator = new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, + Spliterator.ORDERED | Spliterator.IMMUTABLE) { + @Override + public boolean tryAdvance(Consumer action) { + if (iterator.hasNext()) { + action.accept(iterator.next()); + return true; } - }; - return StreamSupport.stream(spliterator, false).onClose(iterator::close); - }); + iterator.close(); + return false; + } + }; + return StreamSupport.stream(spliterator, false).onClose(iterator::close); } private Iterable findAll(ClientSession clientSession, MongoPreparedQuery preparedQuery, boolean stream) { @@ -286,16 +301,15 @@ private Iterable findAll(ClientSession clientSession, MongoPreparedQue } private R findOneFiltered(ClientSession clientSession, MongoPreparedQuery preparedQuery) { - return find(clientSession, preparedQuery) - .limit(1) - .map(r -> { - Class type = preparedQuery.getRootEntity(); - RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity(); - if (type.isInstance(r)) { - return (R) triggerPostLoad(preparedQuery.getAnnotationMetadata(), persistentEntity, type.cast(r)); - } - return r; - }).first(); + return findOne(find(clientSession, preparedQuery) + .map(r -> { + Class type = preparedQuery.getRootEntity(); + RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity(); + if (type.isInstance(r)) { + return (R) triggerPostLoad(preparedQuery.getAnnotationMetadata(), persistentEntity, type.cast(r)); + } + return r; + })); } private R findOneAggregated(ClientSession clientSession, MongoPreparedQuery preparedQuery) { @@ -306,13 +320,26 @@ private R findOneAggregated(ClientSession clientSession, MongoPreparedQue BsonDocument result = aggregate(clientSession, preparedQuery, BsonDocument.class).first(); return convertResult(database.getCodecRegistry(), resultType, result, preparedQuery.isDtoProjection()); } - return aggregate(clientSession, preparedQuery).map(r -> { + return findOne(aggregate(clientSession, preparedQuery).map(r -> { RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity(); if (type.isInstance(r)) { return (R) triggerPostLoad(preparedQuery.getAnnotationMetadata(), persistentEntity, type.cast(r)); } return r; - }).first(); + })); + } + + private static R findOne(MongoIterable iterable) { + try (MongoCursor iterator = iterable.iterator()) { + if (iterator.hasNext()) { + R result = iterator.next(); + if (iterator.hasNext()) { + throw new NonUniqueResultException(); + } + return result; + } + return null; + } } private Iterable findAllAggregated(ClientSession clientSession, 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 dda8e26a8c..c0675d7f79 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 @@ -27,7 +27,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.Query; -import io.micronaut.data.document.model.query.builder.MongoQueryBuilder; +import io.micronaut.data.document.model.query.builder.MongoQueryBuilder2; import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.model.PersistentPropertyPath; @@ -50,10 +50,14 @@ import io.micronaut.data.runtime.query.internal.DelegateStoredQuery; import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonDocumentWrapper; +import org.bson.BsonDouble; import org.bson.BsonInt32; +import org.bson.BsonInt64; import org.bson.BsonObjectId; import org.bson.BsonRegularExpression; import org.bson.BsonValue; +import org.bson.codecs.Encoder; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.types.ObjectId; @@ -74,15 +78,15 @@ /** * Default implementation of {@link MongoStoredQuery}. * - * @param The entity type - * @param The result type + * @param The entity type + * @param The result type * @author Denis Stepanov * @since 3.3. */ @Internal final class DefaultMongoStoredQuery extends DefaultBindableParametersStoredQuery implements DelegateStoredQuery, MongoStoredQuery { - private static final Pattern MONGO_PARAM_PATTERN = Pattern.compile("\\W*(\\" + MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER + ":(\\d)+)\\W*"); + private static final Pattern MONGO_PARAM_PATTERN = Pattern.compile("\\W*(\\" + MongoQueryBuilder2.QUERY_PARAMETER_PLACEHOLDER + ":(\\d)+)\\W*"); private static final Logger LOG = LoggerFactory.getLogger(DefaultMongoStoredQuery.class); private static final BsonDocument EMPTY = new BsonDocument(); @@ -105,12 +109,12 @@ final class DefaultMongoStoredQuery extends DefaultBindableParametersStore ConversionService conversionService, RuntimePersistentEntity persistentEntity) { this(storedQuery, - codecRegistry, - attributeConverterRegistry, - runtimeEntityRegistry, - conversionService, - persistentEntity, - storedQuery.getAnnotationMetadata().stringValue(Query.class, "update").orElse(null)); + codecRegistry, + attributeConverterRegistry, + runtimeEntityRegistry, + conversionService, + persistentEntity, + storedQuery.getAnnotationMetadata().stringValue(Query.class, "update").orElse(null)); } DefaultMongoStoredQuery(StoredQuery storedQuery, @@ -120,7 +124,7 @@ final class DefaultMongoStoredQuery extends DefaultBindableParametersStore ConversionService conversionService, RuntimePersistentEntity persistentEntity, String updateJson) { - super(storedQuery, persistentEntity); + super(storedQuery, persistentEntity, conversionService); this.storedQuery = storedQuery; this.codecRegistry = codecRegistry; this.attributeConverterRegistry = attributeConverterRegistry; @@ -159,9 +163,9 @@ final class DefaultMongoStoredQuery extends DefaultBindableParametersStore if (operationType == OperationType.DELETE) { String query = storedQuery.getQuery(); deleteData = new DeleteData( - StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query), - getParameterInRole(MongoRoles.FILTER_ROLE), - getParameterInRole(MongoRoles.DELETE_OPTIONS_ROLE) + StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query), + getParameterInRole(MongoRoles.FILTER_ROLE), + getParameterInRole(MongoRoles.DELETE_OPTIONS_ROLE) ); } else { deleteData = null; @@ -173,10 +177,10 @@ final class DefaultMongoStoredQuery extends DefaultBindableParametersStore } String query = storedQuery.getQuery(); updateData = new UpdateData( - BsonDocument.parse(updateJson), StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query), - getParameterInRole(MongoRoles.FILTER_ROLE), - getParameterInRole(MongoRoles.UPDATE_ROLE), - getParameterInRole(MongoRoles.UPDATE_OPTIONS_ROLE) + BsonDocument.parse(updateJson), StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query), + getParameterInRole(MongoRoles.FILTER_ROLE), + getParameterInRole(MongoRoles.UPDATE_ROLE), + getParameterInRole(MongoRoles.UPDATE_OPTIONS_ROLE) ); } else { updateData = null; @@ -314,7 +318,7 @@ private boolean needsProcessing(List values) { private boolean needsProcessingValue(BsonValue value) { if (value instanceof BsonDocument bsonDocument) { - BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER, null); + BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder2.QUERY_PARAMETER_PLACEHOLDER, null); if (queryParameterIndex != null) { return true; } @@ -413,7 +417,7 @@ public void bindMany(QueryParameterBinding binding, Collection values) { 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); + BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder2.QUERY_PARAMETER_PLACEHOLDER, null); if (queryParameterIndex != null) { int index = queryParameterIndex.getValue(); QueryParameterBinding queryParameterBinding = getQueryBindings().get(index); @@ -421,7 +425,78 @@ private BsonValue replaceQueryParametersInBsonValue(BsonValue value, @Nullable I if (e == null) { throw new DataAccessException("Cannot bind a value at index: " + index); } - return getValue(e.getKey(), e.getValue()); + BsonValue bsonValue = getValue(e.getKey(), e.getValue()); + if (bsonDocument.containsKey(MongoQueryBuilder2.NEGATE)) { + if (bsonValue instanceof BsonDocumentWrapper bsonDocumentWrapper) { + Object object = bsonDocumentWrapper.getWrappedDocument(); + if (object instanceof Long aLong) { + return new BsonDocumentWrapper<>( + -aLong, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Integer integer) { + return new BsonDocumentWrapper<>( + -integer, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Double aDouble) { + return new BsonDocumentWrapper<>( + -aDouble, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Float aFloat) { + return new BsonDocumentWrapper<>( + -aFloat, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + } + return switch (bsonValue.getBsonType()) { + case INT32 -> new BsonInt32(-bsonValue.asInt32().getValue()); + case INT64 -> new BsonInt64(-bsonValue.asInt64().getValue()); + case DOUBLE -> new BsonDouble(-bsonValue.asDouble().getValue()); + default -> bsonValue; + }; + } + if (bsonDocument.containsKey(MongoQueryBuilder2.RECIPROCATE)) { + if (bsonValue instanceof BsonDocumentWrapper bsonDocumentWrapper) { + Object object = bsonDocumentWrapper.getWrappedDocument(); + if (object instanceof Long aLong) { + return new BsonDocumentWrapper<>( + 1L / aLong, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Integer integer) { + return new BsonDocumentWrapper<>( + 1 / integer, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Double aDouble) { + return new BsonDocumentWrapper<>( + 1D / aDouble, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + if (object instanceof Float aFloat) { + return new BsonDocumentWrapper<>( + 1F / aFloat, + (Encoder) bsonDocumentWrapper.getEncoder() + ); + } + } + return switch (bsonValue.getBsonType()) { + case INT32 -> new BsonInt32(1 / bsonValue.asInt32().getValue()); + case INT64 -> new BsonInt64(1L / bsonValue.asInt64().getValue()); + case DOUBLE -> new BsonDouble(1D / bsonValue.asDouble().getValue()); + default -> bsonValue; + }; + } + return bsonValue; } for (Map.Entry entry : bsonDocument.entrySet()) { BsonValue bsonValue = entry.getValue(); @@ -458,7 +533,7 @@ private BsonValue replaceQueryParametersInBsonValue(BsonValue value, @Nullable I String queryParamIndexStr = matcher.group(2); queryParamIndex = Integer.parseInt(queryParamIndexStr); } catch (Exception e) { - LOG.info("Failed to get mongo parameter for regex {}", e); + LOG.info("Failed to get mongo parameter for regex {}", pattern, e); } if (queryParamIndex != null) { QueryParameterBinding queryParameterBinding = getQueryBindings().get(queryParamIndex); @@ -467,7 +542,14 @@ private BsonValue replaceQueryParametersInBsonValue(BsonValue value, @Nullable I throw new DataAccessException("Cannot bind a value at index: " + queryParamIndex); } pattern = pattern.replace(matcher.group(1), e.getValue().toString()); - return new BsonRegularExpression(pattern, bsonRegularExpression.getOptions()); + String options = bsonRegularExpression.getOptions(); + if (options.contains("l")) { + pattern = pattern + .replace("_", ".") + .replace("%", ".*"); + options = options.replace("l", ""); + } + return new BsonRegularExpression(pattern, options); } } } @@ -642,9 +724,9 @@ private void copyNonNullFrom(UpdateOptions to, UpdateOptions from) { public MongoUpdate getUpdateMany(InvocationContext invocationContext) { return new MongoUpdate( - getUpdate(invocationContext, null), - getFilter(invocationContext, null), - getOptions(invocationContext)); + getUpdate(invocationContext, null), + getFilter(invocationContext, null), + getOptions(invocationContext)); } public MongoUpdate getUpdateOne(E entity) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultReactiveMongoRepositoryOperations.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultReactiveMongoRepositoryOperations.java index 776d2ab4aa..c5f7c55dab 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultReactiveMongoRepositoryOperations.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultReactiveMongoRepositoryOperations.java @@ -202,6 +202,16 @@ public Mono findOptional(PreparedQuery preparedQuery) { @Override public Mono> findPage(PagedQuery pagedQuery) { + if (pagedQuery instanceof PreparedQuery pg) { + PreparedQuery preparedQuery = (PreparedQuery) pg; + return findAll(preparedQuery) + .collectList() + .map(content -> Page.of( + content, + pagedQuery.getPageable(), + -1L + )); + } throw new DataAccessException("Not supported!"); } 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 9d015f666c..34aff53ee1 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 @@ -176,8 +176,8 @@ class MongoCriteriaSpec extends Specification { } as Specification, ] expectedWhereQuery << [ - '{$and:[{enabled:{$gte:{$mn_qp:0}}},{enabled:{$lte:{$mn_qp:1}}}]}', - '{$and:[{amount:{$gte:{$mn_qp:0}}},{amount:{$lte:{$mn_qp:1}}}]}', + '{enabled:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', + '{amount:{$gte:{$mn_qp:0},$lte:{$mn_qp:1}}}', '{enabled:{$eq:true}}', '[{$match:{enabled:{$eq:true}}},{$sort:{amount:-1,budget:1}}]', '{enabled:{$eq:true}}', diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoDocumentRepositorySpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoDocumentRepositorySpec.groovy index 888cde4f07..aed267e073 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoDocumentRepositorySpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoDocumentRepositorySpec.groovy @@ -39,6 +39,7 @@ import io.micronaut.data.document.tck.repositories.DomainEventsRepository import io.micronaut.data.document.tck.repositories.SaleRepository import io.micronaut.data.document.tck.repositories.StudentRepository import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions import io.micronaut.data.mongodb.operations.options.MongoFindOptions import io.micronaut.data.repository.jpa.criteria.QuerySpecification @@ -54,6 +55,19 @@ class MongoDocumentRepositorySpec extends AbstractDocumentRepositorySpec impleme MongoClient mongoClient = context.getBean(MongoClient) + void "test between"() { + given: + savePersons(["A", "B", "C", "D", "E", "F"]) + when: + def peopleBetween = personRepository.findAllByNameBetween("B", "E").collect { it.name} + then: + peopleBetween == ["B", "C", "D", "E"] + when: + def peopleNotBetween = personRepository.findAllByNameNotBetween("B", "E").collect { it.name} + then: + peopleNotBetween == ["A", "F"] + } + void "test id mapping"() { given: savePersons(["Dennis", "Jeff", "James", "Dennis"]) diff --git a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/repositories/MongoPersonRepository.java b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/repositories/MongoPersonRepository.java index dfd0ce62c2..d38381a0bc 100644 --- a/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/repositories/MongoPersonRepository.java +++ b/data-mongodb/src/test/java/io/micronaut/data/document/mongodb/repositories/MongoPersonRepository.java @@ -26,6 +26,10 @@ public interface MongoPersonRepository extends PersonRepository { List queryAll(); + List findAllByNameBetween(String from, String to); + + List findAllByNameNotBetween(String from, String to); + @MongoFindQuery(filter = "{name:{$regex: :t}}", sort = "{ name : 1 }", project = "{ name: 1}") List customFind(String t); diff --git a/data-mongodb/src/test/resources/logback.xml b/data-mongodb/src/test/resources/logback.xml index 9610e60fed..ee48058ee1 100644 --- a/data-mongodb/src/test/resources/logback.xml +++ b/data-mongodb/src/test/resources/logback.xml @@ -6,6 +6,8 @@ + + diff --git a/data-processor/build.gradle b/data-processor/build.gradle index bdeda8269b..bd319ac169 100644 --- a/data-processor/build.gradle +++ b/data-processor/build.gradle @@ -1,11 +1,14 @@ plugins { id "io.micronaut.build.internal.data-module" + id 'antlr' } dependencies { api projects.micronautDataModel api(mnSql.jakarta.persistence.api) + implementation libs.antlr.runtime + compileOnly mn.micronaut.core.processor testAnnotationProcessor mn.micronaut.inject.java @@ -21,4 +24,11 @@ dependencies { testImplementation projects.micronautDataTck testImplementation projects.micronautDataTx testImplementation projects.micronautDataJdbc + + antlr libs.antlr +} + +generateGrammarSource { + outputDirectory = file("$outputDirectory/io/micronaut/data/jdql") + arguments += ["-package", "io.micronaut.data.jdql"] } diff --git a/data-processor/src/main/antlr/JDQL.g4 b/data-processor/src/main/antlr/JDQL.g4 new file mode 100644 index 0000000000..be98ad8c4e --- /dev/null +++ b/data-processor/src/main/antlr/JDQL.g4 @@ -0,0 +1,163 @@ +grammar JDQL; + +// Copied from https://github.com/eclipse-jnosql/jnosql/blob/58917799e77bca640f73fc8cecc1fb7815ea5ba5/jnosql-communication/jnosql-communication-query/antlr4/org/eclipse/jnosql/query/grammar/data/JDQL.g4 + +statement : select_statement | update_statement | delete_statement; + +select_statement : select_clause? from_clause? where_clause? orderby_clause?; +update_statement : UPDATE entity_name set_clause where_clause?; +delete_statement : DELETE from_clause where_clause?; + +from_clause : FROM entity_name; + +where_clause : WHERE conditional_expression; + +set_clause : SET update_item (COMMA update_item)*; +update_item : state_field_path_expression EQ (scalar_expression | NULL); + +select_clause : SELECT select_list; +select_list + : state_field_path_expression (COMMA state_field_path_expression)* + | aggregate_expression + ; +aggregate_expression : COUNT '(' THIS ')'; + +orderby_clause : ORDER BY orderby_item (COMMA orderby_item)*; +orderby_item : state_field_path_expression (ASC | DESC)?; + +conditional_expression + // highest to lowest precedence + : LPAREN conditional_expression RPAREN + | null_comparison_expression + | in_expression + | between_expression + | like_expression + | comparison_expression + | NOT conditional_expression + | conditional_expression AND conditional_expression + | conditional_expression OR conditional_expression + ; + +comparison_expression : scalar_expression comparison_operator scalar_expression; +comparison_operator : EQ | GT | GTEQ | LT | LTEQ | NEQ; + +between_expression : scalar_expression NOT? BETWEEN scalar_expression AND scalar_expression; +like_expression : scalar_expression NOT? LIKE (STRING | input_parameter); + +in_expression : state_field_path_expression NOT? IN '(' in_item (',' in_item)* ')'; +in_item : literal | enum_literal | input_parameter; // could simplify to just literal + +null_comparison_expression : state_field_path_expression IS NOT? NULL; + +scalar_expression + // highest to lowest precedence + : LPAREN scalar_expression RPAREN + | primary_expression + | scalar_expression MUL scalar_expression + | scalar_expression DIV scalar_expression + | scalar_expression PLUS scalar_expression + | scalar_expression MINUS scalar_expression + | scalar_expression CONCAT scalar_expression + ; + +primary_expression + : function_expression + | special_expression + | state_field_path_expression + | enum_literal + | input_parameter + | literal + ; + +function_expression + : ('abs(' | 'ABS(') scalar_expression ')' + | ('length(' | 'LENGTH(') scalar_expression ')' + | ('lower(' | 'LOWER(') scalar_expression ')' + | ('upper(' | 'UPPER(') scalar_expression ')' + | ('left(' | 'LEFT(') scalar_expression ',' scalar_expression ')' + | ('right(' | 'RIGHT(') scalar_expression ',' scalar_expression ')' + ; + +special_expression + : LOCAL_DATE + | LOCAL_DATETIME + | LOCAL_TIME + | TRUE + | FALSE + ; + +state_field_path_expression : IDENTIFIER (DOT IDENTIFIER)* | FULLY_QUALIFIED_IDENTIFIER; + +entity_name : IDENTIFIER; // no ambiguity + +enum_literal : IDENTIFIER (DOT IDENTIFIER)* | FULLY_QUALIFIED_IDENTIFIER; // ambiguity with state_field_path_expression resolvable semantically + +input_parameter : COLON IDENTIFIER | QUESTION INTEGER; + +literal : STRING | INTEGER | DOUBLE | FLOAT; + +// Tokens defined to be case-insensitive using character classes +SELECT : [sS][eE][lL][eE][cC][tT]; +UPDATE : [uU][pP][dD][aA][tT][eE]; +DELETE : [dD][eE][lL][eE][tT][eE]; +FROM : [fF][rR][oO][mM]; +WHERE : [wW][hH][eE][rR][eE]; +SET : [sS][eE][tT]; +ORDER : [oO][rR][dD][eE][rR]; +BY : [bB][yY]; +NOT : [nN][oO][tT]; +IN : [iI][nN]; +IS : [iI][sS]; +NULL : [nN][uU][lL][lL]; +COUNT : [cC][oO][uU][nN][tT]; +TRUE : [tT][rR][uU][eE]; +FALSE : [fF][aA][lL][sS][eE]; +ASC : [aA][sS][cC]; +DESC : [dD][eE][sS][cC]; +AND : [aA][nN][dD]; +OR : [oO][rR]; +LOCAL_DATE : [lL][oO][cC][aA][lL] [dD][aA][tT][eE]; +LOCAL_DATETIME : [lL][oO][cC][aA][lL] [dD][aA][tT][eE][tT][iI][mM][eE]; +LOCAL_TIME : [lL][oO][cC][aA][lL] [tT][iI][mM][eE]; +BETWEEN : [bB][eE][tT][wW][eE][eE][nN]; +LIKE : [lL][iI][kK][eE]; +THIS : [tT][hH][iI][sS]; +LOCAL : [lL][oO][cC][aA][lL]; +DATE : [dD][aA][tT][eE]; +DATETIME : [dD][aA][tT][eE][tT][iI][mM][eE]; +TIME : [tT][iI][mM][eE]; + +// Operators +EQ : '='; +GT : '>'; +LT : '<'; +NEQ : '<>'; +GTEQ : '>='; +LTEQ : '<='; +PLUS : '+'; +MINUS : '-'; +MUL : '*'; +DIV : '/'; +CONCAT : '||'; + +// Special Characters +COMMA : ','; +DOT : '.'; +LPAREN : '('; +RPAREN : ')'; +COLON : ':'; +QUESTION : '?'; + +// Identifier and literals +FULLY_QUALIFIED_IDENTIFIER : [a-zA-Z_][a-zA-Z0-9_]* (DOT [a-zA-Z_][a-zA-Z0-9_]*)+; +IDENTIFIER : [a-zA-Z_][a-zA-Z0-9_]*; +STRING : '\'' ( ~('\'' | '\\') | '\\' . | '\'\'' )* '\'' // single quoted strings with embedded single quotes handled + | '"' ( ~["\\] | '\\' . )* '"' ; // double quoted strings +INTEGER : '-'?[0-9]+; +DOUBLE : '-'?[0-9]+'.'[0-9]*?'d' | '-'?'.'[0-9]+?'d'; +FLOAT : '-'?[0-9]+'.'[0-9]*?'f' | '-'?'.'[0-9]+?'f'; + +// Whitespace and Comments +WS : [ \t\r\n]+ -> skip ; +LINE_COMMENT : '//' ~[\r\n]* -> skip; +BLOCK_COMMENT : '/*' .*? '*/' -> skip; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/jdql/JDQLCriteriaBuilderUtils.java b/data-processor/src/main/java/io/micronaut/data/processor/jdql/JDQLCriteriaBuilderUtils.java new file mode 100644 index 0000000000..313f770e38 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/jdql/JDQLCriteriaBuilderUtils.java @@ -0,0 +1,583 @@ +/* + * Copyright 2017-2025 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.processor.jdql; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.OrderBy; +import io.micronaut.data.jdql.JDQLBaseListener; +import io.micronaut.data.jdql.JDQLParser; +import io.micronaut.data.model.PersistentEntity; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCommonAbstractCriteria; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaUpdate; +import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder; +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaDelete; +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaQuery; +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaUpdate; +import io.micronaut.data.processor.visitors.MatchFailedException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MethodElement; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Order; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Selection; +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonToken; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.InputMismatchException; +import org.antlr.v4.runtime.NoViableAltException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.atn.ATNConfigSet; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; + +/** + * The utils to generate Criteria queries from Jakarta Data Query Language statements. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JDQLCriteriaBuilderUtils { + + private JDQLCriteriaBuilderUtils() { + } + + public static PersistentEntityCommonAbstractCriteria build(String query, + PersistentEntity rootPersistentEntity, + MethodElement methodElement, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder) { + + ParseTree child = parse(query, methodElement); + if (child instanceof io.micronaut.data.jdql.JDQLParser.Delete_statementContext deleteStatementContext) { + return JDQLCriteriaBuilderUtils.buildDelete(deleteStatementContext, classElementResolver, criteriaBuilder); + } + if (child instanceof JDQLParser.Update_statementContext updateStatementContext) { + return JDQLCriteriaBuilderUtils.buildUpdate(updateStatementContext, classElementResolver, criteriaBuilder); + } + if (child instanceof JDQLParser.Select_statementContext select_clauseContext) { + return JDQLCriteriaBuilderUtils.buildSelect(rootPersistentEntity, select_clauseContext, classElementResolver, criteriaBuilder, methodElement); + } + + throw new MatchFailedException("Unrecognized query: " + child.getParent(), methodElement); + } + + public static PersistentEntityCriteriaQuery buildCount(String query, + PersistentEntity rootPersistentEntity, + MethodElement methodElement, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder) { + + ParseTree child = parse(query, methodElement); + if (child instanceof JDQLParser.Select_statementContext select_clauseContext) { + return JDQLCriteriaBuilderUtils.buildCount(rootPersistentEntity, select_clauseContext, classElementResolver, criteriaBuilder); + } + + throw new MatchFailedException("Unrecognized count query: " + child.getParent(), methodElement); + } + + private static ParseTree parse(String query, Element originatingElement) { + var inputStream = CharStreams.fromString(query); + var lexer = new io.micronaut.data.jdql.JDQLLexer(inputStream); + ANTLRErrorListener errorListener = new ANTLRErrorListener() { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + throw new MatchFailedException("Failed to parse Jakarta Data query: " + prettifyAntlrError(offendingSymbol, line, charPositionInLine, msg, e, query), originatingElement); + } + + @Override + public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { + } + + @Override + public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { + } + + @Override + public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { + } + }; + var tokenStream = new CommonTokenStream(lexer); + var parser = new JDQLParser(tokenStream); + lexer.removeErrorListeners(); + parser.removeErrorListeners(); + lexer.addErrorListener(errorListener); + parser.addErrorListener(errorListener); + JDQLParser.StatementContext statement = parser.statement(); + return statement.getChild(0); + } + + private static String prettifyAntlrError(Object offendingSymbol, + int line, + int charPositionInLine, + String message, + RecognitionException e, + String query) { + String errorText = "At " + line + ":" + charPositionInLine; + if (offendingSymbol instanceof CommonToken commonToken) { + String token = commonToken.getText(); + if (token != null && !token.isEmpty()) { + errorText += " and token '" + token + "'"; + } + } + errorText += ", "; + if (e instanceof NoViableAltException) { + errorText += message.substring(0, message.indexOf('\'')); + if (query.isEmpty()) { + errorText += "'*' (empty query string)"; + } else { + String lineText = query.lines().toList().get(line - 1); + String text = lineText.substring(0, charPositionInLine) + "*" + lineText.substring(charPositionInLine); + errorText += "'" + text + "'"; + } + } else if (e instanceof InputMismatchException) { + errorText += message.substring(0, message.length() - 1) + .replace(" expecting {", ", expecting one of the following tokens: "); + } else { + errorText += message; + } + return errorText; + } + + public static PersistentEntityCriteriaQuery buildSelect(PersistentEntity rootPersistentEntity, + JDQLParser.Select_statementContext selectStatementContext, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder, + MethodElement methodElement) { + + SourcePersistentEntityCriteriaQuery query = criteriaBuilder + .createQuery(null); + PersistentEntityRoot root; + JDQLParser.From_clauseContext fromClauseContext = selectStatementContext.from_clause(); + if (fromClauseContext != null) { + String entityName = fromClauseContext.entity_name().getText(); + root = query.from(classElementResolver.apply(entityName)); + } else { + root = query.from(rootPersistentEntity); + } + Predicate predicate = getPredicate(selectStatementContext.where_clause(), root, criteriaBuilder); + if (predicate != null) { + query.where(predicate); + } + query.orderBy( + getOrders(selectStatementContext.orderby_clause(), root, criteriaBuilder, methodElement) + ); + JDQLParser.Select_clauseContext selectClauseContext = selectStatementContext.select_clause(); + if (selectClauseContext != null) { + JDQLParser.Select_listContext selectList = selectClauseContext.select_list(); + JDQLParser.Aggregate_expressionContext aggregateExpression = selectList.aggregate_expression(); + if (aggregateExpression != null) { + query.select(criteriaBuilder.count(root)); + } else { + query.multiselect( + selectList.state_field_path_expression().stream() + .>map(s -> getExpression(s, root, criteriaBuilder)) + .toList() + ); + } + } + return query; + } + + public static PersistentEntityCriteriaQuery buildCount(PersistentEntity rootPersistentEntity, + JDQLParser.Select_statementContext selectStatementContext, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder) { + + SourcePersistentEntityCriteriaQuery query = criteriaBuilder + .createQuery(null); + PersistentEntityRoot root; + JDQLParser.From_clauseContext fromClauseContext = selectStatementContext.from_clause(); + if (fromClauseContext != null) { + String entityName = fromClauseContext.entity_name().getText(); + root = query.from(classElementResolver.apply(entityName)); + } else { + root = query.from(rootPersistentEntity); + } + Predicate predicate = getPredicate(selectStatementContext.where_clause(), root, criteriaBuilder); + if (predicate != null) { + query.where(predicate); + } + query.select(criteriaBuilder.count(root)); + return query; + } + + public static PersistentEntityCriteriaUpdate buildUpdate(JDQLParser.Update_statementContext updateStatementContext, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder) { + String entityName = updateStatementContext.entity_name().getText(); + + JDQLParser.Where_clauseContext whereClauseContext = updateStatementContext.where_clause(); + + SourcePersistentEntityCriteriaUpdate updateQuery = criteriaBuilder + .createCriteriaUpdate(null); + PersistentEntityRoot root = updateQuery.from(classElementResolver.apply(entityName)); + Predicate predicate = getPredicate(whereClauseContext, root, criteriaBuilder); + if (predicate != null) { + updateQuery.where(predicate); + } + + ParseTreeWalker.DEFAULT.walk(new JDQLBaseListener() { + + @Override + public void exitUpdate_item(JDQLParser.Update_itemContext ctx) { + String name = ctx.state_field_path_expression().getText(); + Expression expression = getExpression(ctx.scalar_expression(), root, criteriaBuilder); + updateQuery.set(name, expression); + } + + }, updateStatementContext); + + return updateQuery; + } + + public static PersistentEntityCriteriaDelete buildDelete(JDQLParser.Delete_statementContext deleteStatementContext, + Function classElementResolver, + SourcePersistentEntityCriteriaBuilder criteriaBuilder) { + + JDQLParser.From_clauseContext fromClauseContext = deleteStatementContext.from_clause(); + String entityName = fromClauseContext.entity_name().getText(); + JDQLParser.Where_clauseContext whereClauseContext = deleteStatementContext.where_clause(); + + SourcePersistentEntityCriteriaDelete deleteQuery = criteriaBuilder + .createCriteriaDelete(null); + PersistentEntityRoot root = deleteQuery.from(classElementResolver.apply(entityName)); + Predicate predicate = getPredicate(whereClauseContext, root, criteriaBuilder); + if (predicate != null) { + deleteQuery.where(predicate); + } + return deleteQuery; + } + + private static Predicate getPredicate(@Nullable JDQLParser.Where_clauseContext whereClause, + Root root, + PersistentEntityCriteriaBuilder criteriaBuilder) { + if (whereClause == null) { + return null; + } + JDQLParser.Conditional_expressionContext conditionalExpression = whereClause.conditional_expression(); + return getPredicate(conditionalExpression, root, criteriaBuilder); + } + + private static List getOrders(@Nullable JDQLParser.Orderby_clauseContext orderByClause, + Root root, + PersistentEntityCriteriaBuilder criteriaBuilder, + MethodElement methodElement) { + List orders = new ArrayList<>(); + if (orderByClause != null) { + List orderbyItemContexts = orderByClause.orderby_item(); + for (JDQLParser.Orderby_itemContext orderbyItemContext : orderbyItemContexts) { + Expression expression = getExpression(orderbyItemContext.state_field_path_expression(), root, criteriaBuilder); + orders.add( + orderbyItemContext.DESC() == null ? criteriaBuilder.asc(expression) : criteriaBuilder.desc(expression) + ); + } + } + if (methodElement != null) { + for (AnnotationValue av : methodElement.getAnnotationValuesByStereotype(OrderBy.class.getName())) { + orders.add(criteriaBuilder.sort( + root.get(av.stringValue().orElseThrow()), + !av.booleanValue("descending").orElse(false), + av.booleanValue("ignoreCase").orElse(false) + )); + } + } + return orders; + } + + private static Predicate getPredicate(JDQLParser.Conditional_expressionContext conditionalExpression, + Root root, + PersistentEntityCriteriaBuilder criteriaBuilder) { + if (conditionalExpression.LPAREN() != null) { + return getPredicate(conditionalExpression.conditional_expression(0), root, criteriaBuilder); + } + if (conditionalExpression.AND() != null) { + return criteriaBuilder.and( + getPredicate(conditionalExpression.conditional_expression(0), root, criteriaBuilder), + getPredicate(conditionalExpression.conditional_expression(1), root, criteriaBuilder) + ); + } + if (conditionalExpression.OR() != null) { + return criteriaBuilder.or( + getPredicate(conditionalExpression.conditional_expression(0), root, criteriaBuilder), + getPredicate(conditionalExpression.conditional_expression(1), root, criteriaBuilder) + ); + } + if (conditionalExpression.NOT() != null) { + return criteriaBuilder.not( + getPredicate(conditionalExpression.conditional_expression(0), root, criteriaBuilder) + ); + } + JDQLParser.Comparison_expressionContext comparisonExpression = conditionalExpression.comparison_expression(); + if (comparisonExpression != null) { + Expression firstExp = getExpression( + comparisonExpression.scalar_expression(0), + root, + criteriaBuilder + ); + Expression secondExp = getExpression( + comparisonExpression.scalar_expression(1), + root, + criteriaBuilder + ); + JDQLParser.Comparison_operatorContext comparisonOperator = comparisonExpression.comparison_operator(); + if (comparisonOperator.EQ() != null) { + return criteriaBuilder.equal(firstExp, secondExp); + } + if (comparisonOperator.NEQ() != null) { + return criteriaBuilder.notEqual(firstExp, secondExp); + } + if (comparisonOperator.GT() != null) { + return criteriaBuilder.greaterThan((Expression) firstExp, (Expression) secondExp); + } + if (comparisonOperator.GTEQ() != null) { + return criteriaBuilder.greaterThanOrEqualTo((Expression) firstExp, (Expression) secondExp); + } + if (comparisonOperator.LT() != null) { + return criteriaBuilder.lessThan((Expression) firstExp, (Expression) secondExp); + } + if (comparisonOperator.LTEQ() != null) { + return criteriaBuilder.lessThanOrEqualTo((Expression) firstExp, (Expression) secondExp); + } + throw new IllegalStateException("Unsupported comparison operator: " + comparisonOperator); + } + JDQLParser.Like_expressionContext likeExpression = conditionalExpression.like_expression(); + if (likeExpression != null) { + Expression pattern; + if (likeExpression.STRING() != null) { + pattern = criteriaBuilder.literal( + getString( + likeExpression.getChild(likeExpression.getChildCount() - 1).getText() + ) + ); + } else { + pattern = (Expression) getExpression(likeExpression.input_parameter(), criteriaBuilder); + } + Expression expression = (Expression) getExpression(likeExpression.scalar_expression(), root, criteriaBuilder); + if (likeExpression.NOT() != null) { + return criteriaBuilder.notLike(expression, pattern); + } + return criteriaBuilder.like(expression, pattern); + } + JDQLParser.In_expressionContext inExpression = conditionalExpression.in_expression(); + if (inExpression != null) { + Expression expression = getExpression(inExpression.state_field_path_expression(), root, criteriaBuilder); + CriteriaBuilder.In in = criteriaBuilder.in(expression); + for (JDQLParser.In_itemContext item : inExpression.in_item()) { + Expression e = getExpression(item, criteriaBuilder); + in.value(e); + } + if (inExpression.NOT() != null) { + return in.not(); + } + return in; + } + JDQLParser.Between_expressionContext betweenExpression = conditionalExpression.between_expression(); + if (betweenExpression != null) { + Predicate between = criteriaBuilder.between( + (Expression) getExpression(betweenExpression.scalar_expression(0), root, criteriaBuilder), + (Expression) getExpression(betweenExpression.scalar_expression(1), root, criteriaBuilder), + (Expression) getExpression(betweenExpression.scalar_expression(2), root, criteriaBuilder) + ); + if (betweenExpression.NOT() != null) { + return between.not(); + } + return between; + } + JDQLParser.Null_comparison_expressionContext nullComparisonExpression = conditionalExpression.null_comparison_expression(); + if (nullComparisonExpression != null) { + Expression expression = getExpression(nullComparisonExpression.state_field_path_expression(), root, criteriaBuilder); + if (nullComparisonExpression.NOT() != null) { + return criteriaBuilder.isNotNull(expression); + } + return criteriaBuilder.isNull(expression); + } + throw new IllegalStateException("Unsupported conditional expression: " + conditionalExpression); + } + + private static String getString(String text) { + return text.substring(1, text.length() - 1); + } + + private static Expression getExpression(JDQLParser.Scalar_expressionContext scalarExpression, + Root root, + CriteriaBuilder criteriaBuilder) { + JDQLParser.Primary_expressionContext primaryExpression = scalarExpression.primary_expression(); + if (primaryExpression != null) { + return getExpression(primaryExpression, root, criteriaBuilder); + } + Expression firstExp = getExpression( + Objects.requireNonNull((JDQLParser.Scalar_expressionContext) scalarExpression.getChild(0), "First expression cannot be null"), + root, + criteriaBuilder + ); + Expression secondExp = getExpression( + Objects.requireNonNull((JDQLParser.Scalar_expressionContext) scalarExpression.getChild(2), "First expression cannot be null"), + root, + criteriaBuilder + ); + if (scalarExpression.PLUS() != null) { + return criteriaBuilder.sum((Expression) firstExp, (Expression) secondExp); + } + if (scalarExpression.MINUS() != null) { + return criteriaBuilder.diff((Expression) firstExp, (Expression) secondExp); + } + if (scalarExpression.CONCAT() != null) { + return criteriaBuilder.concat((Expression) firstExp, (Expression) secondExp); + } + if (scalarExpression.MUL() != null) { + return criteriaBuilder.prod((Expression) firstExp, (Expression) secondExp); + } + if (scalarExpression.DIV() != null) { + return criteriaBuilder.quot((Expression) firstExp, (Expression) secondExp); + } + throw new IllegalStateException("Unknown primary expression"); + } + + private static Expression getExpression(JDQLParser.Primary_expressionContext context, + Root root, + CriteriaBuilder criteriaBuilder) { + if (context.literal() != null) { + return getExpression(context.literal(), criteriaBuilder); + } + if (context.input_parameter() != null) { + return getExpression(context.input_parameter(), criteriaBuilder); + } + if (context.special_expression() != null) { + var specialExpression = context.special_expression().getText(); + return switch (specialExpression.toUpperCase(Locale.US)) { + case "TRUE" -> criteriaBuilder.literal(true); + case "FALSE" -> criteriaBuilder.literal(false); + default -> + throw new UnsupportedOperationException("Unsupported special expression: " + specialExpression); + }; + } + if (context.enum_literal() != null) { + return getExpression(context.enum_literal()); + } + if (context.state_field_path_expression() != null) { + var stateContext = context.state_field_path_expression(); + return getExpression(stateContext, root, criteriaBuilder); + } + JDQLParser.Function_expressionContext functionExpression = context.function_expression(); + if (functionExpression != null) { + SourcePersistentEntityCriteriaBuilder sourcePersistentEntityCriteriaBuilder = (SourcePersistentEntityCriteriaBuilder) criteriaBuilder; + Expression expression = getExpression(functionExpression.scalar_expression(0), root, criteriaBuilder); + String functionName = functionExpression.getChild(0).getText().toLowerCase(); + return switch (functionName) { + case "abs(" -> criteriaBuilder.abs(expression); + case "length(" -> criteriaBuilder.length(expression); + case "lower(" -> criteriaBuilder.lower(expression); + case "upper(" -> criteriaBuilder.upper(expression); + case "left(" -> sourcePersistentEntityCriteriaBuilder.startsWithString( + expression, + (Expression) getExpression(functionExpression.scalar_expression(1), root, criteriaBuilder) + ); + case "right(" -> sourcePersistentEntityCriteriaBuilder.endingWithString( + expression, + (Expression) getExpression(functionExpression.scalar_expression(1), root, criteriaBuilder) + ); + default -> + throw new UnsupportedOperationException("Unsupported function expression: " + functionName); + }; + } + throw new UnsupportedOperationException("Not supported expression: " + context.getText()); + } + + private static Expression getExpression(JDQLParser.Enum_literalContext enumLiteralContext) { + throw new UnsupportedOperationException("Unsupported enum: " + enumLiteralContext); + } + + private static Expression getExpression(JDQLParser.LiteralContext literal, CriteriaBuilder criteriaBuilder) { + if (literal.STRING() != null) { + return criteriaBuilder.literal( + getString( + literal.STRING().getText() + ) + ); + } else if (literal.INTEGER() != null) { + return criteriaBuilder.literal(Integer.valueOf(literal.INTEGER().getText())); + } else if (literal.DOUBLE() != null) { + return criteriaBuilder.literal(Double.valueOf(literal.DOUBLE().getText())); + } else if (literal.FLOAT() != null) { + return criteriaBuilder.literal(Float.valueOf(literal.FLOAT().getText())); + } + throw new IllegalStateException("Unknown literal parameter: " + literal); + } + + private static Expression getExpression(JDQLParser.State_field_path_expressionContext stateFieldPathExpression, + Root root, + CriteriaBuilder criteriaBuilder) { + var text = stateFieldPathExpression.getText(); + if (stateFieldPathExpression.FULLY_QUALIFIED_IDENTIFIER() != null) { + return criteriaBuilder.literal(text); + } + return root.get(text); + } + + private static Expression getExpression(JDQLParser.Input_parameterContext inputParameter, + CriteriaBuilder criteriaBuilder) { + SourcePersistentEntityCriteriaBuilder sourcePersistentEntityCriteriaBuilder = (SourcePersistentEntityCriteriaBuilder) criteriaBuilder; + String text = inputParameter.getChild(0).getText(); + if (text.equals("?")) { + int parameterIndex = Integer.parseInt(inputParameter.getChild(1).getText()) - 1; + return sourcePersistentEntityCriteriaBuilder.parameterReferencingMethodParameter(parameterIndex); + } + if (text.equals(":")) { + return sourcePersistentEntityCriteriaBuilder.parameterReferencingMethodParameter(inputParameter.getChild(1).getText()); + } + throw new IllegalStateException("Unknown input parameter: " + text); + } + + private static Expression getExpression(JDQLParser.In_itemContext inItem, + CriteriaBuilder criteriaBuilder) { + JDQLParser.LiteralContext literal = inItem.literal(); + if (literal != null) { + return getExpression(literal, criteriaBuilder); + } + JDQLParser.Enum_literalContext enumLiteral = inItem.enum_literal(); + if (enumLiteral != null) { + return getExpression(enumLiteral); + } + JDQLParser.Input_parameterContext inputParameter = inItem.input_parameter(); + if (inputParameter != null) { + return getExpression(inputParameter, criteriaBuilder); + } + throw new IllegalStateException("Unknown IN item: " + inItem); + } + +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataByMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataByMapper.java new file mode 100644 index 0000000000..edd29b6a87 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataByMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.By; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @By annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataByMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.By"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(By.class).value(annotation.stringValue().orElseThrow()).build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataDeleteMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataDeleteMapper.java new file mode 100644 index 0000000000..f8217457da --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataDeleteMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Delete; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Delete annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataDeleteMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Delete"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Delete.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataDeleteExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataFindMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataFindMapper.java new file mode 100644 index 0000000000..d6c024216f --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataFindMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Find; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Find annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataFindMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Find"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Find.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataInsertMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataInsertMapper.java new file mode 100644 index 0000000000..04461d2d33 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataInsertMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Insert; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Insert annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataInsertMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Insert"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Insert.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataInsertExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByListMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByListMapper.java new file mode 100644 index 0000000000..424f8b32f4 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByListMapper.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.OrderBy; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @OrderBy.List annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataOrderByListMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.OrderBy.List"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + JakartaDataOrderByMapper jakartaDataOrderByMapper = new JakartaDataOrderByMapper(); + return List.of( + AnnotationValue.builder(OrderBy.List.class).values( + annotation.getAnnotations(AnnotationMetadata.VALUE_MEMBER) + .stream() + .flatMap(av -> jakartaDataOrderByMapper.map(av, visitorContext).stream()) + .toArray(AnnotationValue[]::new) + ).build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByMapper.java new file mode 100644 index 0000000000..0d0df421bb --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataOrderByMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.OrderBy; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @OrderBy annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataOrderByMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.OrderBy"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(OrderBy.class).members(annotation.getValues()).build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataParamsMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataParamsMapper.java new file mode 100644 index 0000000000..8245969cc0 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataParamsMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Param annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataParamsMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Param"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Parameter.class).value(annotation.stringValue().orElseThrow()).build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataRepositoryMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataRepositoryMapper.java new file mode 100644 index 0000000000..9697f53240 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataRepositoryMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Repository; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Repository annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataRepositoryMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Repository"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Repository.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataSaveMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataSaveMapper.java new file mode 100644 index 0000000000..fc7ef01c7a --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataSaveMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Save; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Save annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataSaveMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Save"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Save.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataInsertExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataUpdateMapper.java b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataUpdateMapper.java new file mode 100644 index 0000000000..9cbb108505 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/mappers/jakarta/data/JakartaDataUpdateMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2020 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.processor.mappers.jakarta.data; + +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.ConvertException; +import io.micronaut.data.annotation.Update; +import io.micronaut.inject.annotation.NamedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Maps Jakarta Data @Update annotation. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataUpdateMapper implements NamedAnnotationMapper { + + @NonNull + @Override + public String getName() { + return "jakarta.data.repository.Update"; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(Update.class).build(), + AnnotationValue.builder(ConvertException.class) + .values(new AnnotationClassValue<>("io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataUpdateExceptionConverter")) + .build() + ); + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentProperty.java b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentProperty.java index 7a65f8ba86..b9cb8be155 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentProperty.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentProperty.java @@ -27,10 +27,14 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.processor.visitors.finders.TypeUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.EnumConstantElement; +import io.micronaut.inject.ast.EnumElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.TypedElement; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -100,6 +104,32 @@ public boolean isEnum() { return type.isEnum(); } + @Override + public List getEnumConstants() { + if (type instanceof EnumElement enumElement) { + List list = new ArrayList<>(); + int i = 0; + for (EnumConstantElement e : enumElement.elements()) { + int finalI = i; + EnumConstant enumConstant = new EnumConstant() { + @Override + public String name() { + return e.getName(); + } + + @Override + public int ordinal() { + return finalI; + } + }; + list.add(enumConstant); + i++; + } + return list; + } + return List.of(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java index d9de88bf3f..f48fa5d4ee 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/SourcePersistentEntityCriteriaBuilder.java @@ -56,15 +56,49 @@ public interface SourcePersistentEntityCriteriaBuilder extends PersistentEntityC ParameterExpression parameter(@NonNull ParameterElement parameterElement, @Nullable PersistentPropertyPath propertyPath); + /** + * Create parameter expression from {@link ParameterElement}. + * + * @param parameterIndex The parameter index + * @param The expression type + * @return new parameter + * @since 4.12 + */ + @NonNull + ParameterExpression parameterReferencingMethodParameter(int parameterIndex); + + /** + * Create parameter expression from {@link ParameterElement}. + * + * @param parameterName The parameter name + * @param The expression type + * @return new parameter + * @since 4.12 + */ + @NonNull + ParameterExpression parameterReferencingMethodParameter(String parameterName); + /** * Create parameter expression from {@link ParameterElement} that is representing an entity instance. * * @param entityParameter The entity parameter element - * @param propertyPath The property path this parameter is representing + * @param propertyPath The property path this parameter is representing * @param The expression type * @return new parameter */ @NonNull ParameterExpression entityPropertyParameter(@NonNull ParameterElement entityParameter, @Nullable PersistentPropertyPath propertyPath); + + @Override + SourcePersistentEntityCriteriaDelete createCriteriaDelete(Class targetEntity); + + @Override + SourcePersistentEntityCriteriaUpdate createCriteriaUpdate(Class targetEntity); + + @Override + SourcePersistentEntityCriteriaQuery createQuery(); + + @Override + SourcePersistentEntityCriteriaQuery createQuery(Class resultClass); } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java index d84381c068..143bfd31d0 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/MethodMatchSourcePersistentEntityCriteriaBuilderImpl.java @@ -15,6 +15,7 @@ */ package io.micronaut.data.processor.model.criteria.impl; +import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.DataType; @@ -66,7 +67,7 @@ public SourcePersistentEntityCriteriaQuery createQuery() { } @Override - public PersistentEntityCriteriaQuery createQuery(Class resultClass) { + public SourcePersistentEntityCriteriaQuery createQuery(Class resultClass) { return new SourcePersistentEntityCriteriaQueryImpl<>(resultClass, methodMatchContext::getEntity, this); } @@ -91,6 +92,24 @@ public ParameterExpression parameter(ParameterElement parameterElement, return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), parameterElement, false, propertyPath); } + @Override + public ParameterExpression parameterReferencingMethodParameter(int parameterIndex) { + return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), methodMatchContext.getParameters()[parameterIndex], false, null); + } + + @Override + public ParameterExpression parameterReferencingMethodParameter(String parameterName) { + ParameterElement parameterElement = null; + ParameterElement[] parameters = methodMatchContext.getParameters(); + for (ParameterElement parameter : parameters) { + if (parameter.stringValue(Parameter.class).orElse(parameter.getName()).equals(parameterName)) { + parameterElement = parameter; + break; + } + } + return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), parameterElement, false, null); + } + @Override public ParameterExpression entityPropertyParameter(ParameterElement entityParameter, @Nullable PersistentPropertyPath propertyPath) { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/QueryResultAnalyzer.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/QueryResultAnalyzer.java index a935aa52cf..4b2c2343ff 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/QueryResultAnalyzer.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/QueryResultAnalyzer.java @@ -80,6 +80,7 @@ public void visit(UnaryExpression unaryExpression) { switch (unaryExpression.getType()) { case COUNT: case COUNT_DISTINCT: + case LENGTH: queryResultTypeName = Long.class.getName(); break; case MAX: diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterExpressionImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterExpressionImpl.java index 80ea0ab2ab..ec053f8b67 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterExpressionImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourceParameterExpressionImpl.java @@ -16,6 +16,7 @@ package io.micronaut.data.processor.model.criteria.impl; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.AutoPopulated; import io.micronaut.data.annotation.Expandable; @@ -227,18 +228,15 @@ public boolean isExpression() { int index = parameterElement == null || isEntityParameter ? -1 : Arrays.asList(parameters).indexOf(parameterElement); boolean requiresPrevValue = index == -1 && autopopulated && !isUpdate; boolean isExpandable = isExpandable(bindingContext, dataType); - String[] path; String[] parameterBindingPath; - if (outgoingQueryParameterProperty != null) { - path = outgoingQueryParameterProperty.getArrayPath(); - if (index != -1) { - parameterBindingPath = getBindingPath(incomingMethodParameterProperty, outgoingQueryParameterProperty); + if (bindingContext.getParameterBindingPath() == null) { + if (incomingMethodParameterProperty != null && outgoingQueryParameterProperty != null) { + parameterBindingPath = getParameterBindingPath(incomingMethodParameterProperty, outgoingQueryParameterProperty); } else { parameterBindingPath = null; } } else { - path = null; - parameterBindingPath = null; + parameterBindingPath = bindingContext.getParameterBindingPath().getArrayPath(); } return new QueryParameterBinding() { @@ -279,7 +277,7 @@ public String[] getParameterBindingPath() { @Override public String[] getPropertyPath() { - return path; + return propertyPath.getArrayPath(); } @Override @@ -317,12 +315,11 @@ private boolean isExpandable(BindingContext bindingContext, DataType dataType) { return !dataType.isArray() && (parameterElement != null && parameterElement.getType().isAssignable(Iterable.class.getName())); } - private String[] getBindingPath(PersistentPropertyPath parameterProperty, PersistentPropertyPath bindedPath) { - if (parameterProperty == null) { - return bindedPath.getArrayPath(); - } - List parameterPath = List.of(parameterProperty.getArrayPath()); - List path = List.of(bindedPath.getArrayPath()); + @Nullable + private String[] getParameterBindingPath(@NonNull PersistentPropertyPath incomingMethodParameterPropertyPath, + @NonNull PersistentPropertyPath outgoingQueryParameterPropertyPath) { + List parameterPath = List.of(incomingMethodParameterPropertyPath.getArrayPath()); + List path = List.of(outgoingQueryParameterPropertyPath.getArrayPath()); if (path.equals(parameterPath)) { return null; } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java index 73e53b6ce4..09826113a6 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentEntityCriteriaBuilderImpl.java @@ -18,7 +18,6 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; -import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; import io.micronaut.data.model.jpa.criteria.impl.AbstractCriteriaBuilder; import io.micronaut.data.processor.model.SourcePersistentEntity; import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder; @@ -28,7 +27,6 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.Tuple; -import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.ParameterExpression; import java.util.function.Function; @@ -45,12 +43,9 @@ public final class SourcePersistentEntityCriteriaBuilderImpl extends AbstractCriteriaBuilder implements SourcePersistentEntityCriteriaBuilder { private final Function entityResolver; - private final CriteriaBuilder criteriaBuilder; - public SourcePersistentEntityCriteriaBuilderImpl(Function entityResolver, - CriteriaBuilder criteriaBuilder) { + public SourcePersistentEntityCriteriaBuilderImpl(Function entityResolver) { this.entityResolver = entityResolver; - this.criteriaBuilder = criteriaBuilder; } @Override @@ -59,23 +54,23 @@ public SourcePersistentEntityCriteriaQuery createQuery() { } @Override - public PersistentEntityCriteriaQuery createQuery(Class resultClass) { + public SourcePersistentEntityCriteriaQuery createQuery(Class resultClass) { return new SourcePersistentEntityCriteriaQueryImpl<>(resultClass, entityResolver, this); } @Override - public PersistentEntityCriteriaQuery createTupleQuery() { - return new SourcePersistentEntityCriteriaQueryImpl<>(Tuple.class, entityResolver, criteriaBuilder); + public SourcePersistentEntityCriteriaQuery createTupleQuery() { + return new SourcePersistentEntityCriteriaQueryImpl<>(Tuple.class, entityResolver, this); } @Override public SourcePersistentEntityCriteriaDelete createCriteriaDelete(Class targetEntity) { - return new SourcePersistentEntityCriteriaDeleteImpl<>(entityResolver, targetEntity, criteriaBuilder); + return new SourcePersistentEntityCriteriaDeleteImpl<>(entityResolver, targetEntity, this); } @Override public SourcePersistentEntityCriteriaUpdate createCriteriaUpdate(Class targetEntity) { - return new SourcePersistentEntityCriteriaUpdateImpl<>(entityResolver, targetEntity, criteriaBuilder); + return new SourcePersistentEntityCriteriaUpdateImpl<>(entityResolver, targetEntity, this); } @Override @@ -88,6 +83,16 @@ public ParameterExpression parameter(ParameterElement parameterElement, P throw notSupportedOperation(); } + @Override + public ParameterExpression parameterReferencingMethodParameter(int parameterIndex) { + return (ParameterExpression) parameter(Object.class, "p" + parameterIndex); + } + + @Override + public ParameterExpression parameterReferencingMethodParameter(String parameterName) { + return (ParameterExpression) parameter(Object.class, parameterName); + } + @Override public ParameterExpression entityPropertyParameter(ParameterElement entityParameter, PersistentPropertyPath propertyPath) { throw notSupportedOperation(); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/MethodMatchContext.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/MethodMatchContext.java index 8cdeebe931..fb1bd6e1dc 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/MethodMatchContext.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/MethodMatchContext.java @@ -16,6 +16,7 @@ package io.micronaut.data.processor.visitors; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.query.builder.QueryBuilder; import io.micronaut.data.processor.model.SourcePersistentEntity; import io.micronaut.inject.ast.ClassElement; @@ -38,10 +39,10 @@ */ public class MethodMatchContext extends MatchContext { - @NonNull - private final SourcePersistentEntity entity; - private final Map parametersInRole; + private SourcePersistentEntity entity; + private final Map parametersInRole; private final Function entityResolver; + private final Function entityBySimplyNameResolver; /** * Creates the context. @@ -64,15 +65,24 @@ public class MethodMatchContext extends MatchContext { @NonNull VisitorContext visitorContext, @NonNull ClassElement returnType, @NonNull MethodElement methodElement, - @NonNull Map parametersInRole, + @NonNull Map parametersInRole, @NonNull Map typeRoles, @NonNull ParameterElement[] parameters, @NonNull Function entityResolver, - @NonNull Map findInterceptors) { + @NonNull Map findInterceptors, + @NonNull Function entityBySimplyNameResolver) { super(queryBuilder, repositoryClass, visitorContext, methodElement, typeRoles, returnType, parameters, findInterceptors); this.entity = entity; this.parametersInRole = Collections.unmodifiableMap(parametersInRole); this.entityResolver = entityResolver; + this.entityBySimplyNameResolver = entityBySimplyNameResolver; + } + + /** + * @return The entity by a simple name resolver + */ + public Function getEntityBySimplyNameResolver() { + return entityBySimplyNameResolver; } /** @@ -82,14 +92,29 @@ public class MethodMatchContext extends MatchContext { */ @SuppressWarnings("ConstantConditions") public boolean hasParameterInRole(@NonNull String role) { - return role != null && parametersInRole.containsKey(role); + return role != null && parametersInRole.containsValue(role); + } + + /** + * Find the parameter in role. + * @param role The parameter role + * @return The parameter + */ + @Nullable + public Element findParameterInRole(@NonNull String role) { + for (Map.Entry e : parametersInRole.entrySet()) { + if (e.getValue().equals(role)) { + return e.getKey(); + } + } + return null; } /** * @return Parameters that fulfill a query execution role */ @NonNull - public Map getParametersInRole() { + public Map getParametersInRole() { return parametersInRole; } @@ -97,18 +122,24 @@ public Map getParametersInRole() { * The root entity being queried. * @return The root entity */ - @NonNull public SourcePersistentEntity getRootEntity() { return entity; } + /** + * @param entity he root entity being queried. + */ + public void setRootEntity(SourcePersistentEntity entity) { + this.entity = entity; + } + /** * Returns a list of parameters that are not fulfilling a specific query role. * @return The parameters not in role */ public @NonNull List getParametersNotInRole() { return Arrays.stream(getParameters()).filter(p -> - !this.parametersInRole.containsValue(p) + !this.parametersInRole.containsKey(p) ).toList(); } @@ -117,7 +148,7 @@ public SourcePersistentEntity getRootEntity() { * @return The parameters not in role */ public @NonNull List getParametersInRoleList() { - return Arrays.stream(getParameters()).filter(this.parametersInRole::containsValue).toList(); + return Arrays.stream(getParameters()).filter(this.parametersInRole::containsKey).toList(); } /** diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java index 0b319d25a8..66949e32d1 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java @@ -30,13 +30,17 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.data.annotation.Delete; import io.micronaut.data.annotation.EntityRepresentation; +import io.micronaut.data.annotation.Insert; import io.micronaut.data.annotation.Join; +import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.ParameterExpression; import io.micronaut.data.annotation.Query; import io.micronaut.data.annotation.Repository; import io.micronaut.data.annotation.RepositoryConfiguration; import io.micronaut.data.annotation.TypeRole; +import io.micronaut.data.annotation.Update; import io.micronaut.data.annotation.sql.Procedure; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.intercept.annotation.DataMethodQuery; @@ -44,6 +48,7 @@ import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.PersistentProperty; @@ -82,9 +87,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -104,18 +111,20 @@ public class RepositoryTypeElementVisitor implements TypeElementVisitor { public static final String SPRING_REPO = "org.springframework.data.repository.Repository"; + public static final String JAKARTA_DATA_REPO = "jakarta.data.repository.DataRepository"; private static final boolean IS_DOCUMENT_ANNOTATION_PROCESSOR = ClassUtils.isPresent("io.micronaut.data.document.processor.mapper.MappedEntityMapper", RepositoryTypeElementVisitor.class.getClassLoader()); + private static final Map COMMON_TYPE_ROLES; private ClassElement currentClass; private ClassElement currentRepository; private QueryBuilder queryEncoder; - private final Map typeRoles = new HashMap<>(); private final List methodsMatchers; private boolean failing = false; private final Set visitedRepositories = new HashSet<>(); private Map dataTypes = Collections.emptyMap(); private final Map entityMap = new HashMap<>(50); private Function entityResolver; + private Function entityBySimplyNameResolver; { List matcherList = new ArrayList<>(20); @@ -124,15 +133,36 @@ public class RepositoryTypeElementVisitor implements TypeElementVisitor roles = new LinkedHashMap<>(); + roles.put(Pageable.class.getName(), TypeRole.PAGEABLE); + roles.put(Sort.class.getName(), TypeRole.SORT); + roles.put(CursoredPage.class.getName(), TypeRole.CURSORED_PAGE); + roles.put(Page.class.getName(), TypeRole.PAGE); + roles.put(Slice.class.getName(), TypeRole.SLICE); + roles.put(Limit.class.getName(), TypeRole.LIMIT); + // Spring Data + roles.put("org.springframework.data.domain.Pageable", TypeRole.PAGEABLE); + roles.put("org.springframework.data.domain.Page", TypeRole.PAGE); + roles.put("org.springframework.data.domain.Slice", TypeRole.SLICE); + roles.put("org.springframework.data.domain.Sort", TypeRole.SORT); + // Jakarta Data + roles.put("jakarta.data.page.Page", TypeRole.PAGE); + roles.put("jakarta.data.page.CursoredPage", TypeRole.CURSORED_PAGE); + roles.put("jakarta.data.page.PageRequest", TypeRole.PAGEABLE); + roles.put("jakarta.data.Order", TypeRole.SORT); + roles.put("jakarta.data.Sort", TypeRole.SORT); + roles.put("jakarta.data.Limit", TypeRole.LIMIT); + COMMON_TYPE_ROLES = Collections.unmodifiableMap(roles); + } + + private Map currentClassTypeRoles; + /** * Default constructor. */ public RepositoryTypeElementVisitor() { - typeRoles.put(Pageable.class.getName(), TypeRole.PAGEABLE); - typeRoles.put(Sort.class.getName(), TypeRole.SORT); - typeRoles.put(CursoredPage.class.getName(), TypeRole.CURSORED_PAGE); - typeRoles.put(Page.class.getName(), TypeRole.PAGE); - typeRoles.put(Slice.class.getName(), TypeRole.SLICE); + } @NonNull @@ -161,6 +191,7 @@ private Map createFindInterceptors(ClassElemen @Override public void visitClass(ClassElement element, VisitorContext context) { + currentClassTypeRoles = new LinkedHashMap<>(COMMON_TYPE_ROLES); String interfaceName = element.getName(); if (failing) { return; @@ -198,6 +229,17 @@ public SourcePersistentEntity apply(ClassElement classElement) { }); } }; + entityBySimplyNameResolver = new Function<>() { + @Override + public SourcePersistentEntity apply(String entitySimpleName) { + for (SourcePersistentEntity persistentEntity : entityMap.values()) { + if (persistentEntity.getSimpleName().equals(entitySimpleName)) { + return persistentEntity; + } + } + return null; + } + }; if (element.hasDeclaredStereotype(Repository.class)) { visitedRepositories.add(interfaceName); @@ -214,24 +256,10 @@ public SourcePersistentEntity apply(ClassElement classElement) { AnnotationClassValue cv = parameterRole.get("type", AnnotationClassValue.class).orElse(null); if (StringUtils.isNotEmpty(role) && cv != null) { context.getClassElement(cv.getName()).ifPresent(ce -> - typeRoles.put(ce.getName(), role) + currentClassTypeRoles.put(ce.getName(), role) ); } } - if (element.isAssignable(SPRING_REPO)) { - context.getClassElement("org.springframework.data.domain.Pageable").ifPresent(ce -> - typeRoles.put(ce.getName(), TypeRole.PAGEABLE) - ); - context.getClassElement("org.springframework.data.domain.Page").ifPresent(ce -> - typeRoles.put(ce.getName(), TypeRole.PAGE) - ); - context.getClassElement("org.springframework.data.domain.Slice").ifPresent(ce -> - typeRoles.put(ce.getName(), TypeRole.SLICE) - ); - context.getClassElement("org.springframework.data.domain.Sort").ifPresent(ce -> - typeRoles.put(ce.getName(), TypeRole.SORT) - ); - } // Annotate repository with EntityRepresentation if present on entity class annotateEntityRepresentationIfPresent(element); @@ -252,17 +280,7 @@ public void visitMethod(MethodElement element, VisitorContext context) { ClassElement genericReturnType = element.getGenericReturnType(); if (queryEncoder != null && currentClass != null && element.isAbstract() && !element.isStatic() && methodsMatchers != null) { ParameterElement[] parameters = element.getParameters(); - Map parametersInRole = new HashMap<>(2); - for (ParameterElement parameter : parameters) { - ClassElement type = parameter.getType(); - this.typeRoles.entrySet().stream().filter(entry -> { - String roleType = entry.getKey(); - return type.isAssignable(roleType); - } - ).forEach(entry -> - parametersInRole.put(entry.getValue(), parameter) - ); - } + Map parametersInRole = getParametersInRole(parameters); if (element.hasDeclaredAnnotation(DataMethod.class)) { // explicitly handled @@ -276,13 +294,16 @@ public void visitMethod(MethodElement element, VisitorContext context) { currentRepository, context, element, - typeRoles, + currentClassTypeRoles, genericReturnType, parameters, findInterceptors); try { - SourcePersistentEntity entity = resolvePersistentEntity(element, parametersInRole); + List parametersNotInRole = Arrays.stream(parameters) + .filter(p -> !parametersInRole.containsKey(p)) + .toList(); + SourcePersistentEntity entity = resolvePersistentEntity(element, parametersInRole, parametersNotInRole); MethodMatchContext methodMatchContext = new MethodMatchContext( queryEncoder, currentRepository, @@ -291,10 +312,11 @@ public void visitMethod(MethodElement element, VisitorContext context) { genericReturnType, element, parametersInRole, - typeRoles, + currentClassTypeRoles, parameters, entityResolver, - findInterceptors + findInterceptors, + entityBySimplyNameResolver ); for (MethodMatcher finder : methodsMatchers) { @@ -333,15 +355,51 @@ public void visitMethod(MethodElement element, VisitorContext context) { } } + private Map getParametersInRole(ParameterElement[] parameters) { + Map parametersInRole = new LinkedHashMap<>(2); + for (ParameterElement parameter : parameters) { + parametersInRole.computeIfAbsent(parameter, p -> findTypeRole(parameter.getType())); + } + return parametersInRole; + } + + private String findTypeRole(ClassElement type) { + Set> entries = currentClassTypeRoles.entrySet(); + for (Map.Entry entry : entries) { + // Find the role by the exact type name + if (type.getName().equals(entry.getKey())) { + return entry.getValue(); + } + } + for (Map.Entry entry : entries) { + // Find the role by the isAssignable + if (type.isAssignable(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + + private List getParametersNotInRole(ParameterElement[] parameters) { + List parametersNotInRole = new ArrayList<>(); + for (ParameterElement parameter : parameters) { + ClassElement type = parameter.getType(); + if (currentClassTypeRoles.keySet().stream().noneMatch(type::isAssignable)) { + parametersNotInRole.add(parameter); + } + } + return parametersNotInRole; + } + private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatchInfo methodInfo) { QueryBuilder queryEncoder = methodMatchContext.getQueryBuilder(); - MethodElement element = methodMatchContext.getMethodElement(); + MethodElement method = methodMatchContext.getMethodElement(); // populate parameter roles - for (Map.Entry entry : methodMatchContext.getParametersInRole().entrySet()) { + for (Map.Entry entry : methodMatchContext.getParametersInRole().entrySet()) { methodInfo.addParameterRole( - entry.getKey(), - entry.getValue().getName() + (ParameterElement) entry.getKey(), + entry.getValue() ); } @@ -354,21 +412,21 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc if (methodInfo.isRawQuery()) { - element.annotate(Query.class, (builder) -> builder.member(DataMethod.META_MEMBER_RAW_QUERY, - element.stringValue(Query.class) + method.annotate(Query.class, (builder) -> builder.member(DataMethod.META_MEMBER_RAW_QUERY, + method.stringValue(Query.class) .map(q -> addRawQueryParameterPlaceholders(queryEncoder, queryResult.getQuery(), queryResult.getQueryParts())) .orElse(null))); ClassElement genericReturnType = methodMatchContext.getReturnType(); if (methodMatchContext.isTypeInRole(genericReturnType, TypeRole.PAGE) || methodMatchContext.isTypeInRole(genericReturnType, TypeRole.CURSORED_PAGE) - || element.isPresent(Query.class, "countQuery") + || method.isPresent(Query.class, "countQuery") ) { QueryResult countQueryResult = methodInfo.getCountQueryResult(); if (countQueryResult == null) { - throw new ProcessingException(element, "Query returns a Page and does not specify a 'countQuery' member."); + throw new ProcessingException(method, "Query returns a Page and does not specify a 'countQuery' member."); } else { - element.annotate( + method.annotate( Query.class, (builder) -> builder.member(DataMethod.META_MEMBER_RAW_COUNT_QUERY, addRawQueryParameterPlaceholders(queryEncoder, countQueryResult.getQuery(), countQueryResult.getQueryParts())) ); @@ -381,13 +439,13 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc QueryResult preparedCount = methodInfo.getCountQueryResult(); if (preparedCount != null) { - element.annotate(Query.class, annotationBuilder -> { + method.annotate(Query.class, annotationBuilder -> { annotationBuilder.value(queryResult.getQuery()); annotationBuilder.member(DataMethod.META_MEMBER_COUNT_QUERY, preparedCount.getQuery()); } ); } else { - element.annotate(Query.class, annotationBuilder -> { + method.annotate(Query.class, annotationBuilder -> { annotationBuilder.value(queryResult.getQuery()); String update = queryResult.getUpdate(); if (StringUtils.isNotEmpty(update)) { @@ -401,8 +459,8 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc if (CollectionUtils.isNotEmpty(joinPaths)) { // Only apply the changes if joins aren't empty. // Implementation might choose to return an empty array to skip the modification of existing annotations. - element.removeAnnotation(Join.class); - joinPaths.forEach(joinPath -> element.annotate(Join.class, builder -> { + method.removeAnnotation(Join.class); + joinPaths.forEach(joinPath -> method.annotate(Join.class, builder -> { builder.member("value", joinPath.getPath()) .member("type", joinPath.getJoinType()); if (joinPath.getAlias().isPresent()) { @@ -413,16 +471,18 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc } } - annotateQueryResultIfApplicable(element, methodInfo, methodMatchContext.getRootEntity()); + annotateQueryResultIfApplicable(method, methodInfo, methodMatchContext.getRootEntity()); - element.annotate(DataMethod.class.getName(), annotationBuilder -> { + method.annotate(DataMethod.class.getName(), annotationBuilder -> { ClassElement runtimeInterceptor = methodInfo.getRuntimeInterceptor(); if (runtimeInterceptor == null) { - throw new MatchFailedException("Unable to implement Repository method: " + currentRepository.getSimpleName() + "." + element.getName() + "(..). No possible runtime implementations found.", element); + throw new MatchFailedException("Unable to implement Repository method: " + currentRepository.getSimpleName() + "." + method.getName() + "(..). No possible runtime implementations found.", method); } annotationBuilder.member(DataMethod.META_MEMBER_INTERCEPTOR, new AnnotationClassValue<>(runtimeInterceptor.getName())); - annotationBuilder.member(DataMethod.META_MEMBER_ROOT_ENTITY, new AnnotationClassValue<>(methodMatchContext.getRootEntity().getName())); + if (methodMatchContext.getRootEntity() != null) { + annotationBuilder.member(DataMethod.META_MEMBER_ROOT_ENTITY, new AnnotationClassValue<>(methodMatchContext.getRootEntity().getName())); + } if (methodInfo.isDto()) { annotationBuilder.member(DataMethod.META_MEMBER_DTO, true); @@ -431,8 +491,32 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc annotationBuilder.member(DataMethod.META_MEMBER_OPTIMISTIC_LOCK, true); } - // include the roles - methodInfo.getParameterRoles().forEach(annotationBuilder::member); + if (!methodInfo.getParameterRoles().isEmpty()) { + // include the roles + for (Map.Entry e : methodInfo.getParameterRoles().entrySet()) { + // Legacy parameters binding doesn't allow duplicate roles + ParameterElement parameter = e.getKey(); + annotationBuilder.member(e.getValue(), parameter.stringValue(Parameter.class).orElse(parameter.getName())); + } + + List parameters = List.of(method.getParameters()); + annotationBuilder.member(DataMethodQuery.META_MEMBER_PARAMETERS_TYPE_ROLES, + methodInfo.getParameterRoles().entrySet() + .stream() + .sorted(Comparator.comparingInt(o -> parameters.indexOf(o.getKey()))) + .map(e -> + AnnotationValue.builder("type") + .value(e.getValue()) + .member("parameterIndex", parameters.indexOf(e.getKey())) + .build() + ).toArray(AnnotationValue[]::new) + ); + } + + String returnTypeRole = findTypeRole(method.getReturnType().getType()); + if (returnTypeRole != null) { + annotationBuilder.member(DataMethodQuery.META_MEMBER_RETURN_TYPE_ROLE, returnTypeRole); + } addQueryDefinition(methodMatchContext, annotationBuilder, @@ -455,7 +539,7 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc } builder.member(AnnotationMetadata.VALUE_MEMBER, query); - builder.member(DataMethodQuery.META_MEMBER_NATIVE, element.booleanValue(Query.class, + builder.member(DataMethodQuery.META_MEMBER_NATIVE, method.booleanValue(Query.class, DataMethodQuery.META_MEMBER_NATIVE).orElse(false)); addQueryDefinition(methodMatchContext, @@ -480,31 +564,42 @@ private void addQueryDefinition(MethodMatchContext methodMatchContext, boolean encodeEntityParameters) { if (methodMatchContext.getMethodElement().hasAnnotation(Procedure.class)) { - annotationBuilder.member(DataMethod.META_MEMBER_PROCEDURE, true); + annotationBuilder.member(DataMethodQuery.META_MEMBER_PROCEDURE, true); } - annotationBuilder.member(DataMethod.META_MEMBER_OPERATION_TYPE, operationType); + annotationBuilder.member(DataMethodQuery.META_MEMBER_OPERATION_TYPE, operationType); if (resultType != null) { - annotationBuilder.member(DataMethod.META_MEMBER_RESULT_TYPE, new AnnotationClassValue<>(resultType.getName())); + annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_TYPE, new AnnotationClassValue<>(resultType.getName())); ClassElement type = resultType.getType(); if (!TypeUtils.isVoid(type)) { - annotationBuilder.member(DataMethod.META_MEMBER_RESULT_DATA_TYPE, TypeUtils.resolveDataType(type, dataTypes)); + annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_DATA_TYPE, TypeUtils.resolveDataType(type, dataTypes)); } } if (queryResult != null) { if (parameterBinding.stream().anyMatch(QueryParameterBinding::isExpandable)) { - annotationBuilder.member(DataMethod.META_MEMBER_EXPANDABLE_QUERY, queryResult.getQueryParts().toArray(new String[0])); + annotationBuilder.member(DataMethodQuery.META_MEMBER_EXPANDABLE_QUERY, queryResult.getQueryParts().toArray(new String[0])); } int max = queryResult.getMax(); if (max > -1) { - annotationBuilder.member(DataMethod.META_MEMBER_LIMIT, max); + annotationBuilder.member(DataMethodQuery.META_MEMBER_LIMIT, max); } long offset = queryResult.getOffset(); if (offset > 0) { - annotationBuilder.member(DataMethod.META_MEMBER_OFFSET, offset); + annotationBuilder.member(DataMethodQuery.META_MEMBER_OFFSET, offset); + } + Sort sort = queryResult.getSort(); + if (sort.isSorted()) { + annotationBuilder.member(DataMethodQuery.META_MEMBER_SORT, sort.getOrderBy().stream().map(order -> + AnnotationValue.builder("order") // ?? Should we add a new annotation + .value(order.getProperty()) + .member("direction", order.getDirection()) + .member("ignoreCase", order.isIgnoreCase()) + .build()) + .toArray(AnnotationValue[]::new) + ); } } @@ -699,7 +794,10 @@ private String addRawQueryParameterPlaceholders(QueryBuilder queryEncoder, Strin return query; } - private SourcePersistentEntity resolvePersistentEntity(MethodElement element, Map parametersInRole) { + @Nullable + private SourcePersistentEntity resolvePersistentEntity(MethodElement element, + Map parametersInRole, + List parametersNotInRole) { ClassElement returnType = element.getGenericReturnType(); SourcePersistentEntity entity = resolveEntityForCurrentClass(); if (entity == null) { @@ -713,13 +811,42 @@ private SourcePersistentEntity resolvePersistentEntity(MethodElement element, Ma for (PersistentProperty persistentProperty : propertiesInRole) { String role = persistentProperty.getAnnotationMetadata().getValue(TypeRole.class, "role", String.class).orElse(null); if (role != null) { - parametersInRole.put(role, ((SourcePersistentProperty) persistentProperty).getPropertyElement()); + parametersInRole.put(((SourcePersistentProperty) persistentProperty).getPropertyElement(), role); } } return entity; - } else { - throw new MatchFailedException("Could not resolved root entity. Either implement the Repository interface or define the entity as part of the signature", element); } + SourcePersistentEntity sourcePersistentEntity = resolvePersistentEntityFromLifecycleMethods(element, parametersNotInRole); + if (sourcePersistentEntity != null) { + return sourcePersistentEntity; + } + if (element.hasStereotype(Query.class)) { + return null; + } + ClassElement owningType = element.getOwningType(); + for (MethodElement method : owningType.getMethods()) { + return resolvePersistentEntityFromLifecycleMethods(method, getParametersNotInRole(method.getParameters())); + } + throw new MatchFailedException("Could not resolved root entity. Either implement the Repository interface or define the entity as part of the signature", element); + } + + @Nullable + private SourcePersistentEntity resolvePersistentEntityFromLifecycleMethods(MethodElement element, + List parametersNotInRole) { + if (element.hasStereotype(Insert.class) || element.hasStereotype(Update.class) || element.hasStereotype(Delete.class)) { + if (!parametersNotInRole.isEmpty()) { + ClassElement type = parametersNotInRole.iterator().next().getGenericType(); + if (type.isArray()) { + type = type.fromArray(); + } else if (type.isAssignable(Iterable.class)) { + type = type.getTypeArguments(Iterable.class).entrySet().iterator().next().getValue(); + } + if (type.hasStereotype(MappedEntity.class)) { + return entityResolver.apply(type); + } + } + } + return null; } @Nullable @@ -730,6 +857,10 @@ private SourcePersistentEntity resolveEntityForCurrentClass() { argName = "T"; typeArguments = currentRepository.getTypeArguments(SPRING_REPO); } + if (typeArguments.isEmpty()) { + argName = "T"; + typeArguments = currentRepository.getTypeArguments(JAKARTA_DATA_REPO); + } if (!typeArguments.isEmpty()) { ClassElement ce = typeArguments.get(argName); if (ce != null) { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java index ce82dc9f45..64ff7e481e 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java @@ -15,7 +15,6 @@ */ package io.micronaut.data.processor.visitors.finders; -import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Experimental; @@ -26,6 +25,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.data.annotation.By; import io.micronaut.data.annotation.DataAnnotationUtils; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.Join; @@ -165,15 +165,15 @@ public final MethodMatchInfo buildMatchInfo(MethodMatchContext matchContext) { ParameterElement entitiesParameter = getEntitiesParameter(); ParameterElement idParameter = Arrays.stream(matchContext.getParameters()).filter(p -> p.hasAnnotation(Id.class)).findFirst().orElse(null); if (idParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ID, idParameter.stringValue(Parameter.class).orElse(idParameter.getName())); + methodMatchInfo.addParameterRole(idParameter, TypeRole.ID); } boolean encodeEntityParameters = !DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(matchContext.getAnnotationMetadata()); if (entityParameter != null) { methodMatchInfo.encodeEntityParameters(encodeEntityParameters); - methodMatchInfo.addParameterRole(TypeRole.ENTITY, entityParameter.getName()); + methodMatchInfo.addParameterRole(entityParameter, TypeRole.ENTITY); } else if (entitiesParameter != null) { methodMatchInfo.encodeEntityParameters(encodeEntityParameters); - methodMatchInfo.addParameterRole(TypeRole.ENTITIES, entitiesParameter.getName()); + methodMatchInfo.addParameterRole(entitiesParameter, TypeRole.ENTITIES); } return methodMatchInfo; } @@ -204,13 +204,23 @@ protected final Predicate extractPredicates(List queryParams, List predicates = new ArrayList<>(queryParams.size()); for (ParameterElement queryParam : queryParams) { String paramName = queryParam.getName(); - PersistentPropertyPath propPath = rootEntity.getPropertyPath(rootEntity.getPath(paramName).orElse(paramName)); + boolean isId = TypeRole.ID.equals(paramName); + if (queryParam.hasAnnotation(By.class)) { + paramName = queryParam.stringValue(By.class).orElseThrow(); + isId = By.ID.equals(paramName); + } + PersistentPropertyPath propPath; + if (isId && rootEntity.hasIdentity()) { + propPath = new PersistentPropertyPath(rootEntity.getIdentity()); + } else { + propPath = rootEntity.getPropertyPath(rootEntity.getPath(paramName).orElse(paramName)); + } ParameterExpression param = ((SourcePersistentEntityCriteriaBuilder) cb).parameter(queryParam, propPath); if (propPath == null) { - if (TypeRole.ID.equals(paramName) && (rootEntity.hasIdentity() || rootEntity.hasCompositeIdentity())) { + if (isId && (rootEntity.hasIdentity() || rootEntity.hasCompositeIdentity())) { predicates.add(cb.equal(root.id(), param)); } else { - throw new MatchFailedException("Cannot query persistentEntity [" + rootEntity.getSimpleName() + "] on non-existent property: " + paramName); + throw new MatchFailedException("Cannot query entity [" + rootEntity.getSimpleName() + "] on non-existent property: " + paramName); } } else { PersistentProperty property = propPath.getProperty(); @@ -528,7 +538,7 @@ protected final Expression getProperty(PersistentEntityRoot root, if (property != null) { return property; } - throw new MatchFailedException("Cannot query entity [" + root.getPersistentEntity().getSimpleName() + "] on non-existent property: " + propertyName); + throw new MatchFailedException("Cannot query entity [" + root.getPersistentEntity().getSimpleName() + "] on non-existent property: " + propertyName + " " + root.getPersistentEntity().getPersistentProperties().stream().map(PersistentProperty::getName).toList()); } @Nullable diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractMethodMatcher.java index 0238efe7cc..9af0d93f02 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractMethodMatcher.java @@ -29,6 +29,7 @@ @Internal public abstract class AbstractMethodMatcher implements MethodMatcher { + protected static final String[] ALL = {"All"}; protected static final String[] ALL_OR_ONE = {"All", "One"}; protected static final String[] TOP_OR_FIRST = {"Top", "First"}; protected static final String FIRST = "First"; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/CountMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/CountMethodMatcher.java index 8abff9402c..b9ff9b7697 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/CountMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/CountMethodMatcher.java @@ -38,6 +38,7 @@ public final class CountMethodMatcher extends AbstractMethodMatcher { public CountMethodMatcher() { super(MethodNameParser.builder() .match(QueryMatchId.PREFIX, "count") + .tryMatch(QueryMatchId.ALL, ALL) .tryMatch(QueryMatchId.DISTINCT, DISTINCT) .tryMatchFirstOccurrencePrefixed(QueryMatchId.PREDICATE, BY) .takeRest(QueryMatchId.PROJECTION) diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindMethodMatcher.java index fdf1219a40..d255a2ac79 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindMethodMatcher.java @@ -59,6 +59,10 @@ public FindMethodMatcher() { @Override public MethodMatch match(MethodMatchContext matchContext, List matches) { + return by(matches); + } + + public static QueryCriteriaMethodMatch by(List matches) { return new QueryCriteriaMethodMatch(matches) { boolean hasIdMatch; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindersUtils.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindersUtils.java index 7b1e22055e..74051d41d0 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindersUtils.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/FindersUtils.java @@ -370,6 +370,8 @@ static FindersUtils.InterceptorMatch resolveSyncFindInterceptor(@NonNull MethodM return typeAndInterceptorEntry(matchContext, firstTypeArgument, FindSliceInterceptor.class); } else if (isContainer(returnType, Iterable.class)) { return typeAndInterceptorEntry(matchContext, firstTypeArgument, FindAllInterceptor.class); + } else if (returnType.isArray()) { + return typeAndInterceptorEntry(matchContext, returnType.fromArray(), FindAllInterceptor.class); } else if (isContainer(returnType, Publisher.class)) { return typeAndInterceptorEntry(matchContext, firstTypeArgument, FindAllReactiveInterceptor.class); } else { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodMatchInfo.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodMatchInfo.java index b0ba374a40..06f204008f 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodMatchInfo.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/MethodMatchInfo.java @@ -15,10 +15,12 @@ */ package io.micronaut.data.processor.visitors.finders; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.model.query.builder.QueryResult; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; import java.util.Collections; @@ -40,7 +42,7 @@ public final class MethodMatchInfo { private final TypedElement resultType; private final ClassElement interceptor; - private final Map parameterRoles = new HashMap<>(2); + private final Map parameterRoles = new HashMap<>(2); private boolean dto; private boolean optimisticLock; @@ -98,18 +100,20 @@ public void setOptimisticLock(boolean optimisticLock) { /** * Adds a parameter role. This indicates that a parameter is involved * somehow in the query. - * @param role The role name - * @param name The parameter + * + * @param parameter The parameter + * @param name The role name * @see io.micronaut.data.annotation.TypeRole */ - public void addParameterRole(CharSequence role, String name) { - parameterRoles.put(role.toString(), name); + public void addParameterRole(@NonNull ParameterElement parameter, @NonNull String name) { + parameterRoles.put(parameter, name); } /** * @return The parameter roles */ - public Map getParameterRoles() { + @NonNull + public Map getParameterRoles() { return Collections.unmodifiableMap(parameterRoles); } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/QueryMatchId.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/QueryMatchId.java index 29f5bc44f8..83f1d304af 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/QueryMatchId.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/QueryMatchId.java @@ -27,6 +27,7 @@ public enum QueryMatchId implements MethodNameParser.MatchId { PREFIX, + ALL, ALL_OR_ONE, LIMIT, FIRST, diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java index 19399944ab..486612cfb6 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java @@ -156,9 +156,9 @@ public MethodMatchInfo buildMatchInfo(MethodMatchContext matchContext) { buildRawQuery(matchContext, methodMatchInfo, entityParameter, entitiesParameter, operationType, implicitQueries); if (entityParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITY, entityParameter.getName()); + methodMatchInfo.addParameterRole(entityParameter, TypeRole.ENTITY); } else if (entitiesParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITIES, entitiesParameter.getName()); + methodMatchInfo.addParameterRole(entitiesParameter, TypeRole.ENTITIES); } return methodMatchInfo; } @@ -338,7 +338,7 @@ public static QueryParameterBinding addBinding(MethodMatchContext matchContext, .filter(p -> p.stringValue(Parameter.class).orElse(p.getName()).equals(name)) .findFirst(); if (element.isPresent()) { - PersistentPropertyPath propertyPath = matchContext.getRootEntity().getPropertyPath(name); + PersistentPropertyPath propertyPath = matchContext.getRootEntity() == null ? null : matchContext.getRootEntity().getPropertyPath(name); bindingContext = bindingContext .incomingMethodParameterProperty(propertyPath) .outgoingQueryParameterProperty(propertyPath); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/Restrictions.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/Restrictions.java index d60e511d89..287a9fb13e 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/Restrictions.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/Restrictions.java @@ -161,6 +161,23 @@ public String getName() { } } + /** + * Greater than equals. + * + * @param The property type + */ + public static class PropertyGreaterThanEqual> extends SinglePropertyExpressionRestriction { + + public PropertyGreaterThanEqual() { + super(PersistentEntityCriteriaBuilder::greaterThanOrEqualTo); + } + + @Override + public String getName() { + return "GreaterThanEqual"; + } + } + /** * Less than. * @@ -195,6 +212,23 @@ public String getName() { } } + /** + * Less than equals. + * + * @param The property type + */ + public static class PropertyLessThanEqual> extends SinglePropertyExpressionRestriction { + + public PropertyLessThanEqual() { + super(PersistentEntityCriteriaBuilder::lessThanOrEqualTo); + } + + @Override + public String getName() { + return "LessThanEqual"; + } + } + /** * Like criterion. */ @@ -554,6 +588,23 @@ public String getName() { } } + /** + * IsNotNull restriction. + * + * @param The property type + */ + public static class PropertyNotNull extends SinglePropertyRestriction { + + public PropertyNotNull() { + super(PersistentEntityCriteriaBuilder::isNotNull); + } + + @Override + public String getName() { + return "NotNull"; + } + } + /** * IsNull restriction. * @@ -571,6 +622,23 @@ public String getName() { } } + /** + * Null restriction. + * + * @param The property type + */ + public static class PropertyNull extends SinglePropertyRestriction { + + public PropertyNull() { + super(PersistentEntityCriteriaBuilder::isNull); + } + + @Override + public String getName() { + return "Null"; + } + } + /** * IsEmpty restriction. */ @@ -628,6 +696,37 @@ public Predicate find(PersistentEntityRoot entityRoot, } + /** + * Between restriction. + * + * @param The property type + */ + public static class PropertyIgnoreCaseBetween> implements PropertyRestriction { + + @Override + public String getName() { + return "IgnoreCaseBetween"; + } + + @Override + public int getRequiredParameters() { + return 2; + } + + @Override + public Predicate find(PersistentEntityRoot entityRoot, + PersistentEntityCriteriaBuilder cb, + Expression expression, + List> parameters) { + return cb.between( + cb.lower((Expression) expression), + cb.lower((Expression) parameters.get(0)), + cb.lower((Expression) parameters.get(1)) + ); + } + + } + /** * Equals restriction. * diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/SaveMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/SaveMethodMatcher.java index 547a130de4..08f06c5f46 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/SaveMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/SaveMethodMatcher.java @@ -95,7 +95,7 @@ protected MethodMatch match(MethodMatchContext matchContext, List { ParameterElement[] parameters = mc.getParameters(); ParameterElement entityParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isEntity(p.getGenericType())).findFirst().orElse(null); @@ -132,10 +132,10 @@ private MethodMatch saveEntity(DataMethod.OperationType operationType) { ); } if (entitiesParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITIES, entitiesParameter.getName()); + methodMatchInfo.addParameterRole(entitiesParameter, TypeRole.ENTITIES); } if (entityParameter != null) { - methodMatchInfo.addParameterRole(TypeRole.ENTITY, entityParameter.getName()); + methodMatchInfo.addParameterRole(entityParameter, TypeRole.ENTITY); } return methodMatchInfo; }; diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java index 9bfccf822a..4a40219657 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java @@ -86,7 +86,13 @@ public static ClassElement getKotlinCoroutineProducedType(@NonNull MethodElement * @return True if is */ public static boolean isIterableOfEntity(@Nullable ClassElement type) { - return type != null && isIterableOfDto(type) && hasPersistedTypeArgument(type); + if (type == null) { + return false; + } + if (type.isArray() && isEntity(type.fromArray())) { + return true; + } + return isIterableOfDto(type) && hasPersistedTypeArgument(type); } /** @@ -127,8 +133,20 @@ public static boolean isEntity(@Nullable ClassElement type) { if (type == null) { return false; } - return type.hasStereotype(MappedEntity.class); - } + return !type.isArray() && type.hasStereotype(MappedEntity.class); + } + +// /** +// * Does the given type have an {@link MappedEntity}. +// * @param parameterElement The type +// * @return True if it does +// */ +// public static boolean isEntity(@Nullable ParameterElement parameterElement) { +// if (parameterElement == null || parameterElement.getGenericType().isArray()) { +// return false; +// } +// return +// } /** * Does the given type have an {@link Introspected}. diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java index 5a1d2b8177..e0eefddb3b 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/UpdateMethodMatcher.java @@ -94,7 +94,7 @@ protected MethodMatch match(MethodMatchContext matchContext, List matches, + public static UpdateCriteriaMethodMatch entityUpdate(List matches, ParameterElement entityParameter, ParameterElement entitiesParameter, boolean isReturning) { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/DeleteAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/DeleteAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..7134fe9bd8 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/DeleteAnnotatedMethodMatcher.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.Delete; +import io.micronaut.data.processor.model.SourcePersistentEntity; +import io.micronaut.data.processor.visitors.MatchFailedException; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.data.processor.visitors.finders.TypeUtils; +import io.micronaut.data.processor.visitors.finders.criteria.DeleteCriteriaMethodMatch; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.processing.ProcessingException; + +import java.util.Arrays; +import java.util.List; + +/** + * The Delete annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class DeleteAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + if (matchContext.getMethodElement().hasStereotype(Delete.class)) { + if (matchContext.getRootEntity() == null) { + throw new ProcessingException(matchContext.getMethodElement(), "Repository does not have a well-defined primary entity type"); + } + ParameterElement[] parameters = matchContext.getParameters(); + ParameterElement entityParameter = null; + ParameterElement entitiesParameter = null; + if (matchContext.getParametersNotInRole().size() == 1) { + entityParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isEntity(p.getGenericType())).findFirst().orElse(null); + entitiesParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isIterableOfEntity(p.getGenericType())).findFirst().orElse(null); + } + + SourcePersistentEntity rootEntity = matchContext.getRootEntity(); + if (!rootEntity.hasIdentity() && !rootEntity.hasCompositeIdentity()) { + throw new MatchFailedException("Delete all not supported for entities with no ID"); + } + + if (entityParameter == null && entitiesParameter == null && parameters.length != 0) { + return new DeleteCriteriaMethodMatch(List.of(), false); + } + + ParameterElement finalEntityParameter = entityParameter; + ParameterElement finalEntitiesParameter = entitiesParameter; + + return new DeleteCriteriaMethodMatch(List.of(), false) { + + @Override + protected boolean supportedByImplicitQueries() { + return finalEntityParameter != null || finalEntitiesParameter != null; + } + + @Override + protected ParameterElement getEntityParameter() { + return finalEntityParameter; + } + + @Override + protected ParameterElement getEntitiesParameter() { + return finalEntitiesParameter; + } + + }; + } + return null; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/FindAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/FindAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..6ba60cc1f3 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/FindAnnotatedMethodMatcher.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.Find; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.FindMethodMatcher; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.inject.processing.ProcessingException; + +import java.util.List; + +/** + * The Find annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class FindAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + if (matchContext.getMethodElement().hasStereotype(Find.class)) { + if (matchContext.getRootEntity() == null) { + throw new ProcessingException(matchContext.getMethodElement(), "Repository does not have a well-defined primary entity type"); + } + return FindMethodMatcher.by(List.of()); + } + return null; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/InsertAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/InsertAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..6fb33504f3 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/InsertAnnotatedMethodMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.Insert; +import io.micronaut.data.intercept.annotation.DataMethod; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.data.processor.visitors.finders.SaveMethodMatcher; +import io.micronaut.data.processor.visitors.finders.TypeUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.processing.ProcessingException; + +/** + * The Insert annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class InsertAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + if (matchContext.getMethodElement().hasStereotype(Insert.class)) { + if (matchContext.getRootEntity() == null) { + throw new ProcessingException(matchContext.getMethodElement(), "Repository does not have a well-defined primary entity type"); + } + MethodElement methodElement = matchContext.getMethodElement(); + boolean producesAnEntity = TypeUtils.doesMethodProducesAnEntityIterableOfAnEntity(methodElement); + if (!TypeUtils.doesReturnVoid(methodElement) + && !TypeUtils.doesMethodProducesANumber(methodElement) + && !producesAnEntity) { + ClassElement producingItem = TypeUtils.getMethodProducingItemType(methodElement); + throw new ProcessingException(methodElement, "Unsupported return type for a save method: " + producingItem.getName()); + } + return SaveMethodMatcher.saveEntity(DataMethod.OperationType.INSERT); + } + return null; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/JakartaDataQueryAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/JakartaDataQueryAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..2853f09a20 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/JakartaDataQueryAnnotatedMethodMatcher.java @@ -0,0 +1,246 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.TypeRole; +import io.micronaut.data.intercept.annotation.DataMethod; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCommonAbstractCriteria; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaUpdate; +import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityCriteriaDelete; +import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityCriteriaUpdate; +import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityQuery; +import io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery; +import io.micronaut.data.model.query.builder.QueryBuilder; +import io.micronaut.data.model.query.builder.QueryResult; +import io.micronaut.data.processor.jdql.JDQLCriteriaBuilderUtils; +import io.micronaut.data.processor.model.SourcePersistentEntity; +import io.micronaut.data.processor.model.criteria.impl.MethodMatchSourcePersistentEntityCriteriaBuilderImpl; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.AbstractCriteriaMethodMatch; +import io.micronaut.data.processor.visitors.finders.FindersUtils; +import io.micronaut.data.processor.visitors.finders.MethodMatchInfo; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.processing.ProcessingException; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * The Jakarta Data Query annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class JakartaDataQueryAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + Optional jdqlQuery = matchContext.getMethodElement().stringValue("jakarta.data.repository.Query"); + if (jdqlQuery.isPresent()) { + + Function findClassElementFn = name -> { + SourcePersistentEntity rootEntity = matchContext.getRootEntity(); + if (rootEntity.getSimpleName().equals(name)) { + return rootEntity.getClassElement(); + } + SourcePersistentEntity persistentEntity = matchContext.getEntityBySimplyNameResolver().apply(name); + if (persistentEntity != null) { + return persistentEntity.getClassElement(); + } + return matchContext.getVisitorContext().getClassElement(name) + .orElseThrow(() -> new ProcessingException(matchContext.getMethodElement(), "Unable to find an entity: " + name)); + }; + PersistentEntityCommonAbstractCriteria criteriaQuery = JDQLCriteriaBuilderUtils.build( + jdqlQuery.get(), + matchContext.getRootEntity(), + matchContext.getMethodElement(), + findClassElementFn, + new MethodMatchSourcePersistentEntityCriteriaBuilderImpl(matchContext) + ); + matchContext.setRootEntity((SourcePersistentEntity) criteriaQuery.getPersistentEntity()); + + if (criteriaQuery instanceof PersistentEntityCriteriaUpdate) { + return getUpdateQuery(criteriaQuery); + } + if (criteriaQuery instanceof PersistentEntityCriteriaDelete) { + return getDeleteQuery(criteriaQuery); + } + if (criteriaQuery instanceof PersistentEntityCriteriaQuery) { + return getSelectQuery(criteriaQuery, jdqlQuery.get(), findClassElementFn); + } + return null; + } + return null; + } + + private static AbstractCriteriaMethodMatch getUpdateQuery(PersistentEntityCommonAbstractCriteria criteriaQuery) { + return new AbstractCriteriaMethodMatch(List.of()) { + @Override + protected DataMethod.OperationType getOperationType() { + return DataMethod.OperationType.UPDATE; + } + + @Override + protected MethodMatchInfo build(MethodMatchContext matchContext) { + FindersUtils.InterceptorMatch interceptorMatch = resolveReturnTypeAndInterceptor(matchContext); + ClassElement resultType = interceptorMatch.returnType(); + boolean isDto = false; + ClassElement interceptorType = interceptorMatch.interceptor(); + + AbstractPersistentEntityCriteriaUpdate query = (AbstractPersistentEntityCriteriaUpdate) criteriaQuery; + + boolean optimisticLock = query.hasVersionRestriction(); + + final AnnotationMetadataHierarchy annotationMetadataHierarchy = new AnnotationMetadataHierarchy( + matchContext.getRepositoryClass().getAnnotationMetadata(), + matchContext.getAnnotationMetadata() + ); + QueryBuilder queryBuilder = matchContext.getQueryBuilder(); + + QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaQuery).buildQuery(annotationMetadataHierarchy, queryBuilder); + + return new MethodMatchInfo( + getOperationType(), + resultType, + interceptorType + ) + .dto(isDto) + .optimisticLock(optimisticLock) + .queryResult(queryResult); + } + }; + } + + private static AbstractCriteriaMethodMatch getDeleteQuery(PersistentEntityCommonAbstractCriteria criteriaQuery) { + return new AbstractCriteriaMethodMatch(List.of()) { + + @Override + protected DataMethod.OperationType getOperationType() { + return DataMethod.OperationType.DELETE; + } + + @Override + protected MethodMatchInfo build(MethodMatchContext matchContext) { + FindersUtils.InterceptorMatch interceptorMatch = resolveReturnTypeAndInterceptor(matchContext); + ClassElement resultType = interceptorMatch.returnType(); + ClassElement interceptorType = interceptorMatch.interceptor(); + + boolean optimisticLock = ((AbstractPersistentEntityCriteriaDelete) criteriaQuery).hasVersionRestriction(); + + final AnnotationMetadataHierarchy annotationMetadataHierarchy = new AnnotationMetadataHierarchy( + matchContext.getRepositoryClass().getAnnotationMetadata(), + matchContext.getAnnotationMetadata() + ); + + QueryBuilder queryBuilder = matchContext.getQueryBuilder(); + QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaQuery).buildQuery(annotationMetadataHierarchy, queryBuilder); + + return new MethodMatchInfo( + getOperationType(), + resultType, + interceptorType + ) + .optimisticLock(optimisticLock) + .queryResult(queryResult); + } + }; + } + + private static AbstractCriteriaMethodMatch getSelectQuery(PersistentEntityCommonAbstractCriteria criteriaQuery, + String query, + Function findClassElementFn) { + return new AbstractCriteriaMethodMatch(List.of()) { + + @Override + protected DataMethod.OperationType getOperationType() { + return DataMethod.OperationType.QUERY; + } + + @Override + protected MethodMatchInfo build(MethodMatchContext matchContext) { + FindersUtils.InterceptorMatch interceptorMatch = resolveReturnTypeAndInterceptor(matchContext); + ClassElement resultType = interceptorMatch.returnType(); + ClassElement interceptorType = interceptorMatch.interceptor(); + + final AnnotationMetadataHierarchy annotationMetadataHierarchy = new AnnotationMetadataHierarchy( + matchContext.getRepositoryClass().getAnnotationMetadata(), + matchContext.getAnnotationMetadata() + ); + + QueryBuilder queryBuilder = matchContext.getQueryBuilder(); + QueryResultPersistentEntityCriteriaQuery persistentEntityCriteriaQuery = (QueryResultPersistentEntityCriteriaQuery) criteriaQuery; + + if (matchContext.hasParameterInRole(TypeRole.PAGEABLE)) { + Element pageableParameter = matchContext.findParameterInRole(TypeRole.PAGEABLE); + AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(pageableParameter), TypeRole.PAGEABLE); + } else if (matchContext.hasParameterInRole(TypeRole.SORT)) { + Element sortParameter = matchContext.findParameterInRole(TypeRole.SORT); + AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(sortParameter), TypeRole.SORT); + } else if (matchContext.hasParameterInRole(TypeRole.LIMIT)) { + Element limitParameter = matchContext.findParameterInRole(TypeRole.LIMIT); + AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(limitParameter), TypeRole.LIMIT); + } + + QueryResult queryResult = persistentEntityCriteriaQuery.buildQuery(annotationMetadataHierarchy, queryBuilder); + + ClassElement genericReturnType = matchContext.getReturnType(); + if (matchContext.isTypeInRole(genericReturnType, TypeRole.PAGE) + || matchContext.isTypeInRole(genericReturnType, TypeRole.CURSORED_PAGE)) { + + PersistentEntityCommonAbstractCriteria countCriteriaQuery = JDQLCriteriaBuilderUtils.buildCount( + query, + matchContext.getRootEntity(), + matchContext.getMethodElement(), + findClassElementFn, + new MethodMatchSourcePersistentEntityCriteriaBuilderImpl(matchContext) + ); + + QueryResult countQueryResult = ((QueryResultPersistentEntityCriteriaQuery) countCriteriaQuery).buildQuery(annotationMetadataHierarchy, queryBuilder); + return new MethodMatchInfo( + getOperationType(), + resultType, + interceptorType + ) + .queryResult(queryResult) + .countQueryResult(countQueryResult); + } + + return new MethodMatchInfo( + getOperationType(), + resultType, + interceptorType + ) + .queryResult(queryResult); + } + }; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/SaveAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/SaveAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..2b3d19b0d5 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/SaveAnnotatedMethodMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.Save; +import io.micronaut.data.intercept.annotation.DataMethod; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.data.processor.visitors.finders.SaveMethodMatcher; +import io.micronaut.data.processor.visitors.finders.TypeUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.processing.ProcessingException; + +/** + * The Insert annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class SaveAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + if (matchContext.getMethodElement().hasStereotype(Save.class)) { + if (matchContext.getRootEntity() == null) { + throw new ProcessingException(matchContext.getMethodElement(), "Repository does not have a well-defined primary entity type"); + } + MethodElement methodElement = matchContext.getMethodElement(); + boolean producesAnEntity = TypeUtils.doesMethodProducesAnEntityIterableOfAnEntity(methodElement); + if (!TypeUtils.doesReturnVoid(methodElement) + && !TypeUtils.doesMethodProducesANumber(methodElement) + && !producesAnEntity) { + ClassElement producingItem = TypeUtils.getMethodProducingItemType(methodElement); + throw new ProcessingException(methodElement, "Unsupported return type for a save method: " + producingItem.getName()); + } + return SaveMethodMatcher.saveEntity(DataMethod.OperationType.INSERT); + } + return null; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/UpdateAnnotatedMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/UpdateAnnotatedMethodMatcher.java new file mode 100644 index 0000000000..d1670d7929 --- /dev/null +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/annotated/UpdateAnnotatedMethodMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2025 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.processor.visitors.finders.annotated; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.annotation.Update; +import io.micronaut.data.processor.visitors.MethodMatchContext; +import io.micronaut.data.processor.visitors.finders.MethodMatcher; +import io.micronaut.data.processor.visitors.finders.TypeUtils; +import io.micronaut.data.processor.visitors.finders.UpdateMethodMatcher; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.processing.ProcessingException; + +import java.util.Arrays; +import java.util.Collections; + +/** + * The Update annotation matcher. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public final class UpdateAnnotatedMethodMatcher implements MethodMatcher { + + @Override + public MethodMatch match(MethodMatchContext matchContext) { + if (matchContext.getMethodElement().hasStereotype(Update.class)) { + if (matchContext.getRootEntity() == null) { + throw new ProcessingException(matchContext.getMethodElement(), "Repository does not have a well-defined primary entity type"); + } + ParameterElement[] parameters = matchContext.getParameters(); + final ParameterElement entityParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isEntity(p.getGenericType())).findFirst().orElse(null); + final ParameterElement entitiesParameter = Arrays.stream(parameters).filter(p -> TypeUtils.isIterableOfEntity(p.getGenericType())).findFirst().orElse(null); + + if (entityParameter != null || entitiesParameter != null) { + return UpdateMethodMatcher.entityUpdate(Collections.emptyList(), entityParameter, entitiesParameter, false); + } + throw new ProcessingException(matchContext.getMethodElement(), "Update method should include an entity to update"); + } + return null; + } + + @Override + public int getOrder() { + return MethodMatcher.DEFAULT_POSITION - 3000; + } +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java index e077c7082d..3f1183a7b9 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java @@ -21,6 +21,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.Join; +import io.micronaut.data.annotation.OrderBy; import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.model.Embedded; @@ -29,7 +30,6 @@ import io.micronaut.data.model.jpa.criteria.PersistentEntityQuery; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; import io.micronaut.data.model.jpa.criteria.PersistentEntitySubquery; -import io.micronaut.data.model.jpa.criteria.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityCriteriaQuery; import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityQuery; import io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery; @@ -53,13 +53,16 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Order; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Selection; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -93,7 +96,7 @@ public QueryCriteriaMethodMatch(List matches) { protected PersistentEntityCriteriaQuery createQuery(MethodMatchContext matchContext, PersistentEntityCriteriaBuilder cb, List> joinSpecs) { - Element paginationParameter = matchContext.getParametersInRole().get(TypeRole.PAGEABLE); + Element paginationParameter = matchContext.findParameterInRole(TypeRole.PAGEABLE); boolean isPageable = matchContext.hasParameterInRole(TypeRole.PAGEABLE); SourcePersistentEntity persistentEntity = matchContext.getRootEntity(); PersistentEntityCriteriaQuery criteriaQuery; @@ -104,11 +107,15 @@ protected PersistentEntityCriteriaQuery createQuery(MethodMatchContext m criteriaQuery = createDefaultQuery(matchContext, cb, joinSpecs); if (isPageable) { AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; - abstractPersistentEntityQuery.getParametersInRole().put(TypeRole.PAGEABLE, List.of(matchContext.getParameters()).indexOf(paginationParameter)); + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(paginationParameter), TypeRole.PAGEABLE); } else if (matchContext.hasParameterInRole(TypeRole.SORT)) { - Element sortParameter = matchContext.getParametersInRole().get(TypeRole.SORT); + Element sortParameter = matchContext.findParameterInRole(TypeRole.SORT); AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; - abstractPersistentEntityQuery.getParametersInRole().put(TypeRole.SORT, List.of(matchContext.getParameters()).indexOf(sortParameter)); + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(sortParameter), TypeRole.SORT); + } else if (matchContext.hasParameterInRole(TypeRole.LIMIT)) { + Element limitParameter = matchContext.findParameterInRole(TypeRole.LIMIT); + AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) criteriaQuery; + abstractPersistentEntityQuery.getParametersInRole().put(List.of(matchContext.getParameters()).indexOf(limitParameter), TypeRole.LIMIT); } } return criteriaQuery; @@ -134,6 +141,7 @@ private PersistentEntityCriteriaQuery createDefaultQuery(MethodMatchCont applyProjection(matchContext, cb, root, query); applyPredicate(matchContext, cb, root, query); applyOrder(cb, root, query); + applyOrderByAnnotation(cb, root, query, matchContext.getMethodElement()); applyForUpdate(query); applyLimit(query); applyJoinSpecs(root, joinSpecs); @@ -174,7 +182,7 @@ private PersistentEntityCriteriaQuery createQueryWithJoinsAndPagination( // Apply pagination and sort to do subquery // NOTE: Sort shouldn't be applied if unpaged AbstractPersistentEntityQuery abstractPersistentEntityQuery = (AbstractPersistentEntityQuery) paginationSubquery; - abstractPersistentEntityQuery.getParametersInRole().put(TypeRole.PAGEABLE_REQUIRED, pageableParameterIndex); + abstractPersistentEntityQuery.getParametersInRole().put(pageableParameterIndex, TypeRole.PAGEABLE_REQUIRED); PersistentEntitySubquery filteredSubquery = paginationSubquery.subquery(mainRoot.getExpressionType()); PersistentEntityRoot filteredRoot = filteredSubquery.from(matchContext.getRootEntity()); @@ -185,8 +193,10 @@ private PersistentEntityCriteriaQuery createQueryWithJoinsAndPagination( applyProjection(matchContext, cb, mainRoot, mainQuery); applyPredicate(matchContext, cb, filteredRoot, filteredSubquery); - applyOrder(cb, filteredRoot, filteredSubquery); +// applyOrder(cb, filteredRoot, filteredSubquery); +// applyOrderByAnnotation(cb, filteredRoot, filteredSubquery, matchContext.getMethodElement()); applyOrder(cb, mainRoot, mainQuery); + applyOrderByAnnotation(cb, mainRoot, mainQuery, matchContext.getMethodElement()); applyForUpdate(mainQuery); @@ -199,7 +209,7 @@ private PersistentEntityCriteriaQuery createQueryWithJoinsAndPagination( // Sort last query AbstractPersistentEntityQuery mainEntityQuery = (AbstractPersistentEntityQuery) mainQuery; - mainEntityQuery.getParametersInRole().put(TypeRole.SORT, pageableParameterIndex); + mainEntityQuery.getParametersInRole().put(pageableParameterIndex, TypeRole.SORT); return mainQuery; } @@ -247,8 +257,53 @@ private void applyForUpdate(PersistentEntityCriteriaQuery query) { private void applyOrder(PersistentEntityCriteriaBuilder cb, PersistentEntityRoot root, PersistentEntityQuery query) { - findMatchPart(matches, QueryMatchId.ORDER) - .ifPresent(text -> applyOrderBy(text, root, query, cb)); + findMatchPart(matches, QueryMatchId.ORDER).ifPresent(text -> applyOrderBy(text, root, query, cb)); + } + + private void applyOrderByAnnotation(PersistentEntityCriteriaBuilder cb, + PersistentEntityRoot root, + PersistentEntityQuery query, + AnnotationMetadata annotationMetadata) { + List orders = new ArrayList<>(); + for (AnnotationValue av : annotationMetadata.getAnnotationValuesByStereotype(OrderBy.class.getName())) { + orders.add(cb.sort( + findOrderProperty(root, av.stringValue().orElseThrow()), + !av.booleanValue("descending").orElse(false), + av.booleanValue("ignoreCase").orElse(false) + )); + } + if (!orders.isEmpty()) { + query.orderBy(orders); + } + } + + private Expression findOrderProperty(PersistentEntityRoot root, String propertyName) { + if (root.getPersistentEntity().getPropertyByName(propertyName) != null) { + return root.get(propertyName); + } + // Look at association paths + io.micronaut.data.model.jpa.criteria.PersistentPropertyPath property = findProperty(root, propertyName); + if (property != null) { + return property; + } + Path path = root; + for (Iterator iterator = StringUtils.splitOmitEmptyStrings(propertyName, '.').iterator(); path != null && iterator.hasNext(); ) { + String next = iterator.next(); + if (iterator.hasNext()) { + path = ((From) path).join(next); + } else { + try { + path = path.get(next); + } catch (Exception e) { + // Ignore + path = null; + } + } + } + if (path == null) { + throw new MatchFailedException("Cannot order by non-existent property: " + propertyName); + } + return path; } private void applyDistinct(PersistentEntityCriteriaQuery mainQuery) { @@ -429,18 +484,6 @@ private void applyOrderBy(String orderBy, } } - private PersistentPropertyPath findOrderProperty(PersistentEntityRoot root, String propertyName) { - if (root.getPersistentEntity().getPropertyByName(propertyName) != null) { - return root.get(propertyName); - } - // Look at association paths - PersistentPropertyPath property = findProperty(root, propertyName); - if (property != null) { - return property; - } - throw new MatchFailedException("Cannot order by non-existent property: " + propertyName); - } - /** * Apply projections. * diff --git a/data-processor/src/main/resources/META-INF/services/io.micronaut.data.processor.visitors.finders.MethodMatcher b/data-processor/src/main/resources/META-INF/services/io.micronaut.data.processor.visitors.finders.MethodMatcher index 0933cd0023..518208a7f0 100644 --- a/data-processor/src/main/resources/META-INF/services/io.micronaut.data.processor.visitors.finders.MethodMatcher +++ b/data-processor/src/main/resources/META-INF/services/io.micronaut.data.processor.visitors.finders.MethodMatcher @@ -13,5 +13,10 @@ io.micronaut.data.processor.visitors.finders.FindMethodMatcher io.micronaut.data.processor.visitors.finders.CountMethodMatcher io.micronaut.data.processor.visitors.finders.UpdateMethodMatcher io.micronaut.data.processor.visitors.finders.SaveMethodMatcher -io.micronaut.data.processor.visitors.finders.SaveOneMethodMatcher io.micronaut.data.processor.visitors.finders.ProcedureMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.InsertAnnotatedMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.UpdateAnnotatedMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.DeleteAnnotatedMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.FindAnnotatedMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.SaveAnnotatedMethodMatcher +io.micronaut.data.processor.visitors.finders.annotated.JakartaDataQueryAnnotatedMethodMatcher diff --git a/data-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/data-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper index 3ac4921ce8..70fd13faf1 100644 --- a/data-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper +++ b/data-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -66,3 +66,14 @@ io.micronaut.data.processor.mappers.spring.SpringVersionMapper io.micronaut.data.processor.mappers.spring.SpringTransactionalEventListenerMapper io.micronaut.data.processor.mappers.spring.SpringRepositoryMapper io.micronaut.data.processor.mappers.jpa.jakarta.StaticMetamodelAnnotationMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataRepositoryMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataParamsMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataInsertMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataUpdateMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataDeleteMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataByMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataFindMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataSaveMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataOrderByMapper +io.micronaut.data.processor.mappers.jakarta.data.JakartaDataOrderByListMapper + diff --git a/data-processor/src/test/groovy/io/micronaut/data/model/jpa/criteria/CriteriaSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/model/jpa/criteria/CriteriaSpec.groovy index 0904c6a4fa..c5ddcaa12c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/model/jpa/criteria/CriteriaSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/model/jpa/criteria/CriteriaSpec.groovy @@ -47,7 +47,7 @@ class CriteriaSpec extends AbstractCriteriaSpec { void setup() { testEntityElement = buildCustomElement() - criteriaBuilder = new SourcePersistentEntityCriteriaBuilderImpl(entityResolver, criteriaBuilder) + criteriaBuilder = new SourcePersistentEntityCriteriaBuilderImpl(entityResolver) criteriaQuery = criteriaBuilder.createQuery() criteriaDelete = criteriaBuilder.createCriteriaDelete(null) criteriaUpdate = criteriaBuilder.createCriteriaUpdate(null) diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/jdql/JakartaDataQueryLanguageBuilderSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/jdql/JakartaDataQueryLanguageBuilderSpec.groovy new file mode 100644 index 0000000000..dce8c87394 --- /dev/null +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/jdql/JakartaDataQueryLanguageBuilderSpec.groovy @@ -0,0 +1,391 @@ +package io.micronaut.data.processor.jdql + +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.data.model.jpa.criteria.CriteriaSpec +import io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery +import io.micronaut.data.model.query.builder.QueryResult +import io.micronaut.data.model.query.builder.sql.Dialect +import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder2 +import io.micronaut.data.processor.model.SourcePersistentEntity +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaBuilder +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaDelete +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaQuery +import io.micronaut.data.processor.model.criteria.SourcePersistentEntityCriteriaUpdate +import io.micronaut.data.processor.model.criteria.impl.SourcePersistentEntityCriteriaBuilderImpl +import io.micronaut.inject.ast.ClassElement +import spock.lang.Specification + +import java.util.function.Function + +class JakartaDataQueryLanguageBuilderSpec extends Specification { + + SqlQueryBuilder2 queryBuilder = new SqlQueryBuilder2(Dialect.POSTGRES) + + SourcePersistentEntityCriteriaBuilder criteriaBuilder + + SourcePersistentEntityCriteriaQuery criteriaQuery + + SourcePersistentEntityCriteriaDelete criteriaDelete + + SourcePersistentEntityCriteriaUpdate criteriaUpdate + + Function entityResolver = new Function() { + + private Map entityMap = new HashMap<>() + + @Override + SourcePersistentEntity apply(ClassElement classElement) { + return entityMap.computeIfAbsent(classElement.getName(), { it -> + new SourcePersistentEntity(classElement, this) + }) + } + } + + Function classElementResolver = new Function() { + + private Map cache = new HashMap<>() + + @Override + ClassElement apply(String name) { + return cache.computeIfAbsent(name, { it -> + if (name == "Box") { + return buildBoxElement() + } + if (name == "Coordinate") { + return buildCoordinateElement() + } + if (name == "AsciiCharacter") { + return buildAsciiCharacter() + } + if (name == "NaturalNumber") { + return buildNaturalNumber() + } + throw new IllegalStateException("Unknown entity: " + name) + }) + } + } + + void setup() { + criteriaBuilder = new SourcePersistentEntityCriteriaBuilderImpl(entityResolver) + criteriaQuery = criteriaBuilder.createQuery() + criteriaDelete = criteriaBuilder.createCriteriaDelete(null) + criteriaUpdate = criteriaBuilder.createCriteriaUpdate(null) + } + + String transform(String q) { + return transform(null, q) + } + + String transform(String rootEntityName, String q) { + def root = new SourcePersistentEntity(classElementResolver.apply(rootEntityName ?: "Box"), (x) -> null) + def query = JDQLCriteriaBuilderUtils.build( + q, root, null, classElementResolver, criteriaBuilder + ) + QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) query) + .buildQuery(AnnotationMetadata.EMPTY_METADATA, queryBuilder) + return queryResult.query + } + + def 'test delete query'() { + when: + def result = transform(jdql) + then: + result == sql + where: + jdql << [ + "DELETE FROM Box", + "DELETE FROM Coordinate WHERE x > 0.0d AND y > 0.0f" + ] + sql << [ + """DELETE FROM "box" """, + """DELETE FROM "coordinate" WHERE ("x" > 0 AND "y" > 0)""" + ] + } + + def 'test update query'() { + when: + def result = transform(jdql) + then: + result == sql + where: + jdql << [ + "UPDATE Coordinate SET x = :newX, y = y / :yDivisor WHERE id = :id", + "UPDATE Box SET length = length + ?1, width = width - ?1, height = height * ?2" + ] + sql << [ + """UPDATE "coordinate" SET "x"=?,"y"="y" / ? WHERE ("id" = ?)""", + """UPDATE "box" SET "length"="length" + ?,"width"="width" - ?,"height"="height" * ?""" + ] + } + + def 'test select'() { + when: + def result = transform(rootEntityName, jdql) + then: + result == sql + where: + rootEntityName << ["Box", "AsciiCharacter", "NaturalNumber", "Box"] + jdql << [ + "WHERE id = :id", + "select thisCharacter where hexadecimal like '4_' and hexadecimal not like '%0' and thisCharacter not in ('E', 'G') and id not between 72 and 78 order by id asc", + "WHERE isOdd = false AND numType = test.NaturalNumber.NumberType.PRIME", + "WHERE LENGTH(name) = ?1 AND length < ?2 ORDER BY name" + ] + sql << [ + """SELECT box_."id",box_."name",box_."length",box_."width",box_."height" FROM "box" box_ WHERE (box_."id" = ?)""", + """SELECT ascii_character_."this_character" FROM "ascii_character" ascii_character_ WHERE (ascii_character_."hexadecimal" LIKE '4_' AND ascii_character_."hexadecimal" NOT LIKE '%0' AND ascii_character_."this_character" NOT IN ('E','G') AND NOT((ascii_character_."id" >= 72 AND ascii_character_."id" <= 78))) ORDER BY ascii_character_."id" ASC""", + """SELECT natural_number_."id",natural_number_."odd",natural_number_."num_bits_required",natural_number_."num_type",natural_number_."num_type_ordinal",natural_number_."floor_of_square_root",natural_number_."is_odd" FROM "natural_number" natural_number_ WHERE (natural_number_."is_odd" = FALSE AND natural_number_."num_type" = 'PRIME')""", + """SELECT box_."id",box_."name",box_."length",box_."width",box_."height" FROM "box" box_ WHERE (LENGTH(box_."name") = ? AND box_."length" < ?) ORDER BY box_."name" ASC""" + ] + } + + private static ClassElement buildBoxElement() { + new CriteriaSpec.CustomAbstractDataSpec().buildClassElement(""" +package test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +class Box { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private long length; + private long width; + private long height; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getLength() { + return length; + } + + public void setLength(long length) { + this.length = length; + } + + public long getWidth() { + return width; + } + + public void setWidth(long width) { + this.width = width; + } + + public long getHeight() { + return height; + } + + public void setHeight(long height) { + this.height = height; + } +} + +""") + } + + private static ClassElement buildCoordinateElement() { + new CriteriaSpec.CustomAbstractDataSpec().buildClassElement(""" +package test; + +import io.micronaut.core.annotation.Introspected; +import java.util.UUID; + +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +@jakarta.persistence.Entity +class Coordinate { + @jakarta.persistence.Id + public UUID id; + + public double x; + + public float y; + + public static Coordinate of(String id, double x, float y) { + Coordinate c = new Coordinate(); + c.id = UUID.nameUUIDFromBytes(id.getBytes()); + c.x = x; + c.y = y; + return c; + } + + @Override + public String toString() { + return "Coordinate@" + Integer.toHexString(hashCode()) + "(" + x + "," + y + ")" + ":" + id; + } +} + +""") + } + + private static ClassElement buildAsciiCharacter() { + new CriteriaSpec.CustomAbstractDataSpec().buildClassElement(""" +package test; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +class AsciiCharacter implements Serializable { + private static final long serialVersionUID = 1L; + + @jakarta.persistence.Id + private long id; + + private int numericValue; + + private String hexadecimal; + + private char thisCharacter; + + private boolean isControl; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getNumericValue() { + return numericValue; + } + + public void setNumericValue(int numericValue) { + this.numericValue = numericValue; + } + + public String getHexadecimal() { + return hexadecimal; + } + + public void setHexadecimal(String hexadecimal) { + this.hexadecimal = hexadecimal; + } + + public char getThisCharacter() { + return thisCharacter; + } + + public void setThisCharacter(char thisCharacter) { + this.thisCharacter = thisCharacter; + } + + public boolean isControl() { + return isControl; + } + + public void setControl(boolean isControl) { + this.isControl = isControl; + } + +} +""") + } + + private static ClassElement buildNaturalNumber() { + new CriteriaSpec.CustomAbstractDataSpec().buildClassElement(""" +package test; + +import io.micronaut.core.annotation.Introspected; + +import java.io.Serializable; + +@jakarta.persistence.Entity +@Introspected(accessKind = {Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}, visibility = Introspected.Visibility.ANY) +class NaturalNumber implements Serializable { + private static final long serialVersionUID = 1L; + + public enum NumberType { + ONE, PRIME, COMPOSITE + } + + @jakarta.persistence.Id + private long id; //AKA the value + + private boolean isOdd; + + private Short numBitsRequired; + + // Sorting on enum types is vendor-specific in Jakarta Data. + // Use numTypeOrdinal for sorting instead. + @jakarta.persistence.Enumerated(jakarta.persistence.EnumType.STRING) + private NumberType numType; // enum of ONE | PRIME | COMPOSITE + + private int numTypeOrdinal; // ordinal value of numType + + private long floorOfSquareRoot; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public boolean isOdd() { + return isOdd; + } + + public void setOdd(boolean isOdd) { + this.isOdd = isOdd; + } + + public Short getNumBitsRequired() { + return numBitsRequired; + } + + public void setNumBitsRequired(Short numBitsRequired) { + this.numBitsRequired = numBitsRequired; + } + + public NumberType getNumType() { + return numType; + } + + public void setNumType(NumberType numType) { + this.numType = numType; + } + + public int getNumTypeOrdinal() { + return numTypeOrdinal; + } + + public void setNumTypeOrdinal(int value) { + numTypeOrdinal = value; + } + + public long getFloorOfSquareRoot() { + return floorOfSquareRoot; + } + + public void setFloorOfSquareRoot(long floorOfSquareRoot) { + this.floorOfSquareRoot = floorOfSquareRoot; + } +} +""") + } + +} diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index c2e0d1626e..17990be93c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -28,13 +28,11 @@ import io.micronaut.data.model.entities.Invoice import io.micronaut.data.model.query.QueryModel import io.micronaut.data.model.query.builder.sql.Dialect import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder -import io.micronaut.data.model.runtime.StoredQuery import io.micronaut.data.processor.entity.ActivityPeriodEntity import io.micronaut.data.processor.visitors.AbstractDataSpec import io.micronaut.data.tck.entities.Author import io.micronaut.data.tck.entities.Restaurant import io.micronaut.data.tck.jdbc.entities.EmployeeGroup -import io.micronaut.inject.ExecutableMethod import spock.lang.Issue import spock.lang.PendingFeature import spock.lang.Unroll @@ -49,8 +47,8 @@ import static io.micronaut.data.processor.visitors.TestUtils.getParameterBinding import static io.micronaut.data.processor.visitors.TestUtils.getParameterBindingPaths import static io.micronaut.data.processor.visitors.TestUtils.getParameterExpressions import static io.micronaut.data.processor.visitors.TestUtils.getParameterPropertyPaths +import static io.micronaut.data.processor.visitors.TestUtils.getParameterRoles import static io.micronaut.data.processor.visitors.TestUtils.getQuery -import static io.micronaut.data.processor.visitors.TestUtils.getQueryParts import static io.micronaut.data.processor.visitors.TestUtils.getRawQuery import static io.micronaut.data.processor.visitors.TestUtils.getResultDataType import static io.micronaut.data.processor.visitors.TestUtils.isExpandableQuery @@ -2087,6 +2085,60 @@ interface TestRepository extends GenericRepository { countQueryAnnotation.getAnnotations("parameters").size() == 1 } + void "test LIMIT method"() { + given: + def repository = buildRepository('test.TestRepository', """ + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.Limit; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.tck.entities.Book; +import io.micronaut.data.tck.entities.Author; +import io.micronaut.data.model.query.builder.sql.Dialect; + +import java.util.List; + +@JdbcRepository(dialect = Dialect.POSTGRES) +interface TestRepository extends GenericRepository { + List findAll(Limit limit); +} + +""") + def findAll = repository.findPossibleMethods("findAll").findFirst().get() + expect: + getQuery(findAll) == """SELECT book_."id",book_."author_id",book_."genre_id",book_."title",book_."total_pages",book_."publisher_id",book_."last_updated" FROM "book" book_""" + isExpandableQuery(findAll) + getParameterRoles(findAll) == ["querylimit"] + } + + void "test JOIN query method"() { + given: + def repository = buildRepository('test.TestRepository', """ + +import io.micronaut.data.annotation.OrderBy; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.tck.entities.Book; +import io.micronaut.data.tck.entities.Author; +import io.micronaut.data.model.query.builder.sql.Dialect; + +import java.util.List; + +@JdbcRepository(dialect = Dialect.H2) +interface TestRepository extends GenericRepository { + @Join("author") + @OrderBy("author.name") + @OrderBy("title") + Page findAll(Pageable pageable); +} + +""") + def findAll = repository.findPossibleMethods("findAll").findFirst().get() + expect: + getQuery(findAll) == """SELECT book_.`id`,book_.`author_id`,book_.`genre_id`,book_.`title`,book_.`total_pages`,book_.`publisher_id`,book_.`last_updated`,book_author_.`name` AS author_name,book_author_.`nick_name` AS author_nick_name FROM `book` book_ INNER JOIN `author` book_author_ ON book_.`author_id`=book_author_.`id` WHERE (book_.`id` IN (SELECT book_book_.`id` FROM `book` book_book_ WHERE (book_book_.`id` IN (SELECT book_book_book_.`id` FROM `book` book_book_book_ INNER JOIN `author` book_book_book_author_ ON book_book_book_.`author_id`=book_book_book_author_.`id`))""" + getParameterRoles(findAll) == ["pageableRequired", "sort"] + } + void "test repository with reused embedded entity"() { when: buildRepository('test.TestRepository', """ diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy index 4570a60f6f..81d8c1a67c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy @@ -99,6 +99,10 @@ class TestUtils { return getParameterValues(metadata.getAnnotation(DataMethod)) } + static String[] getParameterRoles(AnnotationMetadataProvider metadata) { + return getParameterRoles(metadata.getAnnotation(DataMethod)) + } + static DataType[] getDataTypes(AnnotationMetadataProvider metadata) { return getDataTypes(metadata.getAnnotation(DataMethod)) } @@ -128,6 +132,15 @@ class TestUtils { .toArray(DataType[]::new) } + static String[] getParameterRoles(AnnotationValue annotationValue) { + return annotationValue.getAnnotations(DataMethod.META_MEMBER_PARAMETERS, DataMethodQueryParameter) + .stream() + .map(p -> { + p.stringValue(DataMethodQueryParameter.META_MEMBER_ROLE).orElse(null) + }) + .toArray(String[]::new) + } + static boolean anyParameterExpandable(AnnotationMetadataProvider metadata) { return metadata.getAnnotation(DataMethod).getAnnotations(DataMethod.META_MEMBER_PARAMETERS, DataMethodQueryParameter) .stream() diff --git a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java index 31eb38172c..4de03a7a9e 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java @@ -35,9 +35,11 @@ import io.micronaut.data.connection.reactive.ReactorConnectionOperations; import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.exceptions.NonUniqueResultException; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.model.runtime.AttributeConverterRegistry; import io.micronaut.data.model.runtime.DeleteBatchOperation; @@ -81,6 +83,7 @@ import io.micronaut.data.runtime.operations.internal.ReactiveCascadeOperations; import io.micronaut.data.runtime.operations.internal.query.BindableParametersStoredQuery; import io.micronaut.data.runtime.operations.internal.sql.AbstractSqlRepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import io.micronaut.data.runtime.operations.internal.sql.SqlJsonColumnMapperProvider; import io.micronaut.data.runtime.operations.internal.sql.SqlPreparedQuery; import io.micronaut.data.runtime.operations.internal.sql.SqlStoredQuery; @@ -807,6 +810,31 @@ public Mono findOptional(@NonNull Class type, @NonNull Object id) { @NonNull @Override public Mono> findPage(@NonNull PagedQuery pagedQuery) { + if (pagedQuery instanceof PreparedQuery pg) { + PreparedQuery preparedQuery = (PreparedQuery) pg; + Pageable pageable = preparedQuery.getPageable(); + return findAll(preparedQuery) + .collectList() + .map(results -> { + if (pageable.getMode() == Pageable.Mode.OFFSET) { + return Page.of(results, pageable, -1L); + } + if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors; + List resultList = (List) results; + if (preparedQuery.getResultDataType() == DataType.ENTITY) { + cursors = sqlPreparedQuery.createCursors(resultList, pageable); + } else if (sqlPreparedQuery.isDtoProjection()) { + RuntimePersistentEntity runtimePersistentEntity = (RuntimePersistentEntity) getEntity(sqlPreparedQuery.getResultType()); + cursors = sqlPreparedQuery.createCursors(resultList, pageable, runtimePersistentEntity); + } else { + throw new IllegalStateException("CursoredPage cannot produce projection result"); + } + return CursoredPage.of(results, pageable, cursors, -1L); + } + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + }); + } throw new UnsupportedOperationException("The findPage method is not supported. Execute the SQL query directly"); } diff --git a/data-runtime/build.gradle b/data-runtime/build.gradle index e4c90ce333..40ae34d149 100644 --- a/data-runtime/build.gradle +++ b/data-runtime/build.gradle @@ -14,8 +14,9 @@ dependencies { api projects.micronautDataTx compileOnly mn.micronaut.json.core + compileOnly libs.managed.jakarta.data.api - implementation mn.reactor + implementation mn.reactor implementation mnSql.jakarta.persistence.api compileOnly(libs.managed.javax.persistence.api) diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/convert/DataConversionServiceFactory.java b/data-runtime/src/main/java/io/micronaut/data/runtime/convert/DataConversionServiceFactory.java index e52fb64b43..33b47171df 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/convert/DataConversionServiceFactory.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/convert/DataConversionServiceFactory.java @@ -26,6 +26,8 @@ import io.micronaut.core.convert.TypeConverterRegistrar; import io.micronaut.core.type.Argument; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Pageable; import jakarta.inject.Singleton; import java.math.BigDecimal; @@ -65,6 +67,7 @@ final class DataConversionServiceFactory { @Bean(typed = DataConversionService.class) DataConversionServiceImpl build(@NonNull BeanContext beanContext) { DataConversionServiceImpl conversionService = new DataConversionServiceImpl(beanContext.getConversionService()); + conversionService.addConverter(Pageable.class, Limit.class, Pageable::getLimit); conversionService.addConverter(Enum.class, Number.class, Enum::ordinal); conversionService.addConverter(Number.class, Enum.class, (index, targetType, context) -> { Enum[] enumConstants = targetType.getEnumConstants(); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/convert/JakartaDataConverters.java b/data-runtime/src/main/java/io/micronaut/data/runtime/convert/JakartaDataConverters.java new file mode 100644 index 0000000000..67409d1d24 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/convert/JakartaDataConverters.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017-2025 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.runtime.convert; + +import io.micronaut.context.annotation.Prototype; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.MutableConversionService; +import io.micronaut.core.convert.TypeConverterRegistrar; +import io.micronaut.data.model.CursoredPage; +import io.micronaut.data.model.CursoredPageable; +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.page.PageRequest; +import jakarta.data.page.impl.CursoredPageRecord; +import jakarta.data.page.impl.PageRecord; + +import java.util.Arrays; +import java.util.List; + +/** + * Jakarta Data converters. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Requires(classes = Order.class) +@Prototype +@Internal +final class JakartaDataConverters implements TypeConverterRegistrar { + + @Override + public void register(MutableConversionService conversionService) { + conversionService.addConverter(Limit.class, io.micronaut.data.model.Limit.class, + limit -> io.micronaut.data.model.Limit.of(limit.maxResults(), (int) limit.startAt() - 1)); + conversionService.addConverter(Order.class, Sort.class, order -> Sort.of( + ((Order) order).sorts().stream().map(sort -> new Sort.Order( + sort.property(), + sort.isAscending() ? Sort.Order.Direction.ASC : Sort.Order.Direction.DESC, + sort.ignoreCase()) + ).toList() + )); + conversionService.addConverter(jakarta.data.Sort.class, Sort.class, sort -> Sort.of( + new Sort.Order( + sort.property(), + sort.isAscending() ? Sort.Order.Direction.ASC : Sort.Order.Direction.DESC, + sort.ignoreCase()) + ) + ); + conversionService.addConverter(jakarta.data.Sort[].class, Sort.class, sort -> Sort.of( + Arrays.stream(sort).map(s -> new Sort.Order( + s.property(), + s.isAscending() ? Sort.Order.Direction.ASC : Sort.Order.Direction.DESC, + s.ignoreCase()) + ).toList() + ) + ); + conversionService.addConverter(jakarta.data.page.PageRequest.class, Pageable.class, pageRequest -> { + if (pageRequest.mode() == PageRequest.Mode.CURSOR_NEXT || pageRequest.mode() == PageRequest.Mode.CURSOR_PREVIOUS) { + return CursoredPageable.from( + (int) (pageRequest.page() - 1), + pageRequest.cursor().map(cursor -> Pageable.Cursor.of((List) cursor.elements())).orElse(null), + pageRequest.mode() == PageRequest.Mode.CURSOR_NEXT ? Pageable.Mode.CURSOR_NEXT : Pageable.Mode.CURSOR_PREVIOUS, + pageRequest.size(), + null, + pageRequest.requestTotal() + ); + } else { + Pageable pageable = Pageable.from((int) (pageRequest.page() - 1), pageRequest.size()); + if (pageRequest.requestTotal()) { + pageable = pageable.withTotal(); + } else { + pageable = pageable.withoutTotal(); + } + return pageable; + } + }); + conversionService.addConverter(Pageable.class, jakarta.data.page.PageRequest.class, JakartaDataConverters::convert); + conversionService.addConverter(Page.class, jakarta.data.page.Page.class, page -> + new PageRecord<>( + convert(page.getPageable()), + page.getContent(), + page.getPageable().requestTotal() ? page.getTotalSize() : -1 + ) + ); + conversionService.addConverter(CursoredPage.class, jakarta.data.page.CursoredPage.class, page -> { + CursoredPage cursoredPage = (CursoredPage) page; + return new CursoredPageRecord<>( + cursoredPage.getContent(), + cursoredPage.getCursors().stream().map(JakartaDataConverters::convertCursor).toList(), + cursoredPage.hasTotalSize() ? cursoredPage.getTotalSize() : -1L, + convert(cursoredPage.getPageable()), + convert(cursoredPage.nextPageable()), + convert(cursoredPage.previousPageable()) + ); + } + ); + } + + private static PageRequest.Cursor convertCursor(Pageable.Cursor c) { + return PageRequest.Cursor.forKey(c.elements().toArray()); + } + + private static PageRequest convert(CursoredPageable pageable) { + if (pageable == null) { + return null; + } + PageRequest.Cursor cursor = pageable.cursor().map(JakartaDataConverters::convertCursor).orElse(null); + if (cursor == null) { + return null; + } + if (pageable.getMode().equals(CursoredPageable.Mode.CURSOR_NEXT)) { + return PageRequest.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.requestTotal()); + } + if (pageable.getMode().equals(Pageable.Mode.CURSOR_PREVIOUS)) { + return PageRequest.beforeCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.requestTotal()); + } + throw new IllegalArgumentException("Unknown mode " + pageable.getMode()); + } + + private static PageRequest convert(Pageable pageable) { + if (pageable == null) { + return null; + } + return PageRequest.ofPage(pageable.getNumber() + 1, pageable.getSize() == -1 ? Integer.MAX_VALUE : pageable.getSize(), pageable.requestTotal()); + } + +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/exceptions/jakarta/data/MicronautDataJakartaDataExceptionConverter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/exceptions/jakarta/data/MicronautDataJakartaDataExceptionConverter.java new file mode 100644 index 0000000000..f8b0ef22da --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/exceptions/jakarta/data/MicronautDataJakartaDataExceptionConverter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2025 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.runtime.exceptions.jakarta.data; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.exceptions.MappingException; +import io.micronaut.data.exceptions.NonUniqueResultException; +import io.micronaut.data.exceptions.OptimisticLockException; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataDeleteExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataInsertExceptionConverter; +import io.micronaut.data.runtime.support.exceptions.jakarta.data.JakartaDataUpdateExceptionConverter; +import jakarta.data.exceptions.DataException; +import jakarta.data.exceptions.EmptyResultException; +import jakarta.data.exceptions.EntityExistsException; +import jakarta.inject.Singleton; + +/** + * The Micronaut Data to Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Singleton +@Requires(classes = jakarta.data.exceptions.OptimisticLockingFailureException.class) +final class MicronautDataJakartaDataExceptionConverter implements JakartaDataExceptionConverter, JakartaDataUpdateExceptionConverter, + JakartaDataDeleteExceptionConverter, JakartaDataInsertExceptionConverter { + + @Override + public Exception convert(Exception exception) { + if (exception instanceof OptimisticLockException) { + throw new jakarta.data.exceptions.OptimisticLockingFailureException(exception.getMessage(), exception); + } + if (exception instanceof NonUniqueResultException) { + throw new jakarta.data.exceptions.NonUniqueResultException(exception.getMessage(), exception); + } + if (exception instanceof io.micronaut.data.exceptions.EmptyResultException) { + throw new EmptyResultException(exception.getMessage(), exception); + } + if (exception instanceof MappingException) { + return new DataException(exception.getMessage(), exception); + } + if (exception instanceof DataAccessException e) { + if (e.getMessage().contains("Unique index or primary key violation")) { + throw new EntityExistsException(exception.getMessage(), exception); + } + return new DataException(exception.getMessage(), exception); + } + return exception; + } +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/AbstractQueryInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/AbstractQueryInterceptor.java index 7304239ea0..3677fbb859 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/AbstractQueryInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/AbstractQueryInterceptor.java @@ -30,7 +30,6 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; -import io.micronaut.core.type.MutableArgumentValue; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.data.annotation.Query; @@ -70,6 +69,7 @@ import io.micronaut.data.runtime.query.PreparedQueryResolver; import io.micronaut.data.runtime.query.StoredQueryDecorator; import io.micronaut.data.runtime.query.StoredQueryResolver; +import io.micronaut.data.runtime.query.internal.DefaultPreparedQuery; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -371,24 +371,21 @@ protected RT getRequiredParameterInRole(MethodInvocationContext conte * @return An optional result */ protected Optional getParameterInRole(MethodInvocationContext context, @NonNull String role, @NonNull Class type) { - return context.stringValue(DataMethod.NAME, role).flatMap(name -> { - RT parameterValue = null; - Map> params = context.getParameters(); - MutableArgumentValue arg = params.get(name); - if (arg != null) { - Object o = arg.getValue(); - if (o != null) { - if (type.isInstance(o)) { - //noinspection unchecked - parameterValue = (RT) o; - } else { - parameterValue = operations.getConversionService() - .convert(o, type).orElse(null); - } - } - } - return Optional.ofNullable(parameterValue); - }); + return DefaultPreparedQuery.getParameterInRole(role, type, context, conversionService); + } + + /** + * Retrieve a parameter in the given role for the given type. + * + * @param context The context + * @param role The role + * @param type The type + * @param The generic type + * @return An optional result + */ + @NonNull + protected List getParametersInRole(@NonNull MethodInvocationContext context, @NonNull String role, @NonNull Class type) { + return DefaultPreparedQuery.getParametersInRole(role, type, context, conversionService); } /** @@ -407,9 +404,10 @@ protected Pageable getPageable(MethodInvocationContext context) { if (limit > 0) { pageable = Pageable.from(0, limit); } - Sort sort = getParameterInRole(context, TypeRole.SORT, Sort.class).orElse(null); - if (sort != null) { - return pageable.orders(sort.getOrderBy()); + } + for (Sort sort : getParametersInRole(context, TypeRole.SORT, Sort.class)) { + if (pageable != sort) { + pageable = pageable.orders(sort.getOrderBy()); } } return pageable; @@ -943,6 +941,11 @@ public Optional getParameterInRole(@NonNull String role, @NonNull Cla return AbstractQueryInterceptor.this.getParameterInRole(method, role, type); } + @Override + public List getParametersInRole(String role, Class type) { + return AbstractQueryInterceptor.this.getParametersInRole(method, role, type); + } + @NonNull @Override public Class getRootEntity() { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DataInterceptorResolver.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DataInterceptorResolver.java index 7f96e90145..7051472a35 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DataInterceptorResolver.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DataInterceptorResolver.java @@ -16,16 +16,19 @@ package io.micronaut.data.runtime.intercept; import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.context.BeanLocator; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.data.annotation.ConvertException; import io.micronaut.core.type.Argument; import io.micronaut.data.annotation.Repository; import io.micronaut.data.annotation.RepositoryConfiguration; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.exceptions.ExceptionConverter; import io.micronaut.data.intercept.DataInterceptor; import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.intercept.annotation.DataMethod; @@ -35,6 +38,7 @@ import io.micronaut.data.runtime.multitenancy.DataSourceTenantResolver; import io.micronaut.inject.ArgumentInjectionPoint; import io.micronaut.inject.InjectionPoint; +import io.micronaut.transaction.support.ExceptionUtil; import jakarta.inject.Singleton; import java.lang.reflect.Modifier; @@ -57,10 +61,12 @@ public final class DataInterceptorResolver { @Nullable private final DataSourceTenantResolver tenantResolver; private final Map> interceptors = new ConcurrentHashMap<>(); + private final BeanLocator beanLocator; - DataInterceptorResolver(RepositoryOperationsRegistry repositoryOperationsRegistry, @Nullable DataSourceTenantResolver tenantResolver) { + DataInterceptorResolver(RepositoryOperationsRegistry repositoryOperationsRegistry, @Nullable DataSourceTenantResolver tenantResolver, BeanLocator beanLocator) { this.repositoryOperationsRegistry = repositoryOperationsRegistry; this.tenantResolver = tenantResolver; + this.beanLocator = beanLocator; } DataInterceptor resolve(@NonNull RepositoryMethodKey key, @@ -106,7 +112,27 @@ private DataInterceptor findDataInterceptor(MethodInvocationCont }); if (interceptorType != null && DataInterceptor.class.isAssignableFrom(interceptorType)) { - return findInterceptor(dataSourceName, operationsType, interceptorType); + DataInterceptor interceptor = findInterceptor(dataSourceName, operationsType, interceptorType); + final Class exceptionConverterClass = context + .classValue(ConvertException.class) + .orElse(null); + if (exceptionConverterClass == null) { + return interceptor; + } + Collection exceptionConverters = beanLocator.getBeansOfType(exceptionConverterClass); + return new DataInterceptor() { + @Override + public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext context) { + try { + return interceptor.intercept(methodKey, context); + } catch (Exception e) { + for (ExceptionConverter exceptionConverter : exceptionConverters) { + e = exceptionConverter.convert(e); + } + return ExceptionUtil.sneakyThrow(e); + } + } + }; } final String interceptorName = context.getAnnotationMetadata().stringValue(DataMethod.class, DataMethod.META_MEMBER_INTERCEPTOR).orElse(null); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java index 84214af5e0..2d114aa969 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java @@ -17,21 +17,12 @@ import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.util.CollectionUtils; import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.model.CursoredPage; -import io.micronaut.data.model.DataType; import io.micronaut.data.model.Page; -import io.micronaut.data.model.Pageable; -import io.micronaut.data.model.Pageable.Cursor; -import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.runtime.PreparedQuery; -import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.operations.RepositoryOperations; -import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; - -import java.util.List; /** * An abstract base implementation of query interceptor for page interceptors @@ -59,40 +50,31 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext if (context.hasAnnotation(Query.class)) { PreparedQuery preparedQuery = prepareQuery(methodKey, context); - Iterable iterable = operations.findAll(preparedQuery); - List results = (List) CollectionUtils.iterableToList(iterable); - Pageable pageable = getPageable(context); - Long totalCount = null; - if (pageable.requestTotal()) { + Page page = operations.findPage(preparedQuery); + if (!page.hasTotalSize() && preparedQuery.getPageable().requestTotal()) { PreparedQuery countQuery = prepareCountQuery(methodKey, context); Number n = operations.findOne(countQuery); - totalCount = n != null ? n.longValue() : null; - } - - Page page; - if (pageable.getMode() == Mode.OFFSET) { - page = Page.of(results, pageable, totalCount); - } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { - List cursors; - List resultList = (List) results; - if (preparedQuery.getResultDataType() == DataType.ENTITY) { - cursors = sqlPreparedQuery.createCursors(resultList, pageable); - } else if (sqlPreparedQuery.isDtoProjection()) { - RuntimePersistentEntity runtimePersistentEntity = (RuntimePersistentEntity) operations.getEntity(sqlPreparedQuery.getResultType()); - cursors = sqlPreparedQuery.createCursors(resultList, pageable, runtimePersistentEntity); + Long totalCount = n != null ? n.longValue() : -1; + if (page instanceof CursoredPage cursoredPage) { + page = CursoredPage.of( + cursoredPage.getContent(), + cursoredPage.getPageable(), + cursoredPage.getCursors(), + totalCount + ); } else { - throw new IllegalStateException("CursoredPage cannot produce projection result"); + page = Page.of( + page.getContent(), + page.getPageable(), + totalCount + ); } - page = CursoredPage.of(results, pageable, cursors, totalCount); - } else { - throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); } if (returnType.isInstance(page)) { return (R) page; - } else { - return operations.getConversionService().convert(page, returnType) - .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); } + return operations.getConversionService().convert(page, returnType) + .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); } else { Page page = operations.findPage(getPagedQuery(context)); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindAllInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindAllInterceptor.java index ab99f6b374..66c4e56cf4 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindAllInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindAllInterceptor.java @@ -23,8 +23,6 @@ import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; -import java.util.Collections; - /** * The default implementation of {@link FindAllInterceptor}. * @param The declaring type @@ -32,7 +30,7 @@ * @author graemerocher * @since 1.0.0 */ -public class DefaultFindAllInterceptor extends AbstractQueryInterceptor> implements FindAllInterceptor { +public class DefaultFindAllInterceptor extends AbstractQueryInterceptor implements FindAllInterceptor { /** * Default constructor. @@ -44,30 +42,28 @@ protected DefaultFindAllInterceptor(RepositoryOperations datastore) { } @Override - public Iterable intercept(RepositoryMethodKey methodKey, MethodInvocationContext> context) { - Class> rt = context.getReturnType().getType(); + public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext context) { + Class rt = context.getReturnType().getType(); if (context.hasAnnotation(Query.class)) { PreparedQuery preparedQuery = prepareQuery(methodKey, context); Iterable iterable = operations.findAll(preparedQuery); if (rt.isInstance(iterable)) { - return (Iterable) iterable; - } else { - return operations.getConversionService().convert( - iterable, - context.getReturnType().asArgument() - ).orElse(Collections.emptyList()); + return (R) iterable; } + return operations.getConversionService().convertRequired( + iterable, + context.getReturnType().asArgument() + ); } else { PagedQuery pagedQuery = getPagedQuery(context); Iterable iterable = operations.findAll(pagedQuery); if (rt.isInstance(iterable)) { - return iterable; - } else { - return operations.getConversionService().convert( - iterable, - context.getReturnType().asArgument() - ).orElse(Collections.emptyList()); + return (R) iterable; } + return operations.getConversionService().convertRequired( + iterable, + context.getReturnType().asArgument() + ); } } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java index f030920452..b6e8f27dc2 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java @@ -15,12 +15,8 @@ */ package io.micronaut.data.runtime.intercept; -import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.NonNull; import io.micronaut.data.intercept.FindCursoredPageInterceptor; -import io.micronaut.data.model.CursoredPageable; -import io.micronaut.data.model.Pageable; -import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.operations.RepositoryOperations; /** @@ -42,16 +38,4 @@ protected DefaultFindCursoredPageInterceptor(@NonNull RepositoryOperations datas super(datastore); } - @Override - protected Pageable getPageable(MethodInvocationContext context) { - Pageable pageable = super.getPageable(context); - if (pageable.getMode() == Mode.OFFSET) { - if (pageable.getNumber() == 0) { - pageable = CursoredPageable.from(pageable.getSize(), pageable.getSort()); - } else { - throw new IllegalArgumentException("Pageable with offset mode provided, but method must return a cursored page"); - } - } - return pageable; - } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultUpdateInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultUpdateInterceptor.java index 217a1dc428..8968026538 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultUpdateInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultUpdateInterceptor.java @@ -55,7 +55,7 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext 0; } else { return null; } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersPreparedQuery.java index abf0ac9128..7415d0eb35 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersPreparedQuery.java @@ -16,9 +16,13 @@ package io.micronaut.data.runtime.operations.internal.query; import io.micronaut.aop.InvocationContext; +import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.convert.ConversionService; import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.model.runtime.QueryParameterBinding; import io.micronaut.data.model.runtime.RuntimePersistentEntity; @@ -42,7 +46,7 @@ public class DefaultBindableParametersPreparedQuery implements BindableParametersPreparedQuery, DelegatePreparedQuery { protected final PreparedQuery preparedQuery; - protected final InvocationContext invocationContext; + protected final MethodInvocationContext invocationContext; protected final BindableParametersStoredQuery storedQuery; public DefaultBindableParametersPreparedQuery(PreparedQuery preparedQuery) { @@ -52,7 +56,7 @@ public DefaultBindableParametersPreparedQuery(PreparedQuery preparedQuery) } public DefaultBindableParametersPreparedQuery(PreparedQuery preparedQuery, - InvocationContext invocationContext, + MethodInvocationContext invocationContext, BindableParametersStoredQuery storedQuery) { this.preparedQuery = preparedQuery; this.invocationContext = invocationContext; @@ -69,6 +73,11 @@ private static BindableParametersStoredQuery unwrap(StoredQuery getPersistentEntity() { return storedQuery.getPersistentEntity(); @@ -99,4 +108,14 @@ public void bindParameters(Binder binder) { BindableParametersPreparedQuery.super.bindParameters(binder); } } + + @Override + public Sort getSort() { + return preparedQuery.getSort(); + } + + @Override + public Limit getQueryLimit() { + return preparedQuery.getQueryLimit(); + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java index 1005469d37..20430c09c5 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DefaultBindableParametersStoredQuery.java @@ -21,10 +21,14 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanWrapper; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Pageable; import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.DelegatingQueryParameterBinding; @@ -56,14 +60,19 @@ public class DefaultBindableParametersStoredQuery implements BindableParam private final StoredQuery storedQuery; private final RuntimePersistentEntity runtimePersistentEntity; + private final ConversionService conversionService; /** * @param storedQuery The stored query * @param runtimePersistentEntity The persistent entity + * @param conversionService The conversion service */ - public DefaultBindableParametersStoredQuery(StoredQuery storedQuery, RuntimePersistentEntity runtimePersistentEntity) { + public DefaultBindableParametersStoredQuery(StoredQuery storedQuery, + RuntimePersistentEntity runtimePersistentEntity, + ConversionService conversionService) { this.storedQuery = storedQuery; this.runtimePersistentEntity = runtimePersistentEntity; + this.conversionService = conversionService; Objects.requireNonNull(storedQuery, "Query cannot be null"); } @@ -100,6 +109,7 @@ protected final void bindParameter(Binder binder, Object value = binding.getValue(); RuntimePersistentProperty persistentProperty = null; Argument argument = null; + boolean skipExpansion = false; if (value == null) { if (binding.isExpression()) { requireInvocationContext(invocationContext); @@ -199,11 +209,20 @@ public JsonDataType getJsonDataType() { }; } } - + if (binding.getRole() != null) { + value = switch (binding.getRole()) { + case TypeRole.PAGEABLE, TypeRole.PAGEABLE_REQUIRED -> conversionService.convertRequired(value, Pageable.class); + case TypeRole.LIMIT -> conversionService.convertRequired(value, Limit.class); + case TypeRole.SORT -> conversionService.convertRequired(value, Sort.class); + default -> + throw new IllegalArgumentException("Unsupported role " + binding.getRole()); + }; + skipExpansion = true; + } List values; if (binding.isExpandable()) { - if (value instanceof Sort) { - return; // Skip + if (skipExpansion) { + return; } values = expandValue(value, binding.getDataType()); } else { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DummyPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DummyPreparedQuery.java index b0665d2fb0..d9273242b7 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DummyPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/query/DummyPreparedQuery.java @@ -18,7 +18,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.value.ConvertibleValues; import io.micronaut.core.type.Argument; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.model.runtime.StoredQuery; import io.micronaut.data.runtime.query.internal.DelegateStoredQuery; @@ -57,6 +59,16 @@ public boolean isRawQuery() { return storedQuery.isRawQuery(); } + @Override + public Sort getSort() { + return storedQuery.getSort(); + } + + @Override + public Limit getQueryLimit() { + return storedQuery.getQueryLimit(); + } + @Override public Class getRepositoryType() { return Object.class; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java index c7439ed63e..d3fd2b4fd9 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java @@ -186,7 +186,7 @@ public StoredQuery decorate(MethodInvocationContext context, Class repositoryType = context.getTarget().getClass(); SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType); RuntimePersistentEntity runtimePersistentEntity = runtimeEntityRegistry.getEntity(storedQuery.getRootEntity()); - return new DefaultSqlStoredQuery<>(storedQuery, runtimePersistentEntity, queryBuilder); + return new DefaultSqlStoredQuery<>(storedQuery, runtimePersistentEntity, queryBuilder, conversionService); } /** @@ -207,7 +207,7 @@ protected PS prepareStatement(StatementSupplier statementFunction, SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(preparedQuery); sqlPreparedQuery.prepare(null); if (!isUpdate) { - sqlPreparedQuery.attachPageable(preparedQuery.getPageable(), isSingleResult); + sqlPreparedQuery.attachPageable(preparedQuery.getPageable(), preparedQuery.getQueryLimit(), preparedQuery.getSort(), isSingleResult); } String query = sqlPreparedQuery.getQuery(); @@ -311,7 +311,7 @@ protected SqlStoredQuery resolveEntityInsert(AnnotationMetadata annota final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType); final QueryResult queryResult = queryBuilder.buildInsert(annotationMetadata, new SqlQueryBuilder2.InsertQueryDefinitionImpl(persistentEntity)); - return new DefaultSqlStoredQuery<>(QueryResultStoredQuery.single(OperationType.INSERT, "Custom insert", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), persistentEntity, queryBuilder); + return new DefaultSqlStoredQuery<>(QueryResultStoredQuery.single(OperationType.INSERT, "Custom insert", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), persistentEntity, queryBuilder, getConversionService()); }); } @@ -373,7 +373,9 @@ protected SqlStoredQuery resolveEntityUpdate(AnnotationMetadata annota return new DefaultSqlStoredQuery<>( QueryResultStoredQuery.single(OperationType.UPDATE, "Custom update", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), persistentEntity, - queryBuilder); + queryBuilder, + getConversionService() + ); }); } @@ -461,7 +463,7 @@ public Class getParameterConverterClass() { } RuntimePersistentEntity associatedEntity = association.getAssociatedEntity(); - return new DefaultSqlStoredQuery<>(new BasicStoredQuery<>(sqlInsert, new String[0], parameters, persistentEntity.getIntrospection().getBeanType(), Object.class, OperationType.INSERT), associatedEntity, queryBuilder); + return new DefaultSqlStoredQuery<>(new BasicStoredQuery<>(sqlInsert, new String[0], parameters, persistentEntity.getIntrospection().getBeanType(), Object.class, OperationType.INSERT), associatedEntity, queryBuilder, getConversionService()); } private SqlQueryBuilder2 findQueryBuilder(Class repositoryType) { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index bb7eeda349..917b2cc0f1 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -22,8 +22,10 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Cursor; import io.micronaut.data.model.Pageable.Mode; @@ -53,6 +55,8 @@ import java.util.Map; import java.util.Optional; +import static io.micronaut.data.runtime.query.internal.DefaultPreparedQuery.hasReturnTypeInRole; + /** * Implementation of {@link SqlPreparedQuery}. * @@ -151,19 +155,30 @@ public void prepare(E entity) { } else if (TypeRole.PAGEABLE_REQUIRED.equals(parameter.getRole())) { Pageable pageable = getPageableParameter(parameter); if (!pageable.isUnpaged()) { - appendPaginationOrOrderQueryPart(q, pageable, false, parameter.getTableAlias(), inx); + appendPageable(q, pageable, pageable.getLimit(), pageable.getSort(), parameter.getTableAlias(), inx); } } else if (TypeRole.PAGEABLE.equals(parameter.getRole())) { Pageable pageable = getPageableParameter(parameter); - appendPaginationOrOrderQueryPart(q, pageable, false, parameter.getTableAlias(), inx); + appendPageable(q, pageable, pageable.getLimit(), pageable.getSort(), parameter.getTableAlias(), inx); } else if (TypeRole.SORT.equals(parameter.getRole())) { Sort sort = getSortParameter(parameter); appendSort(sort, q, sqlStoredQuery.getQueryBuilder(), parameter.getTableAlias()); - int limit = sqlStoredQuery.getLimit(); - int offset = sqlStoredQuery.getOffset(); - if (limit != -1 || offset > 0) { + Limit limit = sqlStoredQuery.getQueryLimit(); + if (!limit.isLimited()) { + limit = getParameterInRole(TypeRole.LIMIT, Limit.class).orElse(limit); + } + if (limit.isLimited()) { + q.append(queryBuilder.buildLimitAndOffset(limit.maxResults(), limit.offset())); + } + } else if (TypeRole.LIMIT.equals(parameter.getRole())) { + Sort sort = storedQuery.getSort(); + if (sort.isSorted()) { + appendSort(sort, q, sqlStoredQuery.getQueryBuilder(), parameter.getTableAlias()); + } + Limit limit = getLimitParameter(parameter); + if (limit.isLimited()) { // Limit defined by the method name - q.append(queryBuilder.buildLimitAndOffset(limit, offset)); + q.append(queryBuilder.buildLimitAndOffset(limit.maxResults(), limit.offset())); } } q.append(sqlStoredQuery.getExpandableQueryParts()[queryParamIndex++]); @@ -174,25 +189,50 @@ public void prepare(E entity) { private Pageable getPageableParameter(QueryParameterBinding parameter) { Object value = getParameterValue(parameter); - if (value instanceof Pageable) { - // The pageable might be modified - return preparedQuery.getPageable(); + Pageable pageable = getConversionService() + .convert(value, Pageable.class).orElseThrow(() -> new IllegalArgumentException("Unsupported parameter type " + parameter.getRole())); + if (pageable.getMode() == Pageable.Mode.OFFSET && hasReturnTypeInRole(TypeRole.CURSORED_PAGE, CursoredPage.class, invocationContext, getConversionService())) { + if (pageable.getNumber() == 0) { + pageable = CursoredPageable.from(pageable.getSize(), pageable.getSort()); + } else { + throw new IllegalArgumentException("Pageable with offset mode provided, but method must return a cursored page"); + } } - if (value instanceof Sort sort) { - return Pageable.UNPAGED.withSort(sort); + Sort storedSort = storedQuery.getSort(); + if (storedSort.isSorted()) { + pageable = pageable.withSort(storedSort.orders(pageable.getOrderBy())); } - throw new IllegalArgumentException("Unsupported parameter type " + parameter.getRole()); + for (Sort sort : getParametersInRole(TypeRole.SORT, Sort.class)) { + if (sort != pageable) { + pageable = pageable.withSort(pageable.getSort().orders(sort.getOrderBy())); + } + } + return pageable; } private Sort getSortParameter(QueryParameterBinding parameter) { Object value = getParameterValue(parameter); - if (value instanceof Pageable pageable) { - return pageable.withoutPaging(); - } - if (value instanceof Sort sort) { - return sort; + Sort sort = getConversionService() + .convert(value, Sort.class).orElseThrow(() -> new IllegalArgumentException("Unsupported parameter type " + parameter.getRole())); + Sort querySort = storedQuery.getSort(); + if (querySort.isSorted()) { + sort = querySort.orders(sort.getOrderBy()); + } + for (Object itemValue : getParametersInRole(TypeRole.SORT, Object.class)) { + if (itemValue != value) { + Sort sortItem = getConversionService().convert(itemValue, Sort.class).orElse(null); + if (sortItem != null) { + sort = sort.orders(sortItem.getOrderBy()); + } + } } - throw new IllegalArgumentException("Unsupported parameter type " + parameter.getRole()); + return sort; + } + + private Limit getLimitParameter(QueryParameterBinding parameter) { + Object value = getParameterValue(parameter); + return getConversionService() + .convert(value, Limit.class).orElseThrow(() -> new IllegalArgumentException("Unsupported parameter type " + parameter.getRole())); } /** @@ -239,12 +279,12 @@ public static CursoredPageable enhancePageable(CursoredPageable cursored, Persis } @Override - public void attachPageable(Pageable pageable, boolean isSingleResult) { + public void attachPageable(Pageable pageable, Limit limit, Sort sort) { if (pageable.isUnpaged() && !pageable.isSorted() || bindPageableOrSort) { return; } StringBuilder builder = new StringBuilder(); - appendPaginationOrOrderQueryPart(builder, pageable, isSingleResult, null, storedQuery.getQueryBindings().size() + 1); + appendPageable(builder, pageable, limit, sort, null, storedQuery.getQueryBindings().size() + 1); int forUpdateIndex = this.query.lastIndexOf(SqlQueryBuilder.STANDARD_FOR_UPDATE_CLAUSE); if (forUpdateIndex == -1) { @@ -257,10 +297,12 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { } } - private void appendPaginationOrOrderQueryPart(StringBuilder query, Pageable pageable, - boolean isSingleResult, - String tableAlias, - int paramIndex) { + private void appendPageable(StringBuilder query, + Pageable pageable, + Limit limit, + Sort sort, + String tableAlias, + int paramIndex) { SqlQueryBuilder2 queryBuilder = sqlStoredQuery.getQueryBuilder(); if (pageable instanceof CursoredPageable cursored) { cursored = enhancePageable(cursored, getPersistentEntity()); @@ -268,14 +310,19 @@ private void appendPaginationOrOrderQueryPart(StringBuilder query, Pageable page appendSort(cursored.getSort(), query, queryBuilder, tableAlias); query.append(queryBuilder.buildLimitAndOffset(cursored.getSize(), 0)); // Append limit } else { - appendSort(pageable.getSort(), query, queryBuilder, tableAlias); - if (isSingleResult && pageable.getOffset() > 0) { - pageable = Pageable.from(pageable.getNumber(), 1); - } - query.append(queryBuilder.buildLimitAndOffset(pageable.getSize(), pageable.getOffset())); + appendLimitOrOrderQueryPart(query, limit, sort, tableAlias); } } + private void appendLimitOrOrderQueryPart(StringBuilder query, + Limit limit, + Sort sort, + String tableAlias) { + SqlQueryBuilder2 queryBuilder = sqlStoredQuery.getQueryBuilder(); + appendSort(sort, query, queryBuilder, tableAlias); + query.append(queryBuilder.buildLimitAndOffset(limit.maxResults(), limit.offset())); + } + private void appendSort(Sort sort, StringBuilder added, SqlQueryBuilder2 queryBuilder, String tableAlias) { RuntimePersistentEntity persistentEntity = getPersistentEntity(); if (sort.isSorted()) { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java index 302802ccfe..8137fb49f7 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.beans.BeanWrapper; +import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.data.annotation.QueryResult; import io.micronaut.data.model.JsonDataType; @@ -55,9 +56,13 @@ public class DefaultSqlStoredQuery extends DefaultBindableParametersStored * @param storedQuery The stored query * @param runtimePersistentEntity The persistent entity * @param queryBuilder The query builder + * @param conversionService The conversion service */ - public DefaultSqlStoredQuery(StoredQuery storedQuery, RuntimePersistentEntity runtimePersistentEntity, SqlQueryBuilder2 queryBuilder) { - super(storedQuery, runtimePersistentEntity); + public DefaultSqlStoredQuery(StoredQuery storedQuery, + RuntimePersistentEntity runtimePersistentEntity, + SqlQueryBuilder2 queryBuilder, + ConversionService conversionService) { + super(storedQuery, runtimePersistentEntity, conversionService); this.queryBuilder = queryBuilder; Objects.requireNonNull(storedQuery, "Query cannot be null"); Objects.requireNonNull(queryBuilder, "Builder cannot be null"); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java index 9f9c6cc1ab..441520d6a1 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java @@ -18,8 +18,11 @@ import io.micronaut.aop.InvocationContext; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.QueryResultInfo; +import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.runtime.operations.internal.query.BindableParametersPreparedQuery; /** @@ -45,9 +48,22 @@ public interface SqlPreparedQuery extends BindableParametersPreparedQuery< * Modify the query according to the pageable. * * @param pageable The pageable + * @param limit The limit + * @param sort The sort * @param isSingleResult is single result */ - void attachPageable(Pageable pageable, boolean isSingleResult); + default void attachPageable(Pageable pageable, Limit limit, Sort sort, boolean isSingleResult) { + attachPageable(pageable, isSingleResult ? Limit.of(1, limit.offset()) : limit, sort); + } + + /** + * Modify the query according to the pageable. + * + * @param pageable The pageable + * @param limit The limit + * @param sort The sort + */ + void attachPageable(Pageable pageable, Limit limit, Sort sort); /** * @return the query result info @@ -64,4 +80,10 @@ public interface SqlPreparedQuery extends BindableParametersPreparedQuery< @Nullable @SuppressWarnings("java:S1452") InvocationContext getInvocationContext(); + + /** + * @return The persistent entity + */ + @Nullable + RuntimePersistentEntity getPersistentEntity(); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlStoredQuery.java index da499294ef..d410f4d2b6 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlStoredQuery.java @@ -21,6 +21,7 @@ import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder2; import io.micronaut.data.model.runtime.QueryParameterBinding; import io.micronaut.data.model.runtime.QueryResultInfo; +import io.micronaut.data.model.runtime.RuntimePersistentEntity; import io.micronaut.data.runtime.operations.internal.query.BindableParametersStoredQuery; import java.util.Map; @@ -67,4 +68,10 @@ public interface SqlStoredQuery extends BindableParametersStoredQuery getPersistentEntity(); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/DefaultPreparedQueryResolver.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/DefaultPreparedQueryResolver.java index 0b1a1a8f69..84e089b8ce 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/DefaultPreparedQueryResolver.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/DefaultPreparedQueryResolver.java @@ -18,6 +18,8 @@ import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; +import io.micronaut.data.annotation.TypeRole; +import io.micronaut.data.model.Limit; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.model.runtime.StoredQuery; @@ -41,6 +43,7 @@ public PreparedQuery resolveQuery(MethodInvocationContext con storedQuery, storedQuery.getQuery(), pageable, + DefaultPreparedQuery.getParameterInRole(TypeRole.LIMIT, Limit.class, context, getConversionService()).orElse(Limit.UNLIMITED), storedQuery.isDtoProjection(), getConversionService() ); @@ -55,6 +58,7 @@ public PreparedQuery resolveCountQuery(MethodInvocationContext extends DefaultStoredDataOperatio private final boolean dto; private final MethodInvocationContext context; private final ConversionService conversionService; + @Nullable + private final Limit limit; /** * The default constructor. @@ -54,6 +67,7 @@ public final class DefaultPreparedQuery extends DefaultStoredDataOperatio * @param storedQuery The stored query * @param finalQuery The final query * @param pageable The pageable + * @param limit The limit * @param dtoProjection Whether the prepared query is a dto projection * @param conversionService The conversion service */ @@ -62,15 +76,115 @@ public DefaultPreparedQuery( StoredQuery storedQuery, String finalQuery, @NonNull Pageable pageable, + @NonNull Limit limit, boolean dtoProjection, ConversionService conversionService) { super(context); this.context = context; this.query = finalQuery; this.storedQuery = storedQuery; - this.pageable = pageable; this.dto = dtoProjection; this.conversionService = conversionService; + this.limit = limit; + if (pageable.getMode() == Pageable.Mode.OFFSET && hasReturnTypeInRole(TypeRole.CURSORED_PAGE, CursoredPage.class, context, conversionService)) { + if (pageable.getNumber() == 0) { + pageable = CursoredPageable.from(pageable.getSize(), pageable.getSort()); + } else { + throw new IllegalArgumentException("Pageable with offset mode provided, but method must return a cursored page"); + } + } + this.pageable = pageable.withSort(storedQuery.getSort().orders(pageable.getOrderBy())); + } + + /** + * Check the return role from the method context. + * + * @param role The role + * @param type The type + * @param methodContext The method context + * @param conversionService The conversion service + * @return The optional parameter + */ + public static boolean hasReturnTypeInRole(@NonNull String role, + @NonNull Class type, + @NonNull MethodInvocationContext methodContext, + @NonNull ConversionService conversionService) { + return methodContext.stringValue(DATA_METHOD_ANN_NAME, DataMethodQuery.META_MEMBER_RETURN_TYPE_ROLE) + .filter(typeRole -> typeRole.equals(role)) + .map(ignore -> conversionService.canConvert(methodContext.getReturnType().getType(), type)) + .orElse(false); + } + + /** + * Find a parameter in role from the method context. + * + * @param role The role + * @param type The type of the parameter in role + * @param methodContext The method context + * @param conversionService The conversion service + * @param The type + * @return The optional parameter + */ + @NonNull + public static Optional getParameterInRole(@NonNull String role, + @NonNull Class type, + @NonNull MethodInvocationContext methodContext, + @NonNull ConversionService conversionService) { + return methodContext.stringValue(DATA_METHOD_ANN_NAME, role).flatMap(name -> { + MutableArgumentValue arg = methodContext.getParameters().get(name); + if (arg == null) { + return Optional.empty(); + } + Object o = arg.getValue(); + if (o == null) { + return Optional.empty(); + } + if (type.isInstance(o)) { + //noinspection unchecked + return Optional.of((RT1) o); + } + return conversionService.convert(o, type); + }); + } + + /** + * Find the parameters in role from the method context. + * + * @param role The role + * @param type The type of the parameter in role + * @param methodContext The method context + * @param conversionService The conversion service + * @param The type + * @return The list of types + */ + @NonNull + public static List getParametersInRole(@NonNull String role, + @NonNull Class type, + @NonNull MethodInvocationContext methodContext, + @NonNull ConversionService conversionService) { + AnnotationValue annotation = methodContext.getAnnotation(DATA_METHOD_ANN_NAME); + if (annotation == null) { + return List.of(); + } + List> roles = annotation.getAnnotations(DataMethodQuery.META_MEMBER_PARAMETERS_TYPE_ROLES); + return roles.stream() + .filter(a -> a.stringValue().orElseThrow().equals(role)) + .flatMap(a -> { + Object value = methodContext.getParameterValues()[a.intValue("parameterIndex").orElseThrow()]; + if (value == null) { + return Stream.empty(); + } + if (type.isInstance(value)) { + //noinspection unchecked + return Stream.of((RT1) value); + } + return conversionService.convert(value, type).stream(); + }).toList(); + } + + @Override + public ConversionService getConversionService() { + return conversionService; } /** @@ -102,21 +216,12 @@ public StoredQuery getStoredQueryDelegate() { @Override public Optional getParameterInRole(@NonNull String role, @NonNull Class type) { - return context.stringValue(DATA_METHOD_ANN_NAME, role).flatMap(name -> { - MutableArgumentValue arg = context.getParameters().get(name); - if (arg == null) { - return Optional.empty(); - } - Object o = arg.getValue(); - if (o == null) { - return Optional.empty(); - } - if (type.isInstance(o)) { - //noinspection unchecked - return Optional.of((RT1) o); - } - return conversionService.convert(o, type); - }); + return getParameterInRole(role, type, context, conversionService); + } + + @Override + public List getParametersInRole(String role, Class type) { + return getParametersInRole(role, type, context, conversionService); } @Override @@ -173,4 +278,32 @@ public Optional getAttribute(CharSequence name, Class type) { return context.getAttribute(name, type); } + @Override + public int getOffset() { + if (limit != null) { + return (int) limit.offset(); + } + return DelegateStoredQuery.super.getOffset(); + } + + @Override + public int getLimit() { + if (limit != null) { + return limit.maxResults(); + } + return DelegateStoredQuery.super.getLimit(); + } + + @Override + public Sort getSort() { + return pageable.getSort(); + } + + @Override + public Limit getQueryLimit() { + if (limit != null) { + return limit; + } + return pageable.getLimit(); + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java index 445b6d9008..a0f79fa3f3 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java @@ -32,6 +32,8 @@ import io.micronaut.data.model.AssociationUtils; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder; import io.micronaut.data.model.runtime.DefaultStoredDataOperation; @@ -91,8 +93,8 @@ public final class DefaultStoredQuery extends DefaultStoredDataOperation< private final boolean jsonEntity; private final OperationType operationType; private final Map> parameterExpressions; - private final int limit; - private final int offset; + private final Limit limit; + private final Sort sort; private final Function stringsEnvResolverValueMapper; /** @@ -236,8 +238,18 @@ public DefaultStoredQuery( this.jsonEntity = DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(annotationMetadata); this.parameterExpressions = annotationMetadata.getAnnotationValuesByType(ParameterExpression.class).stream() .collect(Collectors.toMap(av -> av.stringValue("name").orElseThrow(), av -> av)); - this.limit = dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_LIMIT).orElse(-1); - this.offset = dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_OFFSET).orElse(0); + this.limit = Limit.of( + dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_LIMIT).orElse(-1), + dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_OFFSET).orElse(0) + ); + this.sort = Sort.of( + dataMethodQuery.getAnnotations(DataMethodQuery.META_MEMBER_SORT).stream() + .map(av -> new Sort.Order( + av.stringValue().orElseThrow(), + av.enumValue("direction", Sort.Order.Direction.class).orElse(Sort.Order.Direction.ASC), + av.booleanValue("ignoreCase").orElse(false)) + ).toList() + ); } private static Class getRequiredRootEntity(ExecutableMethod context) { @@ -299,13 +311,13 @@ private static List getQueryParameters(List extends PreparedQuery, Delega */ PreparedQuery getPreparedQueryDelegate(); + @Override + default ConversionService getConversionService() { + return getPreparedQueryDelegate().getConversionService(); + } + @Override default StoredQuery getStoredQueryDelegate() { return getPreparedQueryDelegate(); @@ -67,6 +76,11 @@ default Optional getParameterInRole(@NonNull String role, @NonNull Cl return getPreparedQueryDelegate().getParameterInRole(role, type); } + @Override + default List getParametersInRole(String role, Class type) { + return getPreparedQueryDelegate().getParametersInRole(role, type); + } + @Override default Class getRepositoryType() { return getPreparedQueryDelegate().getRepositoryType(); @@ -117,4 +131,13 @@ default Optional getAttribute(CharSequence name, Class type) { return getPreparedQueryDelegate().getAttribute(name, type); } + @Override + default Sort getSort() { + return getPreparedQueryDelegate().getSort(); + } + + @Override + default Limit getQueryLimit() { + return getPreparedQueryDelegate().getQueryLimit(); + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java index 101bb4059f..3c629d952a 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java @@ -19,6 +19,8 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.type.Argument; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.Limit; +import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.runtime.QueryParameterBinding; import io.micronaut.data.model.runtime.StoredQuery; @@ -187,4 +189,14 @@ default int getLimit() { default int getOffset() { return getStoredQueryDelegate().getOffset(); } + + @Override + default Limit getQueryLimit() { + return getStoredQueryDelegate().getQueryLimit(); + } + + @Override + default Sort getSort() { + return getStoredQueryDelegate().getSort(); + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataDeleteExceptionConverter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataDeleteExceptionConverter.java new file mode 100644 index 0000000000..824f433108 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataDeleteExceptionConverter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2025 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.runtime.support.exceptions.jakarta.data; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.exceptions.ExceptionConverter; + +/** + * The Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public interface JakartaDataDeleteExceptionConverter extends ExceptionConverter { +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataExceptionConverter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataExceptionConverter.java new file mode 100644 index 0000000000..2a8dc4d845 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataExceptionConverter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2025 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.runtime.support.exceptions.jakarta.data; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.exceptions.ExceptionConverter; + +/** + * The Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public interface JakartaDataExceptionConverter extends ExceptionConverter { +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataInsertExceptionConverter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataInsertExceptionConverter.java new file mode 100644 index 0000000000..65c8533559 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataInsertExceptionConverter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2025 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.runtime.support.exceptions.jakarta.data; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.exceptions.ExceptionConverter; + +/** + * The Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public interface JakartaDataInsertExceptionConverter extends ExceptionConverter { +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataUpdateExceptionConverter.java b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataUpdateExceptionConverter.java new file mode 100644 index 0000000000..2d6246f26e --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/support/exceptions/jakarta/data/JakartaDataUpdateExceptionConverter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2025 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.runtime.support.exceptions.jakarta.data; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.data.exceptions.ExceptionConverter; + +/** + * The Jakarta Data exception converter. + * + * @author Denis Stepanov + * @since 4.12 + */ +@Internal +public interface JakartaDataUpdateExceptionConverter extends ExceptionConverter { +} diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy index 8cf2f8c588..b9be75a279 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy @@ -120,17 +120,9 @@ abstract class AbstractPageSpec extends Specification { page.content.every() { it instanceof Person } !page.hasTotalSize() - when: - page.getTotalPages() - - then: - thrown(IllegalStateException) - - when: - page.getTotalSize() - - then: - thrown(IllegalStateException) + and: + page.getTotalPages() == -1 + page.getTotalSize() == -1 } void "test pageable sort"() { diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy index 312e78b1c2..01dd3f39af 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy @@ -680,17 +680,22 @@ abstract class AbstractRepositorySpec extends Specification { given: cleanupData() saveSampleBooks() - when: def books1 = bookRepository.listPageableCustomQuery(Pageable.from(0).order("author.name").order("title")).getContent() def books2 = bookRepository.findAll(Pageable.from(0).order("author.name").order("title")).getContent() - + // Order defined only by method + def books3 = bookRepository.findAllSorted(Pageable.from(0)).getContent() + // Extra order + def books4 = bookRepository.findAllSorted2(Pageable.from(0).order("title")).getContent() then: books1.size() == 6 books2.size() == 6 + books3.size() == 6 + books4.size() == 6 books1[0].title == "The Border" books2[0].title == "The Border" - + books3[0].title == "The Border" + books4[0].title == "The Border" cleanup: cleanupData() } diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java index 094973b40a..1c3a18d509 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java @@ -19,8 +19,10 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.Expandable; +import io.micronaut.data.annotation.Find; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.Join; +import io.micronaut.data.annotation.OrderBy; import io.micronaut.data.annotation.Query; import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.model.DataType; @@ -67,6 +69,17 @@ public abstract class BookRepository implements PageableRepository, @Join("author") public abstract Page findAll(@NonNull Pageable pageable); + @Find + @Join("author") + @OrderBy("author.name") + @OrderBy("title") + public abstract Page findAllSorted(Pageable pageable); + + @Find + @Join("author") + @OrderBy("author.name") + public abstract Page findAllSorted2(Pageable pageable); + @Join(value = "author", type = Join.Type.LEFT_FETCH) public abstract Page findByTotalPagesGreaterThan(int totalPages, Pageable pageable); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2f4341719..2125bd10bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ micronaut-coherence = "5.0.4" groovy = "4.0.26" managed-javax-persistence = "2.2" +managed-jakarta-data = "1.0.1" spring-data = "3.4.4" @@ -52,6 +53,9 @@ sonatype-scan = "3.0.0" # meaning that they should be extracted into their own BOM kotlin-coroutines = "1.9.0" +antlr = "4.13.2" + + [libraries] micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } @@ -87,6 +91,7 @@ kotlin-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } managed-javax-persistence-api = { module = "javax.persistence:javax.persistence-api", version.ref = "managed-javax-persistence" } +managed-jakarta-data-api = { module = 'jakarta.data:jakarta.data-api', version.ref = "managed-jakarta-data" } # JPA @@ -101,6 +106,11 @@ lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } micronaut-azure-cosmos = { module = "io.micronaut.azure:micronaut-azure-cosmos", version.ref = "micronaut-azure" } +# Antlr + +antlr = { module = "org.antlr:antlr4", version.ref = "antlr"} +antlr-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr"} + # Test jupiter-engine = { module = 'org.junit.jupiter:junit-jupiter-engine' } @@ -108,6 +118,9 @@ jupiter-api = { module = 'org.junit.jupiter:junit-jupiter-api' } groovy-sql = { module = "org.apache.groovy:groovy-sql" } groovy-dateutil = { module = "org.apache.groovy:groovy-dateutil", version.ref = "groovy" } +jakarta-data-tck = { module = 'jakarta.data:jakarta.data-tck', version.ref = "managed-jakarta-data" } +junit-platform-suite-engine = { module = "org.junit.platform:junit-platform-suite-engine" } + # Benchmark jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } diff --git a/jakarta-data-tck/hibernate/build.gradle b/jakarta-data-tck/hibernate/build.gradle new file mode 100644 index 0000000000..693a6cea09 --- /dev/null +++ b/jakarta-data-tck/hibernate/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "java-library" + id "io.micronaut.build.internal.data-base" + id "io.micronaut.build.internal.data-dependencies" +} + +dependencies { + testAnnotationProcessor mn.micronaut.inject.java + testAnnotationProcessor projects.micronautDataProcessor + + testImplementation projects.micronautJakartaDataTck.micronautSupport + testImplementation projects.micronautDataHibernateJpa + testImplementation projects.micronautDataProcessor + testImplementation mnValidation.micronaut.validation.processor + testImplementation mnValidation.micronaut.validation + + testRuntimeOnly mnTest.junit.jupiter.engine + testRuntimeOnly mnSql.micronaut.jdbc.tomcat + testRuntimeOnly mnLogging.logback.classic + testRuntimeOnly mnSql.h2 + testRuntimeOnly mn.snakeyaml +} + +test { + scanForTestClasses false + systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") + useJUnitPlatform() + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} diff --git a/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/DummyBean.java b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/DummyBean.java new file mode 100644 index 0000000000..929e50a65b --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/DummyBean.java @@ -0,0 +1,29 @@ +package io.micronaut.data.jakarta.tck; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +// Prevent Hibernate failing for tests without entities +@Entity +public class DummyBean { + + @Id + private Long id; + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java new file mode 100644 index 0000000000..e04109d696 --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java @@ -0,0 +1,37 @@ +package io.micronaut.data.jakarta.tck; + +import ee.jakarta.tck.data.standalone.entity.EntityTests; +import ee.jakarta.tck.data.standalone.persistence.PersistenceEntityTests; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.reflect.Method; + +public class FilterExtension implements ExecutionCondition { + private static final ConditionEvaluationResult DISABLED = ConditionEvaluationResult.disabled("DISABLED"); + + public FilterExtension() { + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Class testClass = context.getTestClass().orElse(null); + String testMethodName = context.getTestMethod().map(Method::getName).orElse(""); + if (testClass == PersistenceEntityTests.class) { + switch (testMethodName) { + case "testMultipleInsertUpdateDelete", + "testVersionedInsertUpdateDelete" -> { + return DISABLED; // Optimistic locking + } + } + } + if (testClass == EntityTests.class) { + if (testMethodName.equals("testLiteralTrue")) { + return DISABLED; // https://hibernate.atlassian.net/browse/HHH-19177 + } + } + return ConditionEvaluationResult.enabled(null); + } + +} diff --git a/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/HibernateJakartaDataTCKSuite.java b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/HibernateJakartaDataTCKSuite.java new file mode 100644 index 0000000000..dfe109fc4b --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/java/io/micronaut/data/jakarta/tck/HibernateJakartaDataTCKSuite.java @@ -0,0 +1,27 @@ +package io.micronaut.data.jakarta.tck; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +public class HibernateJakartaDataTCKSuite { + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.entity.*") + public static class EntityTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.persistence.*") + public static class PersistenceTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.web.validation.*") + public static class ValidationTests { + } + +} diff --git a/jakarta-data-tck/hibernate/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/jakarta-data-tck/hibernate/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000000..24ed0a5f45 --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.micronaut.data.jakarta.tck.FilterExtension diff --git a/jakarta-data-tck/hibernate/src/test/resources/application.yml b/jakarta-data-tck/hibernate/src/test/resources/application.yml new file mode 100644 index 0000000000..1f0428b19d --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/resources/application.yml @@ -0,0 +1,20 @@ +--- +micronaut: + application: + name: data-example +--- +datasources: + default: + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=USER + driverClassName: org.h2.Driver + username: sa + password: '' +jpa: + default: + properties: + uniqueResultOnFindOne: true + persistOrMergeOnSave: true + hibernate: + show_sql: true + hbm2ddl: + auto: update diff --git a/jakarta-data-tck/hibernate/src/test/resources/logback.xml b/jakarta-data-tck/hibernate/src/test/resources/logback.xml new file mode 100644 index 0000000000..ee632b41b2 --- /dev/null +++ b/jakarta-data-tck/hibernate/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/jakarta-data-tck/jdbc/build.gradle b/jakarta-data-tck/jdbc/build.gradle new file mode 100644 index 0000000000..498ae66767 --- /dev/null +++ b/jakarta-data-tck/jdbc/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "java-library" + id "io.micronaut.build.internal.data-base" + id "io.micronaut.build.internal.data-dependencies" +} + +dependencies { + + testImplementation projects.micronautJakartaDataTck.micronautSupport + testImplementation projects.micronautDataJdbc + testImplementation projects.micronautDataProcessor + testImplementation mnValidation.micronaut.validation.processor + testImplementation mnValidation.micronaut.validation + + testRuntimeOnly mnTest.junit.jupiter.engine + testRuntimeOnly mnSql.micronaut.jdbc.tomcat + testRuntimeOnly mnLogging.logback.classic + testRuntimeOnly mnSql.h2 + testRuntimeOnly mn.snakeyaml +} + +test { + scanForTestClasses false + systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") + useJUnitPlatform() + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} diff --git a/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java b/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java new file mode 100644 index 0000000000..1851522aa1 --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java @@ -0,0 +1,41 @@ +package io.micronaut.data.jakarta.tck; + +import ee.jakarta.tck.data.standalone.entity.EntityTests; +import ee.jakarta.tck.data.standalone.persistence.PersistenceEntityTests; +import ee.jakarta.tck.data.web.validation.ValidationTests; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.reflect.Method; + +public class FilterExtension implements ExecutionCondition { + private static final ConditionEvaluationResult DISABLED = ConditionEvaluationResult.disabled("DISABLED"); + + public FilterExtension() { + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Class testClass = context.getTestClass().orElse(null); + String testMethodName = context.getTestMethod().map(Method::getName).orElse(""); + if (testClass == EntityTests.class) { + switch (testMethodName) { + case "testBasicRepositoryBuiltInMethods", "testBasicRepositoryMethods" -> { + return DISABLED; // Support deciding between persist or update when save is called + } + + } + } + if (testClass == ValidationTests.class) { + switch (testMethodName) { + case "testSaveWithValidConstraints", "testUpdateAllWithValidConstraints", "testUpdateWithValidConstraints" -> { + return DISABLED; // Support deciding between persist or update when save is called + } + + } + } + return ConditionEvaluationResult.enabled(null); + } + +} diff --git a/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/MicronautJdbcDataTCKSuite.java b/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/MicronautJdbcDataTCKSuite.java new file mode 100644 index 0000000000..b36d632784 --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/java/io/micronaut/data/jakarta/tck/MicronautJdbcDataTCKSuite.java @@ -0,0 +1,27 @@ +package io.micronaut.data.jakarta.tck; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +public class MicronautJdbcDataTCKSuite { + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.entity.*") + public static class EntityTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.persistence.*") + public static class PersistenceTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.web.validation.*") + public static class ValidationTests { + } + +} diff --git a/jakarta-data-tck/jdbc/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/jakarta-data-tck/jdbc/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000000..24ed0a5f45 --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.micronaut.data.jakarta.tck.FilterExtension diff --git a/jakarta-data-tck/jdbc/src/test/resources/application.yml b/jakarta-data-tck/jdbc/src/test/resources/application.yml new file mode 100644 index 0000000000..de90b44385 --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/resources/application.yml @@ -0,0 +1,14 @@ +--- +micronaut: + application: + name: data-example +--- +datasources: + default: + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=USER + driverClassName: org.h2.Driver + username: sa + password: '' + schema-generate: create_drop + dialect: H2 + uniqueResultOnFindOne: true diff --git a/jakarta-data-tck/jdbc/src/test/resources/aprocessor.properties b/jakarta-data-tck/jdbc/src/test/resources/aprocessor.properties new file mode 100644 index 0000000000..42ba28d52d --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/resources/aprocessor.properties @@ -0,0 +1 @@ +implementation=jdbc diff --git a/jakarta-data-tck/jdbc/src/test/resources/logback.xml b/jakarta-data-tck/jdbc/src/test/resources/logback.xml new file mode 100644 index 0000000000..ee632b41b2 --- /dev/null +++ b/jakarta-data-tck/jdbc/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/jakarta-data-tck/mongodb/build.gradle b/jakarta-data-tck/mongodb/build.gradle new file mode 100644 index 0000000000..c51713e7a3 --- /dev/null +++ b/jakarta-data-tck/mongodb/build.gradle @@ -0,0 +1,42 @@ +import io.micronaut.testresources.buildtools.KnownModules + +plugins { + id "java-library" + id "io.micronaut.build.internal.data-base" + id "io.micronaut.build.internal.data-dependencies" + id 'io.micronaut.test-resources' +} + +dependencies { + + testImplementation projects.micronautJakartaDataTck.micronautSupport + testImplementation projects.micronautDataMongodb + testImplementation projects.micronautDataDocumentProcessor + testImplementation mnValidation.micronaut.validation.processor + testImplementation mnValidation.micronaut.validation + testImplementation mnMongo.mongo.driver + + testRuntimeOnly mnTest.junit.jupiter.engine + testRuntimeOnly mnLogging.logback.classic + testRuntimeOnly mn.snakeyaml +} + + +micronaut { + version libs.versions.micronaut.platform.get() + testResources { + enabled = true + inferClasspath = false + additionalModules.add(KnownModules.MONGODB) + clientTimeout = 300 + version = libs.versions.micronaut.testresources.get() + } +} + + +test { + scanForTestClasses false + systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") + useJUnitPlatform() + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} diff --git a/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java b/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java new file mode 100644 index 0000000000..792160918f --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/FilterExtension.java @@ -0,0 +1,43 @@ +package io.micronaut.data.jakarta.tck; + +import ee.jakarta.tck.data.standalone.entity.EntityTests; +import ee.jakarta.tck.data.standalone.persistence.PersistenceEntityTests; +import ee.jakarta.tck.data.web.validation.ValidationTests; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.reflect.Method; + +public class FilterExtension implements ExecutionCondition { + private static final ConditionEvaluationResult DISABLED = ConditionEvaluationResult.disabled("DISABLED"); + + public FilterExtension() { + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Class testClass = context.getTestClass().orElse(null); + String testMethodName = context.getTestMethod().map(Method::getName).orElse(""); + if (testClass == EntityTests.class) { + switch (testMethodName) { + case "testBasicRepositoryBuiltInMethods", "testBasicRepositoryMethods" -> { + return DISABLED; // Support deciding between persist or update when save is called + } + case "testIgnoreCase" -> { + return DISABLED; // Between doesn't support case insensitive + } + } + } + if (testClass == ValidationTests.class) { + switch (testMethodName) { + case "testSaveWithValidConstraints", "testUpdateAllWithValidConstraints", "testUpdateWithValidConstraints" -> { + return DISABLED; // Support deciding between persist or update when save is called + } + + } + } + return ConditionEvaluationResult.enabled(null); + } + +} diff --git a/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/MongoDBJakartaDataTCKSuite.java b/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/MongoDBJakartaDataTCKSuite.java new file mode 100644 index 0000000000..37730d7907 --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/java/io/micronaut/data/jakarta/tck/MongoDBJakartaDataTCKSuite.java @@ -0,0 +1,28 @@ +package io.micronaut.data.jakarta.tck; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectMethod; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +public class MongoDBJakartaDataTCKSuite { + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.entity.*") + public static class EntityTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.standalone.persistence.*") + public static class PersistenceTests { + } + + @Suite + @SelectPackages("ee.jakarta.tck.data") + @IncludeClassNamePatterns("ee.jakarta.tck.data.web.validation.*") + public static class ValidationTests { + } + +} diff --git a/jakarta-data-tck/mongodb/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/jakarta-data-tck/mongodb/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000000..24ed0a5f45 --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.micronaut.data.jakarta.tck.FilterExtension diff --git a/jakarta-data-tck/mongodb/src/test/resources/application.yml b/jakarta-data-tck/mongodb/src/test/resources/application.yml new file mode 100644 index 0000000000..00663279b1 --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/resources/application.yml @@ -0,0 +1,9 @@ +micronaut: + application: + name: data-example + data: + mongodb: + create-collections: true + + +mongodb.uuid-representation: STANDARD diff --git a/jakarta-data-tck/mongodb/src/test/resources/aprocessor.properties b/jakarta-data-tck/mongodb/src/test/resources/aprocessor.properties new file mode 100644 index 0000000000..c579a95d07 --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/resources/aprocessor.properties @@ -0,0 +1 @@ +implementation=mongodb diff --git a/jakarta-data-tck/mongodb/src/test/resources/logback.xml b/jakarta-data-tck/mongodb/src/test/resources/logback.xml new file mode 100644 index 0000000000..ee632b41b2 --- /dev/null +++ b/jakarta-data-tck/mongodb/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/jakarta-data-tck/support/build.gradle b/jakarta-data-tck/support/build.gradle new file mode 100644 index 0000000000..7ca355fc8e --- /dev/null +++ b/jakarta-data-tck/support/build.gradle @@ -0,0 +1,32 @@ +plugins { + id "java-library" + id "io.micronaut.build.internal.data-base" + id "io.micronaut.build.internal.data-dependencies" +} + +dependencies { + api mn.micronaut.inject.java + api mn.micronaut.inject + api mn.micronaut.aop + api mn.micronaut.context + + api libs.jakarta.data.tck + api(libs.jakarta.data.tck) { + artifact { + classifier = "sources" + } + } + api libs.managed.jakarta.data.api + + api mnLogging.slf4j.jul.to.slf4j + api("jakarta.servlet:jakarta.servlet-api:6.1.0") + api mnSql.jakarta.persistence.api + + runtimeOnly(mnLogging.logback.classic) + + api mnTest.junit.jupiter.api + api libs.junit.platform.suite.engine + + compileOnly projects.micronautDataJdbc + compileOnly projects.micronautDataMongodb +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java new file mode 100644 index 0000000000..3772fa4c95 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; + +/** + * The exception indicates compilation error. + * + * @author Denis Stepanov + */ +@Internal +public final class ArchiveCompilationException extends Exception { + + public ArchiveCompilationException(String message) { + super(message); + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java new file mode 100644 index 0000000000..05c52644c0 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java @@ -0,0 +1,188 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor; +import io.micronaut.annotation.processing.BeanDefinitionInjectProcessor; +import io.micronaut.annotation.processing.PackageConfigurationInjectProcessor; +import io.micronaut.annotation.processing.TypeElementVisitorProcessor; +import io.micronaut.core.annotation.Internal; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +import javax.annotation.processing.Processor; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * IMPORTANT: assumes that it is possible to iterate through the {@code Archive} + * and for each {@code .class} file in there, find a corresponding {@code .java} + * file in this class's classloader. In other words, the CDI TCK source JAR must + * be on classpath. + */ +@Internal +final class ArchiveCompiler { + private final DeploymentDir deploymentDir; + private final Archive deploymentArchive; + + ArchiveCompiler(DeploymentDir deploymentDir, Archive deploymentArchive) { + this.deploymentDir = deploymentDir; + this.deploymentArchive = deploymentArchive; + } + + void compile() throws ArchiveCompilationException, ArchiveCompilerException { + try { + if (deploymentArchive instanceof WebArchive) { + compileWar(); + } else { + throw new ArchiveCompilerException("Unknown archive type: " + deploymentArchive); + } + } catch (IOException e) { + throw new ArchiveCompilerException("Compilation failed", e); + } + } + + private void compileWar() throws ArchiveCompilationException, IOException { + List sourceFiles = new ArrayList<>(); + for (Map.Entry entry : deploymentArchive.getContent().entrySet()) { + String path = entry.getKey().get(); + if (path.startsWith("/WEB-INF/classes") && path.endsWith(".class")) { + String sourceFile = path.replace("/WEB-INF/classes", "") + .replace(".class", ".java"); + + if (sourceFile.contains("$")) { + // skip nested classes + // + // this is crude, maybe there's a better way? + continue; + } + + Path sourceFilePath = deploymentDir.source.resolve(sourceFile.substring(1)); // sourceFile begins with `/` + + Files.createDirectories(sourceFilePath.getParent()); // make sure the directory exists + try (InputStream in = ArchiveCompiler.class.getResourceAsStream(sourceFile)) { + if (in == null) { + // This might be a non-inner class defined in another class + continue; + } + Files.copy(in, sourceFilePath); + } + + sourceFiles.add(sourceFilePath.toFile()); + } else if (path.startsWith("/WEB-INF/lib") && path.endsWith(".jar")) { + String jarFile = path.replace("/WEB-INF/lib", ""); + Path jarFilePath = deploymentDir.lib.resolve(jarFile.substring(1)); // jarFile begins with `/` + + Files.createDirectories(jarFilePath.getParent()); // make sure the directory exists + try (InputStream in = entry.getValue().getAsset().openStream()) { + Files.copy(in, jarFilePath); + } + } + } + + doCompile(sourceFiles, deploymentDir.target.toFile()); + } + + private void doCompile(Collection testSources, File outputDir) throws ArchiveCompilationException, IOException { + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager mgr = compiler.getStandardFileManager(diagnostics, null, null)) { + final String targetDir = outputDir.getAbsolutePath(); + JavaCompiler.CompilationTask task = compiler.getTask( + null, + mgr, + diagnostics, + Arrays.asList("-d", targetDir, "-verbose", "-parameters"), + null, + mgr.getJavaFileObjectsFromFiles( + Stream.concat( + testSources.stream(), + Stream.of( + applicationClass(testSources).toFile() + ) + ).toList() + ) + ); + task.setProcessors(getAnnotationProcessors()); + Boolean success = task.call(); + if (!Boolean.TRUE.equals(success)) { + outputDiagnostics(diagnostics); + } + } + } + + private Path applicationClass(Collection testSources) throws IOException { + String annotations = ""; + String packageName = "ee.jakarta.tck.data"; + final Path packagePath = deploymentDir.target.resolve( + packageName.replace('.', '/') + ); + Files.createDirectories(packagePath); + final Path applicationSource = packagePath.resolve("Application.java"); + String sourceCode = "package " + packageName + ";\n" + + annotations + " class Application {}"; + Files.writeString(applicationSource, sourceCode); + return applicationSource; + } + + private void outputDiagnostics(DiagnosticCollector diagnostics) throws ArchiveCompilationException { + throw new ArchiveCompilationException("Compilation failed:\n" + diagnostics.getDiagnostics() + .stream() + .map(it -> { + System.out.println(it); + if (it.getSource() == null) { + return "- " + it.getMessage(Locale.US); + } + Path source = deploymentDir.source.relativize(Paths.get(it.getSource().toUri().getPath())); + return "- " + source + ":" + it.getLineNumber() + " " + it.getMessage(Locale.US); + }) + .collect(Collectors.joining("\n"))); + } + + private List getAnnotationProcessors() { + List result = new ArrayList<>(); + result.add(new TypeElementVisitorProcessor()); + result.add(new AggregatingTypeElementVisitorProcessor()); + result.add(new PackageConfigurationInjectProcessor()); + result.add(new BeanDefinitionInjectProcessor() { + @Override + protected boolean isProcessedAnnotation(String annotationName) { + return true; + } + }); + return result; + } + +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java new file mode 100644 index 0000000000..65ddbbcf48 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; + +/** + * The exception indicates internal compiler error. + * + * @author Denis Stepanov + */ +@Internal +public final class ArchiveCompilerException extends Exception { + public ArchiveCompilerException(String message) { + super(message); + } + + public ArchiveCompilerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java new file mode 100644 index 0000000000..348c233e09 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.stream.Stream; + +@Internal +final class DeploymentClassLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + DeploymentClassLoader(DeploymentDir deploymentDir) throws IOException { + super(findUrls(deploymentDir)); + } + + private static URL[] findUrls(DeploymentDir deploymentDir) throws IOException { + List result = new ArrayList<>(); + + result.add(deploymentDir.target.toUri().toURL()); + + try (Stream stream = Files.walk(deploymentDir.lib)) { + List jars = stream.filter(p -> p.toString().endsWith(".jar")).toList(); + for (Path jar : jars) { + result.add(jar.toUri().toURL()); + } + } + + return result.toArray(new URL[0]); + } + + @Override + public InputStream getResourceAsStream(String name) { + return super.getResourceAsStream(name); + } + + @Override + public URL getResource(String name) { + return super.getResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return super.getResources(name); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class clazz = findLoadedClass(name); + if (clazz != null) { + return clazz; + } + + try { + clazz = findClass(name); + if (resolve) { + resolveClass(clazz); + } + return clazz; + } catch (ClassNotFoundException ignored) { + return super.loadClass(name, resolve); + } + } + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java new file mode 100644 index 0000000000..4cfe944606 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +@Internal +final class DeploymentDir { + final Path root; + final Path source; + final Path target; + final Path lib; + + DeploymentDir() throws IOException { + this.root = Files.createTempDirectory("odi-arquillian-"); + + this.source = Files.createDirectory(root.resolve("source")); + this.target = Files.createDirectory(root.resolve("target")); + this.lib = Files.createDirectory(root.resolve("lib")); + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java new file mode 100644 index 0000000000..231255908d --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package io.micronaut.validation.tck; + +import ee.jakarta.tck.data.standalone.entity.Coordinate; +import ee.jakarta.tck.data.standalone.entity.EntityTests; +import ee.jakarta.tck.data.standalone.entity.MultipleEntityRepo; +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; +import org.jboss.arquillian.test.spi.TestClass; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.container.ClassContainer; + + +public class TCKArchiveProcessor implements ApplicationArchiveProcessor { + + @Override + public void process(Archive applicationArchive, TestClass testClass) { + if (applicationArchive instanceof ClassContainer) { + if (testClass.getName().equals(EntityTests.class.getName())) { + ((ClassContainer) applicationArchive).addClass(MultipleEntityRepo.class); + ((ClassContainer) applicationArchive).addClass(Coordinate.class); + } + } + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java new file mode 100644 index 0000000000..18a75eae85 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; +import org.jboss.arquillian.container.spi.ConfigurationException; +import org.jboss.arquillian.container.spi.client.container.ContainerConfiguration; + +@Internal +final class TckContainerConfiguration implements ContainerConfiguration { + @Override + public void validate() throws ConfigurationException { + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java new file mode 100644 index 0000000000..6a08aab511 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java @@ -0,0 +1,214 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.validation.tck.runtime.TestClassVisitor; +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; +import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; +import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.InstanceProducer; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.test.spi.TestClass; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.container.LibraryContainer; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.descriptor.api.Descriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Map; +import java.util.Objects; + +@Internal +public final class TckDeployableContainer implements DeployableContainer { + + private static final Logger LOGGER = LoggerFactory.getLogger(TckDeployableContainer.class); + + static ClassLoader old; + + public static ThreadLocal APP = new ThreadLocal<>(); + + @Inject + @DeploymentScoped + private InstanceProducer runningApplicationContext; + + @Inject + @DeploymentScoped + private InstanceProducer applicationClassLoader; + + @Inject + @DeploymentScoped + private InstanceProducer deploymentDir; + + @Inject + private Instance testClass; + + static Object testInstance; + + @Override + public void deploy(Descriptor descriptor) { + throw new UnsupportedOperationException("Container does not support deployment of Descriptors"); + + } + + @Override + public void undeploy(Descriptor descriptor) { + throw new UnsupportedOperationException("Container does not support deployment of Descriptors"); + + } + + @Override + public Class getConfigurationClass() { + return TckContainerConfiguration.class; + } + + @Override + public void setup(TckContainerConfiguration configuration) { + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + @Override + public ProtocolDescription getDefaultProtocol() { + return new ProtocolDescription("Micronaut"); + } + + private static JavaArchive buildSupportLibrary() { + return ShrinkWrap.create(JavaArchive.class, "micronaut-jakarta-data-tck-support.jar") + .addAsManifestResource("META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor") + .addAsResource("logback.xml") + .addPackage(TestClassVisitor.class.getPackage()); + } + + @Override + public ProtocolMetaData deploy(Archive archive) { + if (archive instanceof LibraryContainer libraryContainer) { + libraryContainer.addAsLibrary(buildSupportLibrary()); + } else { + throw new IllegalStateException("Expected library container!"); + } + old = Thread.currentThread().getContextClassLoader(); + if (testClass.get() == null) { + throw new IllegalStateException("Test class not available"); + } + Class testJavaClass = testClass.get().getJavaClass(); + Objects.requireNonNull(testJavaClass); + + try { + DeploymentDir deploymentDir = new DeploymentDir(); + this.deploymentDir.set(deploymentDir); + + new ArchiveCompiler(deploymentDir, archive).compile(); + + ClassLoader classLoader = new DeploymentClassLoader(deploymentDir); + applicationClassLoader.set(classLoader); + Thread.currentThread().setContextClassLoader(classLoader); + + // Reset precached BeanIntrospector + BeanIntrospector shared = BeanIntrospector.SHARED; + Class defautlBeanIntrospectorClass = classLoader.loadClass("io.micronaut.core.beans.DefaultBeanIntrospector"); + ReflectionUtils.setField(defautlBeanIntrospectorClass, "introspectionMap", shared, null); + ReflectionUtils.setField(defautlBeanIntrospectorClass, "classLoader", shared, classLoader); + + ApplicationContext applicationContext = ApplicationContext.builder() + .packages("ee.jakarta.tck.data") + .classLoader(classLoader) + .build() + .start(); + + testInstance = applicationContext.getBean(classLoader.loadClass(testJavaClass.getName())); + + runningApplicationContext.set(applicationContext); + APP.set(applicationContext); + Thread.currentThread().setContextClassLoader(classLoader); + + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + + return new ProtocolMetaData(); + } + + @Override + public void undeploy(Archive archive) { + try { + ApplicationContext appContext = runningApplicationContext.get(); + if (appContext != null) { + Thread.currentThread().setContextClassLoader(runningApplicationContext.get().getClassLoader()); + appContext.stop(); + } + testInstance = null; + + DeploymentDir deploymentDir = this.deploymentDir.get(); + if (deploymentDir != null) { + deleteDirectory(deploymentDir.root); + } + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + private static void deleteDirectory(Path dir) { + try { + Files.walkFileTree(dir, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOGGER.warn("Unable to delete directory: {}", dir, e); + } + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java new file mode 100644 index 0000000000..c1b23ce3fb --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; +import org.jboss.arquillian.container.test.spi.client.protocol.Protocol; +import org.jboss.arquillian.core.spi.LoadableExtension; + +/** + * TCK loadable extension. + */ +@Internal +public class TckExtension implements LoadableExtension { + + @Override + public void register(ExtensionBuilder builder) { +// SLF4JBridgeHandler.removeHandlersForRootLogger(); +// SLF4JBridgeHandler.install(); +// Logger.getLogger("").setLevel(Level.FINEST); + builder.service(ApplicationArchiveProcessor.class, TCKArchiveProcessor.class); + builder.service(DeployableContainer.class, TckDeployableContainer.class); + builder.service(Protocol.class, TckProtocol.class); + builder.observer(TckObserver.class); + } + +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java new file mode 100644 index 0000000000..827af3e43f --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ExecutableMethod; +import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; +import org.jboss.arquillian.core.api.InstanceProducer; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.test.spi.event.suite.After; +import org.jboss.arquillian.test.spi.event.suite.Before; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +/** + * Observe the test events and invoke before/after. + * + * @author Denis Stepanov + */ +public class TckObserver { + + @Inject + @DeploymentScoped + private InstanceProducer runningApplicationContext; + + public void execute(@Observes Before event) { + Object testInstance = TckDeployableContainer.testInstance; + BeanDefinition beanDefinition = runningApplicationContext.get().getBeanDefinition(testInstance.getClass()); + beanDefinition.getExecutableMethods().stream() + .filter(method -> method.hasAnnotation(BeforeEach.class)) + .forEach(executableMethod -> { + ((ExecutableMethod) executableMethod).invoke(testInstance); + }); + } + + public void execute(@Observes After event) { + Object testInstance = TckDeployableContainer.testInstance; + BeanDefinition beanDefinition = runningApplicationContext.get().getBeanDefinition(testInstance.getClass()); + beanDefinition.getExecutableMethods().stream() + .filter(method -> method.hasAnnotation(AfterEach.class)) + .forEach(executableMethod -> { + ((ExecutableMethod) executableMethod).invoke(testInstance); + }); + } + +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java new file mode 100644 index 0000000000..297023bbc0 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java @@ -0,0 +1,153 @@ +/* + * Copyright 2017-2023 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.validation.tck; + +import io.micronaut.core.annotation.Internal; +import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; +import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; +import org.jboss.arquillian.container.test.impl.client.protocol.local.LocalDeploymentPackager; +import org.jboss.arquillian.container.test.impl.execution.event.LocalExecutionEvent; +import org.jboss.arquillian.container.test.spi.ContainerMethodExecutor; +import org.jboss.arquillian.container.test.spi.client.deployment.DeploymentPackager; +import org.jboss.arquillian.container.test.spi.client.protocol.Protocol; +import org.jboss.arquillian.container.test.spi.client.protocol.ProtocolConfiguration; +import org.jboss.arquillian.container.test.spi.command.CommandCallback; +import org.jboss.arquillian.core.api.Event; +import org.jboss.arquillian.core.api.Injector; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.test.spi.TestMethodExecutor; +import org.jboss.arquillian.test.spi.TestResult; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +@Internal +final class TckProtocol implements Protocol { + + @Inject + Instance injector; + + @Override + public Class getProtocolConfigurationClass() { + return TckProtocolConfiguration.class; + } + + @Override + public ProtocolDescription getDescription() { + return new ProtocolDescription("Micronaut"); + } + + @Override + public DeploymentPackager getPackager() { + return new LocalDeploymentPackager(); + } + + @Override + public ContainerMethodExecutor getExecutor(TckProtocolConfiguration protocolConfiguration, + ProtocolMetaData metaData, + CommandCallback callback) { + return injector.get().inject(new TckMethodExecutor()); + } + + public static class TckProtocolConfiguration implements ProtocolConfiguration { + } + + static class TckMethodExecutor implements ContainerMethodExecutor { + + @Inject + Event event; + + @Inject + Instance testResult; + + @Inject + Instance classLoaderInstance; + + @Override + public TestResult invoke(TestMethodExecutor testMethodExecutor) { + + event.fire(new LocalExecutionEvent(new TestMethodExecutor() { + + @Override + public void invoke(Object... parameters) throws Throwable { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(classLoaderInstance.get()); + + Object actualTestInstance = TckDeployableContainer.testInstance; + Method actualMethod = null; + try { + actualMethod = actualTestInstance.getClass().getMethod(getMethod().getName(), + convertToTCCL(getMethod().getParameterTypes())); + } catch (NoSuchMethodException e) { + // the method should still be present, just not public, let's try declared methods + actualMethod = actualTestInstance.getClass().getDeclaredMethod(getMethod().getName(), + convertToTCCL(getMethod().getParameterTypes())); + actualMethod.setAccessible(true); + } + try { + actualMethod.invoke(actualTestInstance, parameters); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause != null) { + throw cause; + } else { + throw e; + } + } + } finally { + Thread.currentThread().setContextClassLoader(loader); + } + } + + @Override + public Method getMethod() { + return testMethodExecutor.getMethod(); + } + + @Override + public Object getInstance() { + return TckDeployableContainer.testInstance; + } + + @Override + public String getMethodName() { + return testMethodExecutor.getMethod().getName(); + } + })); + return testResult.get(); + } + + } + + static Class[] convertToTCCL(Class[] classes) throws ClassNotFoundException { + return convertToCL(classes, Thread.currentThread().getContextClassLoader()); + } + + static Class[] convertToCL(Class[] classes, ClassLoader classLoader) throws ClassNotFoundException { + Class[] result = new Class[classes.length]; + for (int i = 0; i < classes.length; i++) { + if (classes[i].getClassLoader() != classLoader) { + result[i] = classLoader.loadClass(classes[i].getName()); + } else { + result[i] = classes[i]; + } + } + return result; + } + +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java new file mode 100644 index 0000000000..a71c6630de --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2023 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.validation.tck.runtime; + +import ee.jakarta.tck.data.framework.junit.anno.Assertion; +import io.micronaut.context.annotation.Executable; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.Vetoed; +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.annotation.Transient; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.mongodb.annotation.MongoRepository; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import org.bson.BsonType; +import org.bson.codecs.pojo.annotations.BsonRepresentation; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +@Internal +public final class TestClassVisitor implements TypeElementVisitor { + + private final boolean isJdbcImplementation; + private final boolean isMogngoDBImplementation; + + public TestClassVisitor() { + Properties prop = new Properties(); + try { + //load a properties file from class path, inside static method + InputStream resourceAsStream = getClass().getResourceAsStream("/aprocessor.properties"); + if (resourceAsStream != null) { + prop.load(resourceAsStream); + } + } + catch (IOException ex) { + // ignore + } + Object implementation = prop.getOrDefault("implementation", ""); + isJdbcImplementation = implementation.equals("jdbc"); + isMogngoDBImplementation = implementation.equals("mongodb"); + } + + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public int getOrder() { + return 88; + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (element.hasStereotype(Repository.class) && isJdbcImplementation) { + element.annotate(JdbcRepository.class, annotationValueBuilder -> { + annotationValueBuilder.member("dialect", Dialect.H2); + }); + } + if (element.hasStereotype(Repository.class) && isMogngoDBImplementation) { + element.annotate(MongoRepository.class); + } + + if (element.getName().startsWith("ee.jakarta.tck.data") && !element.isEnum()) { + if (element.hasStereotype(Introspected.class)) { + element.annotate(Introspected.class, builder -> { + builder.member("accessKind", new Introspected.AccessKind[]{Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}); + builder.member("visibility", Introspected.Visibility.ANY); + }); + } + element.annotate(Executable.class); + element.annotate(Prototype.class); + + element.getMethods().forEach(ce -> { + if (ce.isStatic() || !ce.isAccessible()) { + ce.annotate(Vetoed.class); + } else { + ce.annotate(Executable.class); + } + }); + } + } + + @Override + public void visitMethod(MethodElement element, VisitorContext context) { + element.removeAnnotation(Assertion.class); + } + + @Override + public void visitField(FieldElement element, VisitorContext context) { + if (isJdbcImplementation || isMogngoDBImplementation) { + if (element.getOwningType().hasStereotype(Entity.class)) { + element.annotate(Nullable.class); + } + if (element.hasStereotype(ElementCollection.class)) { + element.annotate(Transient.class); + } + } + if (isMogngoDBImplementation && element.getType().getName().equals("char")) { + element.annotate(BsonRepresentation.class, builder -> builder.value(BsonType.STRING)); + } + } +} diff --git a/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 0000000000..eb6805e09c --- /dev/null +++ b/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1 @@ +io.micronaut.validation.tck.runtime.TestClassVisitor diff --git a/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 0000000000..d012a66a49 --- /dev/null +++ b/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +io.micronaut.validation.tck.TckExtension diff --git a/settings.gradle b/settings.gradle index 778d04a656..c66795512f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -146,3 +146,8 @@ include 'benchmarks:benchmark-micronaut-data-mongodb' include 'benchmarks:benchmark-spring-data' include 'benchmarks:benchmark-spring-data-jdbc' include 'benchmarks:benchmark-spring-data-mongodb' + +include 'jakarta-data-tck:support' +include 'jakarta-data-tck:hibernate' +include 'jakarta-data-tck:jdbc' +include 'jakarta-data-tck:mongodb' From d4d9533429ebe56e151fa2d565d0135e73ae63b4 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 31 Mar 2025 16:44:56 +0200 Subject: [PATCH 2/3] Fix merge --- .../AbstractHibernateOperations.java | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java index 9819cd311c..be800d1fd3 100644 --- a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java @@ -322,7 +322,11 @@ public Map getQueryHints(@NonNull StoredQuery storedQuery) * @param The result type */ protected void collectFindOne(S session, PreparedQuery preparedQuery, ResultCollector collector) { - collectResults(session, preparedQuery.getQuery(), preparedQuery, preparedQuery.getQueryLimit(), preparedQuery.getSort(), collector); + Limit limit = preparedQuery.getQueryLimit(); + if (!limit.isLimited()) { + limit = preparedQuery.getPageable().getLimit(); + } + collectResults(session, preparedQuery.getQuery(), preparedQuery, limit, preparedQuery.getSort(), collector); } /** @@ -336,29 +340,23 @@ protected void collectFindOne(S session, PreparedQuery preparedQuery, protected void collectFindAll(S session, PreparedQuery preparedQuery, ResultCollector collector) { String queryStr = preparedQuery.getQuery(); Pageable pageable = preparedQuery.getPageable(); - if (pageable != Pageable.UNPAGED) { - if (pageable.getMode() != Mode.OFFSET) { - throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by hibernate operations"); - } - Sort sort = pageable.getSort(); - if (sort.isSorted()) { - queryStr += QUERY_BUILDER.buildOrderBy(queryStr, getEntity(preparedQuery.getRootEntity()), AnnotationMetadata.EMPTY_METADATA, sort, - preparedQuery.isNative()).getQuery(); - } - if (preparedQuery.isNative()) { - // Native queries don't support setting the order - pageable = pageable.withoutSort(); - } + Limit limit = preparedQuery.getQueryLimit(); + if (!limit.isLimited()) { + limit = pageable.getLimit(); } - collectResults(session, queryStr, preparedQuery, preparedQuery.getQueryLimit(), pageable.getSort(), collector); + collectResults(session, queryStr, preparedQuery, limit, pageable.getSort(), collector); } protected final void collectResults(S session, - String queryStr, - PreparedQuery preparedQuery, - Limit limit, - Sort sort, - ResultCollector resultCollector) { + String queryStr, + PreparedQuery preparedQuery, + Limit limit, + Sort sort, + ResultCollector resultCollector) { + if (sort != null && sort.isSorted()) { + queryStr += QUERY_BUILDER.buildOrderBy(queryStr, getEntity(preparedQuery.getRootEntity()), AnnotationMetadata.EMPTY_METADATA, sort, + preparedQuery.isNative()).getQuery(); + } if (preparedQuery.isDtoProjection()) { P q; if (preparedQuery.isNative()) { @@ -366,13 +364,13 @@ protected final void collectResults(S session, } else if (queryStr.toLowerCase(Locale.ENGLISH).startsWith("select new ")) { @SuppressWarnings("unchecked") Class wrapperType = (Class) ReflectionUtils.getWrapperType(preparedQuery.getResultType()); P query = createQuery(session, queryStr, wrapperType); - bindPreparedQuery(query, preparedQuery, limit, sort, session); + bindPreparedQuery(query, preparedQuery, limit, session); resultCollector.collect(query); return; } else { q = createQuery(session, queryStr, Tuple.class); } - bindPreparedQuery(q, preparedQuery, limit, sort, session); + bindPreparedQuery(q, preparedQuery, limit, session); resultCollector.collectTuple(q, tuple -> { Set properties = tuple.getElements().stream().map(TupleElement::getAlias).collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))); return (new BeanIntrospectionMapper() { @@ -398,7 +396,7 @@ public ConversionService getConversionService() { Class rootEntity = preparedQuery.getRootEntity(); if (wrapperType != rootEntity) { P nativeQuery = createNativeQuery(session, queryStr, Tuple.class); - bindPreparedQuery(nativeQuery, preparedQuery, limit, sort, session); + bindPreparedQuery(nativeQuery, preparedQuery, limit, session); resultCollector.collectTuple(nativeQuery, tuple -> { Object o = tuple.get(0); if (wrapperType.isInstance(o)) { @@ -413,7 +411,7 @@ public ConversionService getConversionService() { } else { q = createQuery(session, queryStr, wrapperType); } - bindPreparedQuery(q, preparedQuery, limit, sort, session); + bindPreparedQuery(q, preparedQuery, limit, session); resultCollector.collect(q); } } @@ -522,10 +520,9 @@ public void bindMany(QueryParameterBinding binding, Collection values) { private void bindPreparedQuery(P q, @NonNull PreparedQuery preparedQuery, @NonNull Limit limit, - @NonNull Sort sort, S currentSession) { bindParameters(q, preparedQuery, true); - bindLimitAndSort(q, limit, sort, preparedQuery.getRootEntity()); + bindLimit(q, limit); bindQueryHints(q, preparedQuery, currentSession); } @@ -607,7 +604,7 @@ protected final FlushModeType getFlushModeType(AnnotationMetadata annotationMeta return annotationMetadata.getAnnotationValuesByType(QueryHint.class).stream().filter(av -> FlushModeType.class.getName().equals(av.stringValue("name").orElse(null))).map(av -> av.enumValue("value", FlushModeType.class)).findFirst().orElse(Optional.empty()).orElse(null); } - private void bindLimitAndSort(@NonNull P q, @NonNull Limit limit, @NonNull Sort sort, @NotNull Class entityClass) { + private void bindLimit(@NonNull P q, @NonNull Limit limit) { if (limit.isLimited()) { int max = limit.maxResults(); if (max >= 0) { @@ -618,10 +615,6 @@ private void bindLimitAndSort(@NonNull P q, @NonNull Limit limit, @NonNull Sort setOffset(q, (int) offset); } } - if (sort != null && sort.isSorted()) { - List orders = getOrders(sort, entityClass); - setOrder(q, orders); - } } protected static List> getOrders(Sort sort, Class entityClass) { @@ -643,7 +636,7 @@ protected final void collectPagedResults(CriteriaBuilder criteriaBuilder, S Root root = query.from(entity); bindCriteriaSort(query, root, criteriaBuilder, pagedQuery.getSort()); P q = createQuery(session, query); - bindLimitAndSort(q, pagedQuery.getQueryLimit(), pagedQuery.getSort(), entity); + bindLimit(q, pagedQuery.getQueryLimit()); bindQueryHints(q, pagedQuery, session); resultCollector.collect(q); } @@ -652,7 +645,7 @@ protected final void collectCountOf(CriteriaBuilder criteriaBuilder, S sessi CriteriaQuery countQuery = criteriaBuilder.createQuery(Long.class); countQuery.select(criteriaBuilder.count(countQuery.from(entity))); P countQ = createQuery(session, countQuery); - bindLimitAndSort(countQ, limit, Sort.UNSORTED, entity); + bindLimit(countQ, limit); resultCollector.collect(countQ); } From 42f784b4901ae37fdd269e049c9648258f41f4e0 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 1 Apr 2025 15:17:28 +0200 Subject: [PATCH 3/3] Correct --- .../jakarta}/tck/ArchiveCompilationException.java | 2 +- .../jakarta}/tck/ArchiveCompiler.java | 2 +- .../jakarta}/tck/ArchiveCompilerException.java | 2 +- .../jakarta}/tck/DeploymentClassLoader.java | 2 +- .../jakarta}/tck/DeploymentDir.java | 2 +- .../jakarta}/tck/TCKArchiveProcessor.java | 2 +- .../jakarta}/tck/TckContainerConfiguration.java | 2 +- .../jakarta}/tck/TckDeployableContainer.java | 8 ++------ .../jakarta}/tck/TckExtension.java | 2 +- .../{validation => data/jakarta}/tck/TckObserver.java | 2 +- .../{validation => data/jakarta}/tck/TckProtocol.java | 2 +- .../data/jakarta/tck/runtime/MongoUtils.java | 11 +++++++++++ .../jakarta}/tck/runtime/TestClassVisitor.java | 7 +++---- .../io.micronaut.inject.visitor.TypeElementVisitor | 2 +- .../org.jboss.arquillian.core.spi.LoadableExtension | 2 +- 15 files changed, 28 insertions(+), 22 deletions(-) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/ArchiveCompilationException.java (95%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/ArchiveCompiler.java (99%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/ArchiveCompilerException.java (96%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/DeploymentClassLoader.java (98%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/DeploymentDir.java (96%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TCKArchiveProcessor.java (97%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TckContainerConfiguration.java (96%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TckDeployableContainer.java (95%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TckExtension.java (97%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TckObserver.java (98%) rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/TckProtocol.java (99%) create mode 100644 jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/MongoUtils.java rename jakarta-data-tck/support/src/main/java/io/micronaut/{validation => data/jakarta}/tck/runtime/TestClassVisitor.java (95%) diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilationException.java similarity index 95% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilationException.java index 3772fa4c95..a54c1cd7e8 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilationException.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilationException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompiler.java similarity index 99% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompiler.java index 05c52644c0..dea9cb908a 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompiler.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompiler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor; import io.micronaut.annotation.processing.BeanDefinitionInjectProcessor; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilerException.java similarity index 96% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilerException.java index 65ddbbcf48..5c30ae3695 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/ArchiveCompilerException.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/ArchiveCompilerException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentClassLoader.java similarity index 98% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentClassLoader.java index 348c233e09..04fa8050f3 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentClassLoader.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentClassLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentDir.java similarity index 96% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentDir.java index 4cfe944606..b50aee7c5e 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/DeploymentDir.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/DeploymentDir.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TCKArchiveProcessor.java similarity index 97% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TCKArchiveProcessor.java index 231255908d..ef5e16a84c 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TCKArchiveProcessor.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TCKArchiveProcessor.java @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import ee.jakarta.tck.data.standalone.entity.Coordinate; import ee.jakarta.tck.data.standalone.entity.EntityTests; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckContainerConfiguration.java similarity index 96% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckContainerConfiguration.java index 18a75eae85..5ab0acb9d4 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckContainerConfiguration.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckContainerConfiguration.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; import org.jboss.arquillian.container.spi.ConfigurationException; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckDeployableContainer.java similarity index 95% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckDeployableContainer.java index 6a08aab511..fbe1eefaf6 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckDeployableContainer.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckDeployableContainer.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.Internal; import io.micronaut.core.beans.BeanIntrospector; import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.validation.tck.runtime.TestClassVisitor; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; @@ -42,7 +41,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; -import java.util.Map; import java.util.Objects; @Internal @@ -107,9 +105,7 @@ public ProtocolDescription getDefaultProtocol() { private static JavaArchive buildSupportLibrary() { return ShrinkWrap.create(JavaArchive.class, "micronaut-jakarta-data-tck-support.jar") - .addAsManifestResource("META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor") - .addAsResource("logback.xml") - .addPackage(TestClassVisitor.class.getPackage()); + .addAsResource("logback.xml"); } @Override diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckExtension.java similarity index 97% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckExtension.java index c1b23ce3fb..0e0cc11dc2 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckExtension.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckExtension.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckObserver.java similarity index 98% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckObserver.java index 827af3e43f..e5f1fae314 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckObserver.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckObserver.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.context.ApplicationContext; import io.micronaut.inject.BeanDefinition; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckProtocol.java similarity index 99% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckProtocol.java index 297023bbc0..a8de6eadfc 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/TckProtocol.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/TckProtocol.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck; +package io.micronaut.data.jakarta.tck; import io.micronaut.core.annotation.Internal; import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/MongoUtils.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/MongoUtils.java new file mode 100644 index 0000000000..74a11a3c90 --- /dev/null +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/MongoUtils.java @@ -0,0 +1,11 @@ +package io.micronaut.data.jakarta.tck.runtime; + +import io.micronaut.inject.ast.FieldElement; +import org.bson.BsonType; +import org.bson.codecs.pojo.annotations.BsonRepresentation; + +class MongoUtils { + public static void bson(FieldElement element) { + element.annotate(BsonRepresentation.class, builder -> builder.value(BsonType.STRING)); + } +} diff --git a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/TestClassVisitor.java similarity index 95% rename from jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java rename to jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/TestClassVisitor.java index a71c6630de..b8c3d64f38 100644 --- a/jakarta-data-tck/support/src/main/java/io/micronaut/validation/tck/runtime/TestClassVisitor.java +++ b/jakarta-data-tck/support/src/main/java/io/micronaut/data/jakarta/tck/runtime/TestClassVisitor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.validation.tck.runtime; +package io.micronaut.data.jakarta.tck.runtime; import ee.jakarta.tck.data.framework.junit.anno.Assertion; import io.micronaut.context.annotation.Executable; @@ -34,8 +34,6 @@ import io.micronaut.inject.visitor.VisitorContext; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; -import org.bson.BsonType; -import org.bson.codecs.pojo.annotations.BsonRepresentation; import java.io.IOException; import java.io.InputStream; @@ -121,7 +119,8 @@ public void visitField(FieldElement element, VisitorContext context) { } } if (isMogngoDBImplementation && element.getType().getName().equals("char")) { - element.annotate(BsonRepresentation.class, builder -> builder.value(BsonType.STRING)); + MongoUtils.bson(element); } } + } diff --git a/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index eb6805e09c..19c9c5ec02 100644 --- a/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/jakarta-data-tck/support/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1 +1 @@ -io.micronaut.validation.tck.runtime.TestClassVisitor +io.micronaut.data.jakarta.tck.runtime.TestClassVisitor diff --git a/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension index d012a66a49..4170fe5e56 100644 --- a/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension +++ b/jakarta-data-tck/support/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -1 +1 @@ -io.micronaut.validation.tck.TckExtension +io.micronaut.data.jakarta.tck.TckExtension