diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 78142de2043..d91a8e4fec0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,14 +45,14 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.1 + uses: graalvm/setup-graalvm@v1.2.2 with: distribution: 'graalvm' java-version: ${{ matrix.java }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.3.2 + uses: gradle/gradle-build-action@v3.4.2 - name: "❓ Optional setup step" run: | @@ -78,7 +78,7 @@ jobs: - name: "📜 Upload binary compatibility check results" if: matrix.java == '17' - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5afb151f4e0..f234f585bd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: artifacts-sha256 path: artifacts-sha256 @@ -115,7 +115,7 @@ jobs: artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} steps: - name: Download artifacts-sha256 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: artifacts-sha256 # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job @@ -146,11 +146,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Download artifacts - # Important: update actions/download-artifact to v4 only when generator_generic_slsa3.yml is also compatible. - # See https://github.com/slsa-framework/slsa-github-generator/issues/3068 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: gradle-build-outputs path: build/repo diff --git a/config/checkstyle/custom-suppressions.xml b/config/checkstyle/custom-suppressions.xml new file mode 100644 index 00000000000..0421e77d823 --- /dev/null +++ b/config/checkstyle/custom-suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index f7d14fb2eb6..73f71b3a499 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -9,5 +9,4 @@ - diff --git a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/common/CosmosDatabaseInitializer.java b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/common/CosmosDatabaseInitializer.java index bbc0e637611..e5ae1404ff0 100644 --- a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/common/CosmosDatabaseInitializer.java +++ b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/common/CosmosDatabaseInitializer.java @@ -66,6 +66,11 @@ @Requires(property = "azure.cosmos.key") final class CosmosDatabaseInitializer { + // For a limited time, if the query runs against a region or emulator that has not yet been updated with the + // new NonStreamingOrderBy query feature the client might run into some issue of not being able to recognize this, + // and throw a 400 exception. If the environment variable `AZURE_COSMOS_DISABLE_NON_STREAMING_ORDER_BY` is set to + // True to opt out of this new query feature, then OLD query features will be used to operate correctly. + private static final String DISABLE_NON_STREAMING_ORDER_BY = "COSMOS.AZURE_COSMOS_DISABLE_NON_STREAMING_ORDER_BY"; private static final Logger LOG = LoggerFactory.getLogger(CosmosDatabaseInitializer.class); /** @@ -82,6 +87,7 @@ void initialize(CosmosClient cosmosClient, @Nullable CosmosDiagnosticsProcessor cosmosDiagnosticsProcessor, CosmosDatabaseConfiguration configuration) { + System.setProperty(DISABLE_NON_STREAMING_ORDER_BY, Boolean.toString(configuration.isDisableNonStreamingOrderBy())); if (LOG.isDebugEnabled()) { LOG.debug("Cosmos Db Initialization Start"); } diff --git a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/config/CosmosDatabaseConfiguration.java b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/config/CosmosDatabaseConfiguration.java index bca7290a4cc..c08c8fcb7b7 100644 --- a/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/config/CosmosDatabaseConfiguration.java +++ b/data-azure-cosmos/src/main/java/io/micronaut/data/cosmos/config/CosmosDatabaseConfiguration.java @@ -51,6 +51,8 @@ public final class CosmosDatabaseConfiguration { private boolean queryMetricsEnabled = true; + private boolean disableNonStreamingOrderBy = false; + public ThroughputSettings getThroughput() { return throughput; } @@ -136,6 +138,26 @@ public void setQueryMetricsEnabled(boolean queryMetricsEnabled) { this.queryMetricsEnabled = queryMetricsEnabled; } + /** + * Gets an indicator telling whether non-streaming order by is by default disabled. + * By default, it is not disabled currently and users can change it as needed. + * Effectively, this value will be set as "COSMOS.AZURE_COSMOS_DISABLE_NON_STREAMING_ORDER_BY" env variable. + * + * @return the disabled non-streaming order by indicator + */ + public boolean isDisableNonStreamingOrderBy() { + return disableNonStreamingOrderBy; + } + + /** + * Sets an indicator telling whether non-streaming order by is by default disabled. + * + * @param disableNonStreamingOrderBy the disabled non-streaming order by indicator + */ + public void setDisableNonStreamingOrderBy(boolean disableNonStreamingOrderBy) { + this.disableNonStreamingOrderBy = disableNonStreamingOrderBy; + } + /** * Throughput settings for database. */ diff --git a/data-azure-cosmos/src/test/groovy/io/micronaut/data/azure/AzureCosmosTestProperties.groovy b/data-azure-cosmos/src/test/groovy/io/micronaut/data/azure/AzureCosmosTestProperties.groovy index 68a92d0430e..fac535d662b 100644 --- a/data-azure-cosmos/src/test/groovy/io/micronaut/data/azure/AzureCosmosTestProperties.groovy +++ b/data-azure-cosmos/src/test/groovy/io/micronaut/data/azure/AzureCosmosTestProperties.groovy @@ -33,6 +33,7 @@ trait AzureCosmosTestProperties implements TestPropertyProvider { System.setProperty("javax.net.ssl.trustStoreType", "PKCS12") def defaultProps = [ + 'azure.cosmos.database.disable-non-streaming-order-by' : 'true', 'azure.cosmos.default-gateway-mode' : 'true', 'azure.cosmos.endpoint-discovery-enabled' : 'false', 'azure.cosmos.endpoint' : emulator.getEmulatorEndpoint(), diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder2.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder2.java index 471f015e606..783d8b02b3d 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder2.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder2.java @@ -141,7 +141,7 @@ protected SqlPredicateVisitor createPredicateVisitor(AnnotationMetadata annotati private static final String ARRAY_CONTAINS = "ARRAY_CONTAINS"; @Override - public void visitIsNull(QueryPropertyPath propertyPath) { + public void visitIsNull(PersistentPropertyPath propertyPath) { query.append(NOT).append(SPACE).append(IS_DEFINED).append(OPEN_BRACKET); appendPropertyRef(propertyPath); query.append(CLOSE_BRACKET).append(SPACE).append(OR).append(SPACE); @@ -151,7 +151,7 @@ public void visitIsNull(QueryPropertyPath propertyPath) { } @Override - public void visitIsNotNull(QueryPropertyPath propertyPath) { + public void visitIsNotNull(PersistentPropertyPath propertyPath) { query.append(IS_DEFINED).append(OPEN_BRACKET); appendPropertyRef(propertyPath); query.append(CLOSE_BRACKET).append(SPACE).append(AND).append(SPACE); @@ -161,7 +161,7 @@ public void visitIsNotNull(QueryPropertyPath propertyPath) { } @Override - public void visitIsEmpty(QueryPropertyPath propertyPath) { + public void visitIsEmpty(PersistentPropertyPath propertyPath) { query.append(NOT).append(SPACE).append(IS_DEFINED).append(OPEN_BRACKET); appendPropertyRef(propertyPath); query.append(CLOSE_BRACKET).append(SPACE).append(OR).append(SPACE); @@ -173,7 +173,7 @@ public void visitIsEmpty(QueryPropertyPath propertyPath) { } @Override - public void visitIsNotEmpty(QueryPropertyPath propertyPath) { + public void visitIsNotEmpty(PersistentPropertyPath propertyPath) { query.append(IS_DEFINED).append(OPEN_BRACKET); appendPropertyRef(propertyPath); query.append(CLOSE_BRACKET).append(SPACE).append(AND).append(SPACE); @@ -185,11 +185,11 @@ public void visitIsNotEmpty(QueryPropertyPath propertyPath) { } @Override - public void visitArrayContains(QueryPropertyPath leftProperty, Expression expression) { + public void visitArrayContains(PersistentPropertyPath leftProperty, Expression expression) { query.append(ARRAY_CONTAINS).append(OPEN_BRACKET); appendPropertyRef(leftProperty); query.append(COMMA); - appendExpression(leftProperty, expression); + appendExpression(expression, leftProperty); query.append(COMMA); query.append("true").append(CLOSE_BRACKET); } 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 9f71177f67a..5d501d3405a 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 @@ -37,6 +37,7 @@ 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.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.FunctionExpression; @@ -45,6 +46,7 @@ import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; 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.LikePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyInPredicate; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; @@ -1033,18 +1035,14 @@ public void visitStartsWith(PersistentPropertyPath leftProperty, Expression e } @Override - public void visitLike(PersistentPropertyPath leftProperty, Expression expression) { - handleRegexPropertyExpression(leftProperty, false, false, false, false, expression); - } - - @Override - public void visitRLike(PersistentPropertyPath leftProperty, Expression expression) { - throw new UnsupportedOperationException("RLike is not supported by this implementation."); - } - - @Override - public void visitILike(PersistentPropertyPath leftProperty, Expression expression) { - throw new UnsupportedOperationException("ILike is not supported by this implementation."); + public void visit(LikePredicate likePredicate) { + if (likePredicate.isCaseInsensitive()) { + throw new UnsupportedOperationException("ILike is not supported by this implementation."); + } + handleRegexPropertyExpression( + CriteriaUtils.requireProperty(likePredicate.getExpression()).getPropertyPath(), + false, false, false, false, + likePredicate.getPattern()); } @Override diff --git a/data-hibernate-jpa/src/main/resources/META-INF/native-image/io.micronaut.data/data-hibernate-jpa-graal/reflect-config.json b/data-hibernate-jpa/src/main/resources/META-INF/native-image/io.micronaut.data/data-hibernate-jpa-graal/reflect-config.json index 1a281b488f4..4ef57458048 100644 --- a/data-hibernate-jpa/src/main/resources/META-INF/native-image/io.micronaut.data/data-hibernate-jpa-graal/reflect-config.json +++ b/data-hibernate-jpa/src/main/resources/META-INF/native-image/io.micronaut.data/data-hibernate-jpa-graal/reflect-config.json @@ -19,5 +19,15 @@ "typeReachable": "org.hibernate.event.service.internal.EventListenerGroupImpl" }, "unsafeAllocated": true + }, + { + "name": "org.hibernate.binder.internal.BatchSizeBinder", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] } ] 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 a27d646621f..038d4655202 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 @@ -43,8 +43,4 @@ class H2CursoredPaginationSpec extends AbstractCursoredPageSpec { return br } - @Override - void init() { - pr.deleteAll() - } } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/one2many/MultipleOneToManySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/one2many/MultipleOneToManySpec.groovy new file mode 100644 index 00000000000..a7f3aa061c1 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/one2many/MultipleOneToManySpec.groovy @@ -0,0 +1,157 @@ +package io.micronaut.data.jdbc.h2.one2many + +import io.micronaut.data.annotation.GeneratedValue +import io.micronaut.data.annotation.Id +import io.micronaut.data.annotation.Join +import io.micronaut.data.annotation.MappedEntity +import io.micronaut.data.jdbc.annotation.JdbcRepository +import io.micronaut.data.jdbc.h2.H2DBProperties +import io.micronaut.data.model.query.builder.sql.Dialect +import io.micronaut.data.repository.CrudRepository +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Instant + +/** + * Test case when an entity has more than one one-to-many collection of the same entity. + */ +@MicronautTest +@H2DBProperties +class MultipleOneToManySpec extends Specification { + + @Shared + @Inject + MatchRepository matchRepository + + @Shared + @Inject + TeamRepository teamRepository + + void "test multiple one to many"() { + given: + def liverpool = teamRepository.save(new Team(name: "Liverpool")) + def manchester = teamRepository.save(new Team(name: "Manchester United")) + def westHam = teamRepository.save(new Team(name: "West Ham United")) + def matchJune1st = matchRepository.save(new Match(date: createDate(2024, 6, 1), location: "Liverpool", + homeTeam: liverpool, awayTeam: manchester)) + matchRepository.save(new Match(date: createDate(2024, 6, 3), location: "Liverpool", + homeTeam: liverpool, awayTeam: westHam)) + matchRepository.save(new Match(date: createDate(2024, 6, 4), location: "Manchester", + homeTeam: manchester, awayTeam: liverpool)) + matchRepository.save(new Match(date: createDate(2024, 6, 5), location: "London", + homeTeam: westHam, awayTeam: manchester)) + when: + def match = matchRepository.getById(matchJune1st.id) + then: + match + match.date == matchJune1st.date + match.location == matchJune1st.location + match.homeTeam != match.awayTeam + match.homeTeam.id == liverpool.id + match.awayTeam.id == manchester.id + when: + def team = teamRepository.getById(liverpool.id) + then: + team + team.id == liverpool.id + team.name == liverpool.name + team.homeMatches.size() == 2 + team.awayMatches.size() == 1 + team.homeMatches[0].awayTeam != team.homeMatches[0].homeTeam + team.homeMatches[1].awayTeam != team.homeMatches[1].homeTeam + team.awayMatches[0].awayTeam != team.awayMatches[0].homeTeam + } + + Instant createDate(int year, int month, int day) { + Calendar calendar = Calendar.instance + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, day) + calendar.toInstant() + } +} + +@MappedEntity +class Team { + @Id + @GeneratedValue + Long id + + String name + + @OneToMany(mappedBy = "homeTeam") + Set homeMatches + + @OneToMany(mappedBy = "awayTeam") + Set awayMatches + + boolean equals(o) { + if (this.is(o)) return true + if (o == null || getClass() != o.class) return false + + Team team = (Team) o + + if (name != team.name) return false + + return true + } + + int hashCode() { + return (name != null ? name.hashCode() : 0) + } +} + +@MappedEntity +class Match { + @Id + @GeneratedValue + Long id + + Instant date + + String location + + @ManyToOne(optional = false) + Team homeTeam + + @ManyToOne(optional = false) + Team awayTeam + + boolean equals(o) { + if (this.is(o)) return true + if (o == null || getClass() != o.class) return false + + Match match = (Match) o + + if (date != match.date) return false + if (location != match.location) return false + + return true + } + + int hashCode() { + int result + result = (date != null ? date.hashCode() : 0) + result = 31 * result + (location != null ? location.hashCode() : 0) + return result + } +} + +@JdbcRepository(dialect = Dialect.H2) +interface TeamRepository extends CrudRepository { + @Join(value = "homeMatches", type = Join.Type.LEFT_FETCH) + @Join(value = "awayMatches", type = Join.Type.LEFT_FETCH) + Team getById(Long id); +} + +@JdbcRepository(dialect = Dialect.H2) +interface MatchRepository extends CrudRepository { + @Join(value = "homeTeam", type = Join.Type.LEFT_FETCH) + @Join(value = "awayTeam", type = Join.Type.LEFT_FETCH) + Match getById(Long id); +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy index e65c34e7d1b..d637ea7bab2 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy @@ -16,17 +16,12 @@ package io.micronaut.data.jdbc.mysql import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Shared class MysqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements MySQLTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context - @Memoized @Override PersonRepository getPersonRepository() { @@ -39,9 +34,4 @@ class MysqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements My return context.getBean(MySqlBookRepository) } - @Override - void init() { - context = ApplicationContext.run(properties) - } - } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy index 6bb64ef6a27..6bc85e23d77 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy @@ -16,17 +16,12 @@ package io.micronaut.data.jdbc.oraclexe import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Shared class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements OracleTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context - @Override @Memoized PersonRepository getPersonRepository() { @@ -39,9 +34,4 @@ class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements return context.getBean(OracleXEBookRepository) } - @Override - void init() { - context = ApplicationContext.run(properties) - } - } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy index 214b741c505..ddf7f7203c2 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -16,17 +16,11 @@ package io.micronaut.data.jdbc.postgres import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Ignore -import spock.lang.Shared -@Ignore("Causes error: 'FATAL: sorry, too many clients already'") class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context @Memoized @Override @@ -40,8 +34,4 @@ class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements return context.getBean(PostgresBookRepository) } - @Override - void init() { - context = ApplicationContext.run(getProperties()) - } } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy index dc13c07c9cb..f747d0a7467 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy @@ -15,17 +15,12 @@ */ package io.micronaut.data.jdbc.sqlserver -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Shared class SqlServerCursoredPaginationSpec extends AbstractCursoredPageSpec implements MSSQLTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context - @Override PersonRepository getPersonRepository() { return context.getBean(MSSQLPersonRepository) @@ -36,8 +31,4 @@ class SqlServerCursoredPaginationSpec extends AbstractCursoredPageSpec implement return context.getBean(MSBookRepository) } - @Override - void init() { - context = ApplicationContext.run(properties) - } } 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 5d2b63df38c..950377b6fa2 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 @@ -126,7 +126,7 @@ default CursoredPageable previousPageable() { @Override default @NonNull CursoredPage map(Function function) { List content = getContent().stream().map(function).collect(Collectors.toList()); - return new DefaultCursoredPage<>(content, getPageable(), getCursors(), getTotalSize()); + return new DefaultCursoredPage<>(content, getPageable(), getCursors(), hasTotalSize() ? getTotalSize() : null); } /** 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 1f817ceb811..9b9032f10d7 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 @@ -96,7 +96,7 @@ default boolean hasNext() { @Override default @NonNull Page map(Function function) { List content = getContent().stream().map(function).toList(); - return new DefaultPage<>(content, getPageable(), getTotalSize()); + return new DefaultPage<>(content, getPageable(), hasTotalSize() ? getTotalSize() : null); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/PersistentEntityUtils.java b/data-model/src/main/java/io/micronaut/data/model/PersistentEntityUtils.java index 72e02ad86c1..24c1202dc80 100644 --- a/data-model/src/main/java/io/micronaut/data/model/PersistentEntityUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/PersistentEntityUtils.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.function.Consumer; /** * Persistent entity utils. @@ -51,7 +52,16 @@ private PersistentEntityUtils() { * @since 4.2.0 */ public static boolean isAccessibleWithoutJoin(Association association, PersistentProperty persistentProperty) { - return association.getAssociatedEntity().getIdentity() == persistentProperty && !association.isForeignKey(); + PersistentProperty identity = association.getAssociatedEntity().getIdentity(); + if (identity instanceof Embedded embedded) { + for (PersistentProperty property : embedded.getAssociatedEntity().getPersistentProperties()) { + if (property == persistentProperty) { + return !association.isForeignKey(); + } + } + + } + return identity == persistentProperty && !association.isForeignKey(); } /** @@ -139,6 +149,12 @@ public static void traversePersistentProperties(PersistentPropertyPath propertyP traversePersistentProperties(propertyPath.getAssociations(), propertyPath.getProperty(), true, consumerProperty); } + public static void traverse(PersistentPropertyPath propertyPath, Consumer consumer) { + BiConsumer, PersistentProperty> consumerProperty + = (associations, property) -> consumer.accept(new PersistentPropertyPath(associations, property)); + traversePersistentProperties(propertyPath.getAssociations(), propertyPath.getProperty(), true, consumerProperty); + } + public static void traversePersistentProperties(PersistentPropertyPath propertyPath, boolean traverseEmbedded, BiConsumer, PersistentProperty> consumerProperty) { diff --git a/data-model/src/main/java/io/micronaut/data/model/PersistentPropertyPath.java b/data-model/src/main/java/io/micronaut/data/model/PersistentPropertyPath.java index 4f588e1858f..2c0dca1dd9d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/PersistentPropertyPath.java +++ b/data-model/src/main/java/io/micronaut/data/model/PersistentPropertyPath.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.ListIterator; +import java.util.Objects; import java.util.Optional; import java.util.StringJoiner; @@ -38,6 +39,15 @@ public class PersistentPropertyPath { private final PersistentProperty property; private String path; + /** + * Default constructor. + * + * @param property The property + */ + public PersistentPropertyPath(@NonNull PersistentProperty property) { + this(List.of(), property, null); + } + /** * Default constructor. * @@ -96,13 +106,13 @@ public static PersistentPropertyPath of(List associations, @NonNull * @return The root bean - possibly modified */ public Object setPropertyValue(Object bean, Object value) { - if (!(property instanceof RuntimePersistentProperty runtimeProperty)) { + if (!(property instanceof RuntimePersistentProperty runtimeProperty)) { throw new IllegalStateException("Expected runtime property!"); } return setProperty(associations, runtimeProperty, bean, value); } - private Object setProperty(List associations, RuntimePersistentProperty property, Object bean, Object value) { + private Object setProperty(List associations, RuntimePersistentProperty property, Object bean, Object value) { if (associations.isEmpty()) { BeanProperty beanProperty = property.getProperty(); return setProperty(beanProperty, bean, value); @@ -279,4 +289,26 @@ public Optional findNamingStrategy() { } return owner.findNamingStrategy(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersistentPropertyPath that = (PersistentPropertyPath) o; + return Objects.equals(associations, that.associations) && Objects.equals(property, that.property); + } + + @Override + public int hashCode() { + return property.hashCode(); + } + + @Override + public String toString() { + return "PersistentPropertyPath{associations=" + associations + ", property=" + property + '}'; + } } 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 e92be06e625..09b67e60a61 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 @@ -78,22 +78,24 @@ public interface PersistentEntityCriteriaBuilder extends CriteriaBuilder { Predicate isNotEmptyString(Expression expression); /** - * Creates a rlike predicate between an expression x and y. + * Creates an case-insensitive like predicate. * * @param x The expression - * @param y The expression + * @param pattern The pattern * @return a new predicate */ - Predicate rlikeString(Expression x, Expression y); + Predicate ilike(Expression x, Expression pattern); /** - * Creates an ilike predicate between an expression x and y. + * Creates an case-insensitive like predicate. * * @param x The expression - * @param y The expression + * @param pattern The pattern * @return a new predicate */ - Predicate ilikeString(Expression x, Expression y); + default Predicate ilike(Expression x, String pattern) { + return ilike(x, literal(pattern)); + } /** * Checks if the expression x starts with the expression y. 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 561ee29aa43..e79de0ced43 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 @@ -19,9 +19,6 @@ import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import io.micronaut.data.model.DataType; -import io.micronaut.data.model.JsonDataType; -import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaQuery; import io.micronaut.data.model.jpa.criteria.impl.expression.BinaryExpression; @@ -32,6 +29,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExpressionBinaryPredicate; +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.PersistentPropertyBetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyBinaryPredicate; @@ -41,8 +39,6 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateUnaryOp; 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.query.BindingParameter; -import io.micronaut.data.model.query.builder.QueryParameterBinding; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CollectionJoin; import jakarta.persistence.criteria.CompoundSelection; @@ -321,14 +317,8 @@ public Predicate isNotEmptyString(@NonNull Expression expression) { @Override @NonNull - public Predicate rlikeString(@NonNull Expression x, @NonNull Expression y) { - return new PersistentPropertyBinaryPredicate<>(requireProperty(x), requirePropertyParameterOrLiteral(y), PredicateBinaryOp.RLIKE); - } - - @Override - @NonNull - public Predicate ilikeString(@NonNull Expression x, @NonNull Expression y) { - return new PersistentPropertyBinaryPredicate<>(requireProperty(x), requirePropertyParameterOrLiteral(y), PredicateBinaryOp.ILIKE); + public Predicate ilike(@NonNull Expression x, @NonNull Expression pattern) { + return new LikePredicate(x, pattern, null, false, true); } @Override @@ -885,40 +875,7 @@ public ParameterExpression parameter(@NonNull Class paramClass, @NonNu */ @NonNull public ParameterExpression parameter(@NonNull Class paramClass, @Nullable String name, @Nullable Object value) { - return new ParameterExpressionImpl<>(paramClass, name) { - - @Override - public QueryParameterBinding bind(BindingContext bindingContext) { - String name = bindingContext.getName() == null ? String.valueOf(bindingContext.getIndex()) : bindingContext.getName(); - PersistentPropertyPath outgoingQueryParameterProperty = bindingContext.getOutgoingQueryParameterProperty(); - if (outgoingQueryParameterProperty == null) { - return new SimpleParameterBinding(name, DataType.forType(paramClass), bindingContext, value); - } - return new PropertyPathParameterBinding(name, outgoingQueryParameterProperty, bindingContext, value); - } - }; - } - - /** - * Create a new parameter with possible constant value. - * - * @param paramClass The param class - * @param propertyPath The property path - * @param value The param value - * @param The param type - * @return the parameter expression - * @since 4.9 - */ - @NonNull - public ParameterExpression parameterOfProperty(@NonNull Class paramClass, @NonNull PersistentPropertyPath propertyPath, @Nullable Object value) { - return new ParameterExpressionImpl<>(paramClass, propertyPath.getProperty().getName()) { - - @Override - public QueryParameterBinding bind(BindingContext bindingContext) { - String name = bindingContext.getName() == null ? String.valueOf(bindingContext.getIndex()) : bindingContext.getName(); - return new PropertyPathParameterBinding(name, propertyPath, bindingContext, value); - } - }; + return new DefaultParameterExpression<>(paramClass, name, value); } /** @@ -1033,130 +990,80 @@ public > Expression> values(@NonNull M map) @Override @NonNull - public Predicate like(@NonNull Expression x, @NonNull Expression pattern) { - return new PersistentPropertyBinaryPredicate<>(requireProperty(x), requirePropertyParameterOrLiteral(pattern), PredicateBinaryOp.LIKE); + public Predicate regex(@NonNull Expression x, @NonNull Expression pattern) { + return new PersistentPropertyBinaryPredicate<>(requireProperty(x), requirePropertyParameterOrLiteral(pattern), PredicateBinaryOp.REGEX); } @Override @NonNull - public Predicate regex(@NonNull Expression x, @NonNull Expression pattern) { - return new PersistentPropertyBinaryPredicate<>(requireProperty(x), requirePropertyParameterOrLiteral(pattern), PredicateBinaryOp.REGEX); + public Predicate like(@NonNull Expression x, @NonNull Expression pattern) { + return new LikePredicate(x, pattern, null, false); } @Override @NonNull public Predicate like(@NonNull Expression x, @NonNull String pattern) { - return new PersistentPropertyBinaryPredicate<>(requireProperty(x), literal(pattern), PredicateBinaryOp.LIKE); + return new LikePredicate(x, literal(pattern), null, false); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate like(@NonNull Expression x, @NonNull Expression pattern, @NonNull Expression escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, pattern, escapeChar, false); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate like(@NonNull Expression x, @NonNull Expression pattern, char escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, pattern, literal(escapeChar), false); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate like(@NonNull Expression x, @NonNull String pattern, @NonNull Expression escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, literal(pattern), escapeChar, false); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate like(@NonNull Expression x, @NonNull String pattern, char escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, literal(pattern), literal(escapeChar), false); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull Expression pattern) { - throw notSupportedOperation(); + return new LikePredicate(x, pattern, null, true); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull String pattern) { - throw notSupportedOperation(); + return new LikePredicate(x, literal(pattern), null, true); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull Expression pattern, @NonNull Expression escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, pattern, escapeChar, true); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull Expression pattern, char escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, pattern, literal(escapeChar), true); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull String pattern, @NonNull Expression escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, literal(pattern), escapeChar, true); } - /** - * Not supported yet. - * - * {@inheritDoc} - */ @Override @NonNull public Predicate notLike(@NonNull Expression x, @NonNull String pattern, char escapeChar) { - throw notSupportedOperation(); + return new LikePredicate(x, literal(pattern), literal(escapeChar), true); } @Override @@ -1613,75 +1520,4 @@ public Expression round(Expression x, Integer n) { throw notSupportedOperation(); } - private record PropertyPathParameterBinding(String getName, - PersistentPropertyPath outgoingQueryParameterProperty, - BindingParameter.BindingContext bindingContext, - @Nullable Object value) implements QueryParameterBinding { - - @Override - public String getKey() { - return getName; - } - - @Override - public DataType getDataType() { - return outgoingQueryParameterProperty.getProperty().getDataType(); - } - - @Override - public JsonDataType getJsonDataType() { - return outgoingQueryParameterProperty.getProperty().getJsonDataType(); - } - - @Override - public String[] getPropertyPath() { - return outgoingQueryParameterProperty.getArrayPath(); - } - - @Override - public boolean isExpandable() { - return bindingContext.isExpandable(); - } - - @Override - public Object getValue() { - return value; - } - } - - private record SimpleParameterBinding(String getName, - DataType dataType, - BindingParameter.BindingContext bindingContext, - @Nullable Object value) implements QueryParameterBinding { - - @Override - public String getKey() { - return getName; - } - - @Override - public DataType getDataType() { - return dataType; - } - - @Override - public JsonDataType getJsonDataType() { - return JsonDataType.DEFAULT; - } - - @Override - public String[] getPropertyPath() { - return null; - } - - @Override - public boolean isExpandable() { - return bindingContext.isExpandable(); - } - - @Override - public Object getValue() { - return value; - } - } } 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 10650df77e4..4474d01616c 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 @@ -97,7 +97,11 @@ protected QueryModelPredicateVisitor createPredicateVisitor(QueryModel queryMode @Override public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder) { - return queryBuilder.buildDelete(annotationMetadata, getQueryModel()); + QueryBuilder2 queryBuilder2 = QueryResultPersistentEntityCriteriaQuery.findQueryBuilder2(queryBuilder); + if (queryBuilder2 == null) { + return queryBuilder.buildDelete(annotationMetadata, getQueryModel()); + } + return buildQuery(annotationMetadata, queryBuilder2); } @Override 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 9b174d6ea3d..0f0e2fc23c8 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 @@ -37,6 +37,7 @@ import io.micronaut.data.model.jpa.criteria.impl.util.Joiner; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.model.query.QueryModel; +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.CriteriaBuilder; @@ -91,6 +92,15 @@ protected AbstractPersistentEntityCriteriaQuery(Class resultType, CriteriaBui this.criteriaBuilder = criteriaBuilder; } + @Override + public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder) { + QueryBuilder2 queryBuilder2 = QueryResultPersistentEntityCriteriaQuery.findQueryBuilder2(queryBuilder); + if (queryBuilder2 == null) { + return queryBuilder.buildQuery(annotationMetadata, getQueryModel()); + } + return buildQuery(annotationMetadata, queryBuilder2); + } + @Override public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder2 queryBuilder) { SelectQueryDefinitionImpl definition = new SelectQueryDefinitionImpl( 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 c4ff9ed199e..f280617dc4b 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 @@ -75,7 +75,11 @@ public abstract class AbstractPersistentEntityCriteriaUpdate implements Persi @Override public QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder) { - return queryBuilder.buildUpdate(annotationMetadata, getQueryModel(), updateValues); + QueryBuilder2 queryBuilder2 = QueryResultPersistentEntityCriteriaQuery.findQueryBuilder2(queryBuilder); + if (queryBuilder2 == null) { + return queryBuilder.buildUpdate(annotationMetadata, getQueryModel(), updateValues); + } + return buildQuery(annotationMetadata, queryBuilder2); } @Override 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 43732bbe7ed..18045ac0051 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 @@ -197,7 +197,9 @@ public static Set> extractPredicateParameters(Expression< } private static void extractPredicateParameters(Expression predicate, Set> parameters) { - if (predicate instanceof PersistentPropertyBinaryPredicate pp) { + if (predicate instanceof LiteralExpression) { + return; + } else if (predicate instanceof PersistentPropertyBinaryPredicate pp) { if (pp.getExpression() instanceof ParameterExpression parameterExpression) { parameters.add(parameterExpression); } @@ -215,6 +217,8 @@ private static void extractPredicateParameters(Expression predicate, Set pred : disjunctionPredicate.getPredicates()) { extractPredicateParameters(pred, parameters); } + } else { + throw new IllegalStateException("Unsupported predicate type: " + predicate.getClass().getSimpleName()); } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultParameterExpression.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultParameterExpression.java new file mode 100644 index 00000000000..732d7e793ce --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultParameterExpression.java @@ -0,0 +1,53 @@ +/* + * 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.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.model.DataType; +import io.micronaut.data.model.PersistentPropertyPath; +import io.micronaut.data.model.query.builder.QueryParameterBinding; + +/** + * The default parameter expression implementation. + * + * @param The parameter type + * @author Denis Stepanov + * @since 4.9.0 + */ +@Internal +final class DefaultParameterExpression extends ParameterExpressionImpl { + + private final @NonNull Class paramClass; + private final @Nullable Object value; + + public DefaultParameterExpression(@NonNull Class paramClass, @Nullable String name, @Nullable Object value) { + super(paramClass, name); + this.paramClass = paramClass; + this.value = value; + } + + @Override + public QueryParameterBinding bind(BindingContext bindingContext) { + String name = bindingContext.getName() == null ? String.valueOf(bindingContext.getIndex()) : bindingContext.getName(); + PersistentPropertyPath outgoingQueryParameterProperty = bindingContext.getOutgoingQueryParameterProperty(); + if (outgoingQueryParameterProperty == null) { + return new SimpleParameterBinding(name, DataType.forType(paramClass), bindingContext.isExpandable(), value); + } + return new PropertyPathParameterBinding(name, outgoingQueryParameterProperty, bindingContext.isExpandable(), value); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentPropertyPath.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultPersistentPropertyPath.java similarity index 81% rename from data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentPropertyPath.java rename to data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultPersistentPropertyPath.java index ffe2108e33a..60677e2a727 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentPropertyPath.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/DefaultPersistentPropertyPath.java @@ -46,15 +46,17 @@ * @since 3.2 */ @Internal -public abstract class AbstractPersistentPropertyPath implements PersistentPropertyPath { +public class DefaultPersistentPropertyPath implements PersistentPropertyPath { - private final PersistentProperty persistentProperty; - private final List path; + private final io.micronaut.data.model.PersistentPropertyPath propertyPath; private final CriteriaBuilder criteriaBuilder; - public AbstractPersistentPropertyPath(PersistentProperty persistentProperty, List path, CriteriaBuilder criteriaBuilder) { - this.persistentProperty = persistentProperty; - this.path = path; + public DefaultPersistentPropertyPath(PersistentProperty persistentProperty, List associations, CriteriaBuilder criteriaBuilder) { + this(new io.micronaut.data.model.PersistentPropertyPath(associations, persistentProperty), criteriaBuilder); + } + + public DefaultPersistentPropertyPath(io.micronaut.data.model.PersistentPropertyPath propertyPath, CriteriaBuilder criteriaBuilder) { + this.propertyPath = propertyPath; this.criteriaBuilder = criteriaBuilder; } @@ -81,12 +83,17 @@ public Predicate in(Expression> values) { @Override public PersistentProperty getProperty() { - return persistentProperty; + return propertyPath.getProperty(); } @Override public List getAssociations() { - return path; + return propertyPath.getAssociations(); + } + + @Override + public io.micronaut.data.model.PersistentPropertyPath getPropertyPath() { + return propertyPath; } @Override @@ -136,9 +143,6 @@ public void visitExpression(ExpressionVisitor expressionVisitor) { @Override public String toString() { - return "PersistentPropertyPath{" + - "persistentProperty=" + persistentProperty + - ", path=" + path + - '}'; + return "PersistentPropertyPath{" + propertyPath + '}'; } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/ParameterExpressionImpl.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/ParameterExpressionImpl.java index d8e2a2ba1b2..e39117b7458 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/ParameterExpressionImpl.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/ParameterExpressionImpl.java @@ -123,8 +123,8 @@ public String getAlias() { @Override public String toString() { return "ParameterExpressionImpl{" + - "type=" + type + - ", name='" + name + '\'' + - '}'; + "type=" + type + + ", name='" + name + '\'' + + '}'; } } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java index a17adaa35a0..03234df22e9 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PredicateVisitor.java @@ -19,6 +19,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExpressionBinaryPredicate; +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.PersistentPropertyBetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyBinaryPredicate; @@ -90,4 +91,11 @@ public interface PredicateVisitor { */ void visit(ExpressionBinaryPredicate expressionBinaryPredicate); + /** + * Visit {@link LikePredicate}. + * + * @param likePredicate The like predicate + */ + void visit(LikePredicate likePredicate); + } diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PropertyPathParameterBinding.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PropertyPathParameterBinding.java new file mode 100644 index 00000000000..d3afec99537 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/PropertyPathParameterBinding.java @@ -0,0 +1,65 @@ +/* + * 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.core.annotation.Nullable; +import io.micronaut.data.model.DataType; +import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.PersistentPropertyPath; +import io.micronaut.data.model.query.builder.QueryParameterBinding; + +/** + * The property path implementation of {@link QueryParameterBinding}. + * + * @param getName The name + * @param propertyPath The property path + * @param isExpandable is expandable + * @param value The value + * @author Denis Stepanov + * @since 4.9.0 + */ +@Internal +record PropertyPathParameterBinding(String getName, + PersistentPropertyPath propertyPath, + boolean isExpandable, + @Nullable Object value) implements QueryParameterBinding { + + @Override + public String getKey() { + return getName; + } + + @Override + public DataType getDataType() { + return propertyPath.getProperty().getDataType(); + } + + @Override + public JsonDataType getJsonDataType() { + return propertyPath.getProperty().getJsonDataType(); + } + + @Override + public String[] getPropertyPath() { + return propertyPath.getArrayPath(); + } + + @Override + public Object getValue() { + return value; + } +} 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 923fb82f3c6..27d881b55c0 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 @@ -38,44 +38,47 @@ @Internal public interface QueryResultPersistentEntityCriteriaQuery { - default QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder) { - if (queryBuilder.getClass().getSimpleName().equals("CosmosSqlQueryBuilder")) { + static QueryBuilder2 findQueryBuilder2(QueryBuilder queryBuilder) { + Class queryBuilderClass = queryBuilder.getClass(); + if (queryBuilderClass.getSimpleName().equals("CosmosSqlQueryBuilder")) { // Use new implementation try { - return buildQuery(annotationMetadata, (QueryBuilder2) getClass() + return (QueryBuilder2) queryBuilderClass .getClassLoader().loadClass("io.micronaut.data.document.model.query.builder.CosmosSqlQueryBuilder2") .getDeclaredConstructor(AnnotationMetadata.class) - .newInstance(((SqlQueryBuilder) queryBuilder).getAnnotationMetadata())); + .newInstance(((SqlQueryBuilder) queryBuilder).getAnnotationMetadata()); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } - if (queryBuilder.getClass().getSimpleName().equals("MongoQueryBuilder")) { + if (queryBuilderClass.getSimpleName().equals("MongoQueryBuilder")) { // Use new implementation try { - return buildQuery(annotationMetadata, (QueryBuilder2) getClass() + return (QueryBuilder2) queryBuilderClass .getClassLoader().loadClass("io.micronaut.data.document.model.query.builder.MongoQueryBuilder2") .getDeclaredConstructor() - .newInstance()); + .newInstance(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } - if (queryBuilder.getClass() == SqlQueryBuilder.class) { + if (queryBuilderClass == SqlQueryBuilder.class) { // Use new implementation - return buildQuery(annotationMetadata, newSqlQueryBuilder((SqlQueryBuilder) queryBuilder)); + return newSqlQueryBuilder((SqlQueryBuilder) queryBuilder); } - if (queryBuilder.getClass() == JpaQueryBuilder.class) { + if (queryBuilderClass == JpaQueryBuilder.class) { // Use new implementation - return buildQuery(annotationMetadata, new JpaQueryBuilder2()); + return new JpaQueryBuilder2(); } - return queryBuilder.buildQuery(annotationMetadata, getQueryModel()); + return null; } + QueryResult buildQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder); + QueryModel getQueryModel(); default QueryResult buildCountQuery(AnnotationMetadata annotationMetadata, QueryBuilder queryBuilder) { @@ -128,7 +131,7 @@ default QueryResult buildCountQuery(AnnotationMetadata annotationMetadata, Query throw new UnsupportedOperationException(); } - private QueryBuilder2 newSqlQueryBuilder(SqlQueryBuilder sqlQueryBuilder) { + private static QueryBuilder2 newSqlQueryBuilder(SqlQueryBuilder sqlQueryBuilder) { // Use new implementation AnnotationMetadata builderAnnotationMetadata = sqlQueryBuilder.getAnnotationMetadata(); if (builderAnnotationMetadata == null) { diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/SimpleParameterBinding.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/SimpleParameterBinding.java new file mode 100644 index 00000000000..dfb2847860b --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/SimpleParameterBinding.java @@ -0,0 +1,64 @@ +/* + * 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.core.annotation.Nullable; +import io.micronaut.data.model.DataType; +import io.micronaut.data.model.JsonDataType; +import io.micronaut.data.model.query.builder.QueryParameterBinding; + +/** + * The simple {@link QueryParameterBinding}. + * + * @param getName The name + * @param dataType The data type + * @param isExpandable is expandable + * @param value The value + * @author Denis Stepanov + * @since 4.9.0 + */ +@Internal +record SimpleParameterBinding(String getName, + DataType dataType, + boolean isExpandable, + @Nullable Object value) implements QueryParameterBinding { + + @Override + public String getKey() { + return getName; + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public JsonDataType getJsonDataType() { + return JsonDataType.DEFAULT; + } + + @Override + public String[] getPropertyPath() { + return null; + } + + @Override + public Object getValue() { + return value; + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/LikePredicate.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/LikePredicate.java new file mode 100644 index 00000000000..03d7cc0b529 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/LikePredicate.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.model.jpa.criteria.impl.predicate; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.model.jpa.criteria.impl.PredicateVisitor; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + +/** + * The property binary operation predicate implementation. + * + * @author Denis Stepanov + * @since 3.2 + */ +@Internal +public final class LikePredicate extends AbstractPredicate { + + private final Expression expression; + private final Expression pattern; + @Nullable + private final Expression escapeChar; + private final boolean negated; + private final boolean caseInsensitive; + + public LikePredicate(Expression expression, Expression pattern) { + this(expression, pattern, null, false, false); + } + + public LikePredicate(Expression expression, Expression pattern, Expression escapeChar, boolean negated) { + this(expression, pattern, escapeChar, negated, false); + } + + public LikePredicate(Expression expression, Expression pattern, Expression escapeChar, boolean negated, boolean caseInsensitive) { + this.expression = expression; + this.pattern = pattern; + this.escapeChar = escapeChar; + this.negated = negated; + this.caseInsensitive = caseInsensitive; + } + + @Override + public Predicate not() { + return new LikePredicate(expression, pattern, escapeChar, !negated); + } + + public Expression getExpression() { + return expression; + } + + public Expression getPattern() { + return pattern; + } + + @Nullable + public Expression getEscapeChar() { + return escapeChar; + } + + @Override + public boolean isNegated() { + return negated; + } + + public boolean isCaseInsensitive() { + return caseInsensitive; + } + + @Override + public void visitPredicate(PredicateVisitor predicateVisitor) { + predicateVisitor.visit(this); + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/PredicateBinaryOp.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/PredicateBinaryOp.java index 914c33a1624..014b141d5f8 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/PredicateBinaryOp.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/predicate/PredicateBinaryOp.java @@ -34,9 +34,6 @@ public enum PredicateBinaryOp { GREATER_THAN_OR_EQUALS, LESS_THAN, LESS_THAN_OR_EQUALS, - RLIKE, - ILIKE, - LIKE, REGEX, CONTAINS, CONTAINS_IGNORE_CASE, diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/query/QueryModelPredicateVisitor.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/query/QueryModelPredicateVisitor.java index 7190efb58cc..7216f3e9082 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/query/QueryModelPredicateVisitor.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/query/QueryModelPredicateVisitor.java @@ -29,6 +29,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExpressionBinaryPredicate; +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.PersistentPropertyBetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyBinaryPredicate; @@ -204,12 +205,6 @@ private QueryModel.Criterion getPropertyToValueRestriction(PredicateBinaryOp op, return Restrictions.endsWith(leftProperty, rightProperty); case STARTS_WITH: return Restrictions.startsWith(leftProperty, rightProperty); - case ILIKE: - return Restrictions.ilike(leftProperty, rightProperty); - case RLIKE: - return Restrictions.rlike(leftProperty, rightProperty); - case LIKE: - return Restrictions.like(leftProperty, rightProperty); case REGEX: return Restrictions.regex(leftProperty, rightProperty); case EQUALS_IGNORE_CASE: @@ -313,6 +308,10 @@ public void visit(PersistentPropertyInPredicate inValues) { } } + @Override + public void visit(LikePredicate likePredicate) { + } + private Object asValue(Object value) { if (value instanceof LiteralExpression literalExpression) { return literalExpression.getValue(); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java index ec81fae6004..56ba2208945 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/util/Joiner.java @@ -36,6 +36,7 @@ import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.ExpressionBinaryPredicate; +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.PersistentPropertyBetweenPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyBinaryPredicate; @@ -262,6 +263,11 @@ public void visit(ExpressionBinaryPredicate expressionBinaryPredicate) { visitPredicateExpression(expressionBinaryPredicate.getRight()); } + @Override + public void visit(LikePredicate likePredicate) { + visitPredicateExpression(likePredicate.getExpression()); + } + /** * The data structure representing a join. */ 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 1c5da824ce2..76ae8b5f66d 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 @@ -18,7 +18,6 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.ArgumentUtils; @@ -36,6 +35,7 @@ import io.micronaut.data.annotation.repeatable.WhereSpecifications; import io.micronaut.data.model.Association; import io.micronaut.data.model.DataType; +import io.micronaut.data.model.Embedded; import io.micronaut.data.model.JsonDataType; import io.micronaut.data.model.PersistentAssociationPath; import io.micronaut.data.model.PersistentEntity; @@ -47,16 +47,20 @@ 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.impl.DefaultPersistentPropertyPath; +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; 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.SelectionVisitor; +import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; 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.LikePredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate; +import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyBinaryPredicate; import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyInPredicate; -import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression; +import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp; import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection; import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection; import io.micronaut.data.model.naming.NamingStrategy; @@ -446,7 +450,7 @@ protected NamingStrategy getNamingStrategy(PersistentEntity entity) { * Gets the mapped name from the association using {@link NamingStrategy}. * * @param namingStrategy the naming strategy being used - * @param association the associatioon + * @param association the association * @return the mapped name for the association */ @NonNull @@ -467,6 +471,18 @@ protected String getMappedName(@NonNull NamingStrategy namingStrategy, @NonNull return namingStrategy.mappedName(associations, property); } + /** + * Gets the mapped name from for the list of associations and property using {@link NamingStrategy}. + * + * @param namingStrategy the naming strategy + * @param propertyPath the property path + * @return the mappen name for the list of associations and property using given naming strategy + */ + @NonNull + protected String getMappedName(@NonNull NamingStrategy namingStrategy, @NonNull PersistentPropertyPath propertyPath) { + return namingStrategy.mappedName(propertyPath.getAssociations(), propertyPath.getProperty()); + } + /** * Builds where clause. * @@ -641,7 +657,7 @@ protected void appendOrder(AnnotationMetadata annotationMetadata, SelectQueryDef } /** - * Adds "forUpdate" pisimmistic locking. + * Adds "forUpdate" pessimistic locking. * * @param queryPosition The query position * @param definition The definition @@ -1199,8 +1215,7 @@ protected final void appendExpression(AnnotationMetadata annotationMetadata, Expression expression, boolean isProjection) { if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - QueryPropertyPath propertyPath = queryState.findProperty(persistentPropertyPath.getPropertyPath()); - appendPropertyRef(annotationMetadata, query, queryState, propertyPath, isProjection); + appendPropertyRef(annotationMetadata, query, queryState, persistentPropertyPath.getPropertyPath(), isProjection); } else if (expression instanceof ParameterExpression parameterExpression) { if (expression instanceof BindingParameter bindingParameter) { queryState.pushParameter(bindingParameter, newBindingContext(null)); @@ -1217,10 +1232,14 @@ protected final void appendExpression(AnnotationMetadata annotationMetadata, protected final void appendPropertyRef(AnnotationMetadata annotationMetadata, StringBuilder query, QueryState queryState, - QueryPropertyPath propertyPath, + PersistentPropertyPath pp, boolean isProjection) { + if (computePropertyPaths() && pp.getProperty() instanceof Embedded) { + throw new IllegalArgumentException("Embedded are not allowed as an expression!"); + } + QueryPropertyPath propertyPath = queryState.findProperty(pp); String tableAlias = propertyPath.getTableAlias(); - String readTransformer = getDataTransformerReadValue(tableAlias, propertyPath.getProperty()).orElse(null); + String readTransformer = isProjection ? getDataTransformerReadValue(tableAlias, propertyPath.getProperty()).orElse(null) : null; if (readTransformer != null) { query.append(readTransformer); return; @@ -1616,7 +1635,7 @@ private QueryPropertyPath findPropertyInternal(PersistentPropertyPath propertyPa joinAssociation = association; continue; } - if (association != joinAssociation.getAssociatedEntity().getIdentity()) { + if (!PersistentEntityUtils.isAccessibleWithoutJoin(joinAssociation, association)) { lastJoinAlias = getRequiredJoinPathAlias(joinPathJoiner.toString()); // Continue to look for a joined property joinAssociation = association; @@ -1645,7 +1664,7 @@ private QueryPropertyPath findPropertyInternal(PersistentPropertyPath propertyPa @NonNull private String getRequiredJoinPathAlias(String path) { if (!isAllowJoins()) { - throw new IllegalArgumentException("Joins cannot be used in a DELETE or UPDATE operation"); + throw new IllegalArgumentException("Joins cannot be used in a DELETE or UPDATE operation and path: " + path); } return getJoinAlias(path); } @@ -1757,7 +1776,7 @@ protected enum QueryPosition { /** * The predicate visitor to construct the query. */ - protected class SqlPredicateVisitor implements AdvancedPredicateVisitor { + protected class SqlPredicateVisitor implements AdvancedPredicateVisitor { protected final PersistentEntity persistentEntity; protected final String tableAlias; @@ -1774,8 +1793,8 @@ protected SqlPredicateVisitor(QueryState queryState, AnnotationMetadata annotati } @Override - public QueryPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - return queryState.findProperty(persistentPropertyPath.getPropertyPath()); + public PersistentPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { + return persistentPropertyPath.getPropertyPath(); } private void visitPredicate(IExpression expression) { @@ -1795,9 +1814,14 @@ public void visit(ConjunctionPredicate conjunction) { if (conjunction.getPredicates().isEmpty()) { return; } - query.append(OPEN_BRACKET); + boolean requiresBracket = query.charAt(query.length() - 1) != '('; + if (requiresBracket) { + query.append(OPEN_BRACKET); + } visitConjunctionPredicates(conjunction.getPredicates()); - query.append(CLOSE_BRACKET); + if (requiresBracket) { + query.append(CLOSE_BRACKET); + } } private void visitConjunctionPredicates(Collection> predicates) { @@ -1859,86 +1883,132 @@ public void visit(NegatedPredicate negate) { } @Override - public void visitEquals(QueryPropertyPath leftProperty, Expression expression, boolean ignoreCase) { - if (ignoreCase) { - appendCaseInsensitiveCriterion(leftProperty, expression, " = "); + public void visit(LikePredicate likePredicate) { + boolean supportsILike = getDialect() == Dialect.POSTGRES; + boolean isCaseInsensitive = !supportsILike && likePredicate.isCaseInsensitive(); + if (isCaseInsensitive) { + query.append("LOWER("); + } + appendExpression(likePredicate.getExpression()); + if (isCaseInsensitive) { + query.append(")"); + } + if (likePredicate.isNegated()) { + query.append(" NOT"); + } + if (likePredicate.isCaseInsensitive() && supportsILike) { + query.append(" ILIKE "); } else { - PersistentProperty property = leftProperty.getProperty(); - PersistentEntity owner = property.getOwner(); - if (owner.equals(persistentEntity) && leftProperty.getAssociations().isEmpty() && (owner.hasIdentity() && owner.getIdentity() == property)) { - visitIdEquals(expression); - } else if (leftProperty.getAssociations().isEmpty() && owner.getVersion() == property) { - appendPredicateOfVersionEquals(expression); + query.append(" LIKE "); + } + Expression pattern = likePredicate.getPattern(); + if (isCaseInsensitive) { + if (pattern instanceof LiteralExpression literalExpression) { + query.append(literalExpression.getValue().toUpperCase()); } else { - appendCriteriaForOperator(leftProperty, expression, " = "); + query.append("LOWER("); + appendExpression(pattern); + query.append(")"); } - } - } - - @Override - public void visitNotEquals(QueryPropertyPath leftProperty, Expression expression, boolean ignoreCase) { - if (ignoreCase) { - appendCaseInsensitiveCriterion(leftProperty, expression, " != "); } else { - appendCriteriaForOperator(leftProperty, expression, " != "); + appendExpression(pattern); } - } - @Override - public void visitGreaterThan(QueryPropertyPath leftProperty, Expression expression) { - appendCriteriaForOperator(leftProperty, expression, " > "); + Expression escapeChar = likePredicate.getEscapeChar(); + if (escapeChar != null) { + query.append(" ESCAPE "); + appendExpression(escapeChar); + } } @Override - public void visitGreaterThanOrEquals(QueryPropertyPath leftProperty, Expression expression) { - appendCriteriaForOperator(leftProperty, expression, " >= "); + public void visitEquals(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) { + PersistentProperty property = leftProperty.getProperty(); + if (computePropertyPaths() && property instanceof Association) { + List predicates = new ArrayList<>(); + PersistentEntityUtils.traverse(leftProperty, pp -> + predicates.add(new PersistentPropertyBinaryPredicate<>( + new DefaultPersistentPropertyPath<>(pp, null), + expression, + ignoreCase ? PredicateBinaryOp.EQUALS_IGNORE_CASE : PredicateBinaryOp.EQUALS + )) + ); + if (predicates.size() == 1) { + predicates.iterator().next().visitPredicate(this); + } else { + visit(new ConjunctionPredicate(predicates)); + } + } else { + if (ignoreCase) { + appendCaseInsensitiveOp(leftProperty, expression, " = "); + } else { + appendSingle(" = ", expression, leftProperty); + } + } } @Override - public void visitLessThan(QueryPropertyPath leftProperty, Expression expression) { - appendCriteriaForOperator(leftProperty, expression, " < "); + public void visitNotEquals(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) { + PersistentProperty property = leftProperty.getProperty(); + if (computePropertyPaths() && property instanceof Association) { + List predicates = new ArrayList<>(); + PersistentEntityUtils.traverse(leftProperty, pp -> + predicates.add(new PersistentPropertyBinaryPredicate<>( + new DefaultPersistentPropertyPath<>(pp, null), + expression, + ignoreCase ? PredicateBinaryOp.NOT_EQUALS_IGNORE_CASE : PredicateBinaryOp.NOT_EQUALS + )) + ); + if (predicates.size() == 1) { + predicates.iterator().next().visitPredicate(this); + } else { + visit(new ConjunctionPredicate(predicates)); + } + } else { + if (ignoreCase) { + appendCaseInsensitiveOp(leftProperty, expression, " != "); + } else { + appendSingle(" != ", expression, leftProperty); + } + } } @Override - public void visitLessThanOrEquals(QueryPropertyPath leftProperty, Expression expression) { - appendCriteriaForOperator(leftProperty, expression, " <= "); + public void visitGreaterThan(PersistentPropertyPath leftProperty, Expression expression) { + appendSingle(" > ", expression, leftProperty); } @Override - public void visitLike(QueryPropertyPath leftProperty, Expression expression) { - appendCriteriaForOperator(leftProperty, expression, " LIKE "); + public void visitGreaterThanOrEquals(PersistentPropertyPath leftProperty, Expression expression) { + appendSingle(" >= ", expression, leftProperty); } @Override - public void visitILike(QueryPropertyPath leftProperty, Expression expression) { - if (getDialect() == Dialect.POSTGRES) { - appendCriteriaForOperator(leftProperty, expression, " ILIKE "); - } else { - appendCaseInsensitiveCriterion(leftProperty, expression, " LIKE "); - } + public void visitLessThan(PersistentPropertyPath leftProperty, Expression expression) { + appendSingle(" < ", expression, leftProperty); } @Override - public void visitRLike(QueryPropertyPath leftProperty, Expression expression) { - throw new IllegalStateException("Not supported"); + public void visitLessThanOrEquals(PersistentPropertyPath leftProperty, Expression expression) { + appendSingle(" <= ", expression, leftProperty); } @Override - public void visitStartsWith(QueryPropertyPath leftProperty, Expression expression, boolean ignoreCase) { + public void visitStartsWith(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) { appendLikeConcatComparison(leftProperty, expression, ignoreCase, "?", "'%'"); } @Override - public void visitContains(QueryPropertyPath leftProperty, Expression expression, boolean ignoreCase) { + public void visitContains(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) { appendLikeConcatComparison(leftProperty, expression, ignoreCase, "'%'", "?", "'%'"); } @Override - public void visitEndsWith(QueryPropertyPath leftProperty, Expression expression, boolean ignoreCase) { + public void visitEndsWith(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) { appendLikeConcatComparison(leftProperty, expression, ignoreCase, "'%'", "?"); } - private void appendLikeConcatComparison(QueryPropertyPath propertyPath, Expression expression, boolean ignoreCase, String... parts) { + private void appendLikeConcatComparison(PersistentPropertyPath propertyPath, Expression expression, boolean ignoreCase, String... parts) { boolean isPostgres = getDialect() == Dialect.POSTGRES; if (ignoreCase && !isPostgres) { query.append("LOWER("); @@ -1957,11 +2027,11 @@ private void appendLikeConcatComparison(QueryPropertyPath propertyPath, Expressi if (ignoreCase && !isPostgres) { return (Runnable) () -> { query.append("LOWER("); - appendExpression(propertyPath, expression); + appendExpression(expression, propertyPath); query.append(")"); }; } else { - return (Runnable) () -> appendExpression(propertyPath, expression); + return (Runnable) () -> appendExpression(expression, propertyPath); } } return (Runnable) () -> query.append(p); @@ -1971,165 +2041,112 @@ private void appendLikeConcatComparison(QueryPropertyPath propertyPath, Expressi @Override public void visitIdEquals(Expression expression) { if (persistentEntity.hasCompositeIdentity()) { - for (PersistentProperty prop : persistentEntity.getCompositeIdentity()) { - appendCriteriaForOperator( - null, - asQueryPropertyPath(tableAlias, prop), - expression, - " = " - ); - query.append(LOGICAL_AND); - } - query.setLength(query.length() - LOGICAL_AND.length()); + new ConjunctionPredicate( + Arrays.stream(persistentEntity.getCompositeIdentity()) + .map(prop -> { + PersistentPropertyPath propertyPath = asPersistentPropertyPath(prop); + return new PersistentPropertyBinaryPredicate<>( + new DefaultPersistentPropertyPath<>(propertyPath, null), + expression, + PredicateBinaryOp.EQUALS + ); + } + ) + .toList() + ).visitPredicate(this); } else if (persistentEntity.hasIdentity()) { - appendCriteriaForOperator( - asQueryPropertyPath(tableAlias, persistentEntity.getIdentity()), + new PersistentPropertyBinaryPredicate<>( + new DefaultPersistentPropertyPath<>(new PersistentPropertyPath(persistentEntity.getIdentity()), null), expression, - " = " - ); + PredicateBinaryOp.EQUALS + ).visitPredicate(this); } else { throw new IllegalStateException("No ID found for entity: " + persistentEntity.getName()); } } - private void appendPredicateOfVersionEquals(Expression expression) { - PersistentProperty prop = persistentEntity.getVersion(); - if (prop == null) { - throw new IllegalStateException("No Version found for entity: " + persistentEntity.getName()); - } - appendCriteriaForOperator( - asQueryPropertyPath(tableAlias, prop), - expression, - " = " - ); + protected final void appendPropertyRef(PersistentPropertyPath propertyPath) { + AbstractSqlLikeQueryBuilder2.this.appendPropertyRef(annotationMetadata, query, queryState, propertyPath, false); } - protected final void appendPropertyRef(QueryPropertyPath propertyPath) { - AbstractSqlLikeQueryBuilder2.this.appendPropertyRef(annotationMetadata, query, queryState, propertyPath, false); + private void appendSingle(String operator, Expression expression, @Nullable PersistentPropertyPath propertyPath) { + appendPropertyRef(propertyPath); + query.append(operator); + appendExpression(expression, propertyPath); } - private void appendCriteriaForOperator(QueryPropertyPath propertyPath, - Expression value, - String operator) { - appendCriteriaForOperator(propertyPath.propertyPath, propertyPath, value, operator); + private void appendExpression(Expression expression) { + appendExpression(expression, null); } - @NextMajorVersion("Remove the trim to have the operators look consistent") - private void appendCriteriaForOperator(PersistentPropertyPath parameterPropertyPath, - QueryPropertyPath propertyPath, - Expression expression, - String operator) { + protected final void appendExpression(Expression expression, PersistentPropertyPath propertyPath) { if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - appendPropertyRef(propertyPath); - query.append(operator.trim()); - appendPropertyRef(getRequiredProperty(persistentPropertyPath)); + appendPropertyRef(persistentPropertyPath.getPropertyPath()); } else if (expression instanceof BindingParameter bindingParameter) { - boolean computePropertyPaths = computePropertyPaths(); - boolean jsonEntity = isJsonEntity(annotationMetadata, persistentEntity); - if (!computePropertyPaths || jsonEntity) { - appendPropertyRef(propertyPath); - query.append(operator); - queryState.pushParameter( - bindingParameter, - newBindingContext(parameterPropertyPath, propertyPath.propertyPath) - ); - return; - } - - String currentAlias = propertyPath.getTableAlias(); - NamingStrategy namingStrategy = propertyPath.getNamingStrategy(); - boolean shouldEscape = propertyPath.shouldEscape(); - boolean[] needsTrimming = {false}; - PersistentEntityUtils.traversePersistentProperties(propertyPath.getPropertyPath(), (associations, property) -> { - if (currentAlias != null) { - query.append(currentAlias).append(DOT); - } - - String columnName = getMappedName(namingStrategy, associations, property); - if (shouldEscape) { - columnName = quote(columnName); - } - query.append(columnName); - - query.append(operator); - String writeTransformer = getDataTransformerWriteValue(currentAlias, property).orElse(null); - Runnable pushParameter = () -> { - queryState.pushParameter( - bindingParameter, - newBindingContext(parameterPropertyPath, PersistentPropertyPath.of(associations, property)) - ); - }; - if (writeTransformer != null) { - appendTransformed(query, writeTransformer, pushParameter); - } else { - pushParameter.run(); - } - query.append(LOGICAL_AND); - needsTrimming[0] = true; - }); + appendBindingParameter(bindingParameter, propertyPath); + } else { + query.append(asLiteral(expression)); + } + } - if (needsTrimming[0]) { - query.setLength(query.length() - LOGICAL_AND.length()); - } + private void appendBindingParameter(BindingParameter bindingParameter, + @Nullable PersistentPropertyPath entityPropertyPath) { + Runnable pushParameter = () -> { + queryState.pushParameter( + bindingParameter, + newBindingContext(null, entityPropertyPath) + ); + }; + if (entityPropertyPath == null) { + pushParameter.run(); } else { - appendPropertyRef(propertyPath); - query.append(operator).append(asLiteral(expression)); + 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 appendCaseInsensitiveCriterion(QueryPropertyPath leftProperty, Expression expression, String operator) { + private void appendCaseInsensitiveOp(PersistentPropertyPath leftProperty, Expression expression, String operator) { query.append("LOWER("); appendPropertyRef(leftProperty); query.append(")") .append(operator) .append("LOWER("); - appendExpression(leftProperty, expression); + appendExpression(expression, leftProperty); query.append(")"); } - protected final void appendExpression(QueryPropertyPath leftProperty, Expression expression) { - if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) { - appendPropertyRef(getRequiredProperty(persistentPropertyPath)); - } else if (expression instanceof ParameterExpression parameterExpression) { - if (expression instanceof BindingParameter bindingParameter) { - queryState.pushParameter(bindingParameter, newBindingContext(leftProperty.propertyPath)); - } else { - throw new IllegalArgumentException("Unknown parameter: " + parameterExpression); - } - } else if (expression instanceof LiteralExpression literalExpression) { - query.append(asLiteral(literalExpression.getValue())); - } else { - throw new IllegalArgumentException("Unsupported expression type: " + expression.getClass()); - } - } - @Override - public void visitIsFalse(QueryPropertyPath propertyPath) { + public void visitIsFalse(PersistentPropertyPath propertyPath) { appendUnaryCondition(" = FALSE", propertyPath); } @Override - public void visitIsNotNull(QueryPropertyPath propertyPath) { + public void visitIsNotNull(PersistentPropertyPath propertyPath) { appendUnaryCondition(" IS NOT NULL", propertyPath); } @Override - public void visitIsNull(QueryPropertyPath propertyPath) { + public void visitIsNull(PersistentPropertyPath propertyPath) { appendUnaryCondition(" IS NULL", propertyPath); } @Override - public void visitIsTrue(QueryPropertyPath propertyPath) { + public void visitIsTrue(PersistentPropertyPath propertyPath) { appendUnaryCondition(" = TRUE", propertyPath); } @Override - public void visitIsEmpty(QueryPropertyPath propertyPath) { + public void visitIsEmpty(PersistentPropertyPath propertyPath) { appendEmptyExpression(" IS NULL" + " " + OR + StringUtils.SPACE, " = ''", " IS EMPTY", propertyPath); } @Override - public void visitIsNotEmpty(QueryPropertyPath propertyPath) { + public void visitIsNotEmpty(PersistentPropertyPath propertyPath) { if (getDialect() == Dialect.ORACLE) { // Oracle treats blank and null the same if (propertyPath.getProperty().isAssignable(CharSequence.class)) { @@ -2147,7 +2164,7 @@ public void visitIsNotEmpty(QueryPropertyPath propertyPath) { private void appendEmptyExpression(String charSequencePrefix, String charSequenceSuffix, String listSuffix, - QueryPropertyPath propertyPath) { + PersistentPropertyPath propertyPath) { if (propertyPath.getProperty().isAssignable(CharSequence.class)) { appendPropertyRef(propertyPath); query.append(charSequencePrefix); @@ -2159,21 +2176,21 @@ private void appendEmptyExpression(String charSequencePrefix, } } - private void appendUnaryCondition(String sqlOp, QueryPropertyPath propertyPath) { + private void appendUnaryCondition(String sqlOp, PersistentPropertyPath propertyPath) { appendPropertyRef(propertyPath); query.append(sqlOp); } @Override - public void visitInBetween(QueryPropertyPath property, Expression from, Expression to) { + public void visitInBetween(PersistentPropertyPath propertyPath, Expression from, Expression to) { query.append(OPEN_BRACKET); - appendPropertyRef(property); + appendPropertyRef(propertyPath); query.append(" >= "); - appendExpression(property, from); + appendExpression(from, propertyPath); query.append(LOGICAL_AND); - appendPropertyRef(property); + appendPropertyRef(propertyPath); query.append(" <= "); - appendExpression(property, to); + appendExpression(to, propertyPath); query.append(CLOSE_BRACKET); } @@ -2183,7 +2200,7 @@ public void visit(PersistentPropertyInPredicate predicate) { } @Override - public void visitIn(QueryPropertyPath propertyPath, Collection values, boolean negated) { + public void visitIn(PersistentPropertyPath propertyPath, Collection values, boolean negated) { if (values.isEmpty()) { return; } @@ -2194,7 +2211,7 @@ public void visitIn(QueryPropertyPath propertyPath, Collection values, boolea while (iterator.hasNext()) { Object value = iterator.next(); if (value instanceof ParameterExpression) { - BindingParameter.BindingContext bindingContext = newBindingContext(propertyPath.propertyPath); + BindingParameter.BindingContext bindingContext = newBindingContext(propertyPath); if (hasOneParameter) { bindingContext = bindingContext.expandable(); } @@ -2307,7 +2324,8 @@ public void visit(LiteralExpression literalExpression) { public void visit(UnaryExpression unaryExpression) { Expression expression = unaryExpression.getExpression(); switch (unaryExpression.getType()) { - case SUM, AVG, MAX, MIN, UPPER, LOWER -> appendFunction(unaryExpression.getType().name(), expression); + case SUM, AVG, MAX, MIN, UPPER, LOWER -> + appendFunction(unaryExpression.getType().name(), expression); case COUNT -> { if (expression instanceof PersistentEntityRoot) { appendRowCount(tableAlias); 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 530932d0532..598866e4ced 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 @@ -109,9 +109,6 @@ default void appendPredicateOfPropertyAndExpression(PredicateBinaryOp op, P left case GREATER_THAN_OR_EQUALS -> visitGreaterThanOrEquals(leftProperty, expression); case LESS_THAN -> visitLessThan(leftProperty, expression); case LESS_THAN_OR_EQUALS -> visitLessThanOrEquals(leftProperty, expression); - case LIKE -> visitLike(leftProperty, expression); - case RLIKE -> visitRLike(leftProperty, expression); - case ILIKE -> visitILike(leftProperty, expression); case STARTS_WITH -> visitStartsWith(leftProperty, expression, false); case STARTS_WITH_IGNORE_CASE -> visitStartsWith(leftProperty, expression, true); case REGEX -> visitRegexp(leftProperty, expression); @@ -138,12 +135,6 @@ default void visitArrayContains(P leftProperty, Expression expression) { void visitStartsWith(P leftProperty, Expression expression, boolean ignoreCase); - void visitLike(P leftProperty, Expression expression); - - void visitRLike(P leftProperty, Expression expression); - - void visitILike(P leftProperty, Expression expression); - void visitEquals(P leftProperty, Expression expression, boolean ignoreCase); void visitNotEquals(P leftProperty, Expression expression, boolean ignoreCase); diff --git a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy index aed6b0bdce3..a69cedb38af 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy @@ -163,6 +163,28 @@ class PageSpec extends Specification { deserializedSort == sort } + void "test empty page map"() { + when:"Map empty page" + def page = Page.empty() + def mappedPage = page.map { it } + then:"No exception thrown, page is mapped" + page.size == -1 + !page.hasTotalSize() + mappedPage.size == -1 + !mappedPage.hasTotalSize() + + when:"Map empty cursored page" + def cursoredPage = CursoredPage.empty() + def mappedCursoredPage = cursoredPage.map { it } + then:"No exception thrown, cursored page is mapped" + cursoredPage.size == -1 + !cursoredPage.cursors + !cursoredPage.hasTotalSize() + mappedCursoredPage.size == -1 + !mappedCursoredPage.cursors + !mappedCursoredPage.hasTotalSize() + } + @EqualsAndHashCode @ToString @Serdeable 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 f2e7099aa27..d9de88bf3fb 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 @@ -17,7 +17,9 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.PersistentProperty; +import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.inject.ast.ParameterElement; import jakarta.persistence.criteria.ParameterExpression; @@ -46,19 +48,23 @@ public interface SourcePersistentEntityCriteriaBuilder extends PersistentEntityC * Create parameter expression from {@link ParameterElement}. * * @param parameterElement The parameter element + * @param propertyPath The property path this parameter is representing * @param The expression type * @return new parameter */ @NonNull - ParameterExpression parameter(@NonNull ParameterElement parameterElement); + ParameterExpression parameter(@NonNull ParameterElement parameterElement, + @Nullable PersistentPropertyPath propertyPath); /** * 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 The expression type * @return new parameter */ @NonNull - ParameterExpression entityPropertyParameter(@NonNull ParameterElement entityParameter); + ParameterExpression entityPropertyParameter(@NonNull ParameterElement entityParameter, + @Nullable PersistentPropertyPath propertyPath); } 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 12a94cf41d6..bd37f6a0913 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 @@ -16,8 +16,10 @@ package io.micronaut.data.processor.model.criteria.impl; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.DataType; 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.criteria.SourcePersistentEntityCriteriaBuilder; @@ -74,12 +76,14 @@ public ParameterExpression expression(PersistentProperty property, Strin } @Override - public ParameterExpression parameter(ParameterElement parameterElement) { - return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), parameterElement, false); + public ParameterExpression parameter(ParameterElement parameterElement, + PersistentPropertyPath propertyPath) { + return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), parameterElement, false, propertyPath); } @Override - public ParameterExpression entityPropertyParameter(ParameterElement entityParameter) { - return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), entityParameter, true); + public ParameterExpression entityPropertyParameter(ParameterElement entityParameter, + @Nullable PersistentPropertyPath propertyPath) { + return new SourceParameterExpressionImpl(dataTypes, methodMatchContext.getParameters(), entityParameter, true, propertyPath); } } 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 b9b59b32393..51adf21d5d6 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 @@ -35,9 +35,7 @@ import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ParameterElement; -import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -60,28 +58,40 @@ public final class SourceParameterExpressionImpl extends ParameterExpressionImpl private final ParameterElement parameterElement; private final boolean isEntityParameter; private boolean isUpdate; + private final PersistentPropertyPath parameterPropertyPath; public SourceParameterExpressionImpl(Map dataTypes, ParameterElement[] parameters, ParameterElement parameterElement, - boolean isEntityParameter) { - super(null, parameterElement == null ? null : parameterElement.getName()); - this.dataTypes = dataTypes; - this.parameters = parameters; - this.parameterElement = parameterElement; - this.isEntityParameter = isEntityParameter; - this.expressionType = null; + boolean isEntityParameter, + PersistentPropertyPath parameterPropertyPath) { + this(parameterElement == null ? null : parameterElement.getName(), + dataTypes, null, parameters, parameterElement, isEntityParameter, false, parameterPropertyPath); } public SourceParameterExpressionImpl(Map dataTypes, String name, - ClassElement expressionType) { + ClassElement expressionType, + PersistentPropertyPath parameterPropertyPath) { + this(name, dataTypes, expressionType, null, null, false, false, parameterPropertyPath); + } + + private SourceParameterExpressionImpl(String name, + Map dataTypes, + ClassElement expressionType, + @Nullable ParameterElement[] parameters, + ParameterElement parameterElement, + boolean isEntityParameter, + boolean isUpdate, + PersistentPropertyPath parameterPropertyPath) { super(null, name); this.dataTypes = dataTypes; - this.parameters = null; - this.parameterElement = null; this.expressionType = expressionType; - this.isEntityParameter = false; + this.parameters = parameters; + this.parameterElement = parameterElement; + this.isEntityParameter = isEntityParameter; + this.isUpdate = isUpdate; + this.parameterPropertyPath = parameterPropertyPath; } @Override @@ -102,6 +112,9 @@ public QueryParameterBinding bind(BindingContext bindingContext) { bindName = bindingContext.getName(); } PersistentPropertyPath incomingMethodParameterProperty = bindingContext.getIncomingMethodParameterProperty(); + if (incomingMethodParameterProperty == null) { + incomingMethodParameterProperty = parameterPropertyPath; + } PersistentPropertyPath outgoingQueryParameterProperty = bindingContext.getOutgoingQueryParameterProperty(); PersistentPropertyPath propertyPath = outgoingQueryParameterProperty == null ? incomingMethodParameterProperty : outgoingQueryParameterProperty; if (propertyPath == null) { @@ -188,21 +201,29 @@ public boolean isExpression() { }; } - if (outgoingQueryParameterProperty == null) { - throw new IllegalStateException("Outgoing query parameter property is required!"); - } boolean autopopulated = propertyPath.getProperty() - .findAnnotation(AutoPopulated.class) - .map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)) - .orElse(false); + .findAnnotation(AutoPopulated.class) + .map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)) + .orElse(false); DataType dataType = getDataType(propertyPath, parameterElement, expressionType); JsonDataType jsonDataType = getJsonDataType(propertyPath, parameterElement, expressionType); String converterClassName = ((SourcePersistentProperty) propertyPath.getProperty()).getConverterClassName(); int index = parameterElement == null || isEntityParameter ? -1 : Arrays.asList(parameters).indexOf(parameterElement); - String[] path = asStringPath(outgoingQueryParameterProperty.getAssociations(), outgoingQueryParameterProperty.getProperty()); - String[] parameterBindingPath = index != -1 ? getBindingPath(incomingMethodParameterProperty, outgoingQueryParameterProperty) : null; 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); + } else { + parameterBindingPath = null; + } + } else { + path = null; + parameterBindingPath = null; + } return new QueryParameterBinding() { @Override @@ -282,10 +303,10 @@ private boolean isExpandable(BindingContext bindingContext, DataType dataType) { private String[] getBindingPath(PersistentPropertyPath parameterProperty, PersistentPropertyPath bindedPath) { if (parameterProperty == null) { - return asStringPath(bindedPath.getAssociations(), bindedPath.getProperty()); + return bindedPath.getArrayPath(); } - List parameterPath = Arrays.asList(asStringPath(parameterProperty.getAssociations(), parameterProperty.getProperty())); - List path = new LinkedList<>(Arrays.asList(asStringPath(bindedPath.getAssociations(), bindedPath.getProperty()))); + List parameterPath = List.of(parameterProperty.getArrayPath()); + List path = List.of(bindedPath.getArrayPath()); if (path.equals(parameterPath)) { return null; } @@ -350,16 +371,4 @@ private JsonDataType getJsonDataType(PersistentPropertyPath propertyPath, Parame return JsonDataType.DEFAULT; } - private String[] asStringPath(List associations, PersistentProperty property) { - if (associations.isEmpty()) { - return new String[]{property.getName()}; - } - List path = new ArrayList<>(associations.size() + 1); - for (Association association : associations) { - path.add(association.getName()); - } - path.add(property.getName()); - return path.toArray(new String[0]); - } - } 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 397d922d041..e69ccb59591 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 @@ -17,6 +17,7 @@ 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; @@ -77,12 +78,12 @@ public ParameterExpression expression(PersistentProperty property, String } @Override - public ParameterExpression parameter(ParameterElement parameterElement) { + public ParameterExpression parameter(ParameterElement parameterElement, PersistentPropertyPath propertyPath) { throw notSupportedOperation(); } @Override - public ParameterExpression entityPropertyParameter(ParameterElement entityParameter) { + public ParameterExpression entityPropertyParameter(ParameterElement entityParameter, PersistentPropertyPath propertyPath) { throw notSupportedOperation(); } } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentPropertyPathImpl.java b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentPropertyPathImpl.java index abd78f80195..ac77e8afaaf 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentPropertyPathImpl.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/criteria/impl/SourcePersistentPropertyPathImpl.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.Association; -import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentPropertyPath; +import io.micronaut.data.model.jpa.criteria.impl.DefaultPersistentPropertyPath; import io.micronaut.data.processor.model.SourcePersistentProperty; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Path; @@ -32,7 +32,7 @@ * @since 3.2 */ @Internal -final class SourcePersistentPropertyPathImpl extends AbstractPersistentPropertyPath implements SourcePersistentPropertyPath { +final class SourcePersistentPropertyPathImpl extends DefaultPersistentPropertyPath implements SourcePersistentPropertyPath { private final Path parentPath; private final SourcePersistentProperty sourcePersistentProperty; 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 b1318fcda76..026d093ad91 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 @@ -614,7 +614,7 @@ private QueryParameterBinding createAdditionalBinding(BindingParameter.BindingCo if (parameterExpression.isPresent()) { ClassElement type = RawQueryMethodMatcher.extractExpressionType(matchContext, parameterExpression.orElseThrow()); - return new SourceParameterExpressionImpl(configuredDataTypes, name, type) + return new SourceParameterExpressionImpl(configuredDataTypes, name, type, null) .bind(bindingContext); } @@ -634,7 +634,8 @@ private QueryParameterBinding createAdditionalBinding(BindingParameter.BindingCo return new SourceParameterExpressionImpl(configuredDataTypes, matchContext.parameters, parameter, - false) + false, + null) .bind(bindingContext); } 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 76fcf5fe033..607bea83560 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 @@ -205,7 +205,7 @@ protected final Predicate extractPredicates(List queryParams, for (ParameterElement queryParam : queryParams) { String paramName = queryParam.getName(); PersistentPropertyPath propPath = rootEntity.getPropertyPath(rootEntity.getPath(paramName).orElse(paramName)); - ParameterExpression param = cb.parameter(queryParam); + ParameterExpression param = cb.parameter(queryParam, propPath); if (propPath == null) { if (TypeRole.ID.equals(paramName) && (rootEntity.hasIdentity() || rootEntity.hasCompositeIdentity())) { predicates.add(cb.equal(root.id(), param)); @@ -365,7 +365,7 @@ private Predicate findMethodPredicate(String expression, restrictionName += IGNORE_CASE; propertyName = propertyName.substring(IGNORE_CASE.length()); } - Restrictions.PropertyRestriction restriction = Restrictions.findPropertyRestriction(restrictionName); + Restrictions.PropertyRestriction restriction = Restrictions.findPropertyRestriction(restrictionName); if (restriction == null) { throw new MatchFailedException("Unknown restriction: " + restrictionName); } @@ -375,7 +375,7 @@ private Predicate findMethodPredicate(String expression, Matcher matcher = RESTRICTIONS_PATTERN.matcher(expression); if (matcher.find()) { String restrictionName = matcher.group(1); - Restrictions.Restriction restriction = Restrictions.findRestriction(restrictionName); + Restrictions.Restriction restriction = Restrictions.findRestriction(restrictionName); if (restriction == null) { throw new MatchFailedException("Unknown restriction: " + restrictionName); } @@ -388,7 +388,7 @@ private Predicate findMethodPredicate(String expression, restrictionName += IGNORE_CASE; propertyName = extractPropertyName(propertyName, IGNORE_CASE); } - Restrictions.PropertyRestriction restriction = Restrictions.findPropertyRestriction(restrictionName); + Restrictions.PropertyRestriction restriction = Restrictions.findPropertyRestriction(restrictionName); return getPropertyRestriction(propertyName, root, cb, parameters, restriction); } @@ -412,7 +412,7 @@ private Predicate getPropertyRestriction(String propertyName, PersistentEntityRoot root, SourcePersistentEntityCriteriaBuilder cb, Iterator parameters, - Restrictions.PropertyRestriction restriction) { + Restrictions.PropertyRestriction restriction) { boolean negation = false; if (propertyName.endsWith(NOT)) { @@ -425,17 +425,18 @@ private Predicate getPropertyRestriction(String propertyName, throw new MatchFailedException("No property name specified in clause: " + restriction.getName()); } - Expression prop = getProperty(root, propertyName); + Expression prop = getProperty(root, propertyName); + List> parameterExpressions = provideParams(parameters, + restriction.getRequiredParameters(), + restriction.getName(), + cb, + prop + ); Predicate predicate = restriction.find(root, cb, prop, - provideParams(parameters, - restriction.getRequiredParameters(), - restriction.getName(), - cb, - prop - ).toArray(new ParameterExpression[0])); + parameterExpressions); if (negation) { predicate = predicate.not(); @@ -446,20 +447,18 @@ private Predicate getPropertyRestriction(String propertyName, private Predicate getRestriction(PersistentEntityRoot root, SourcePersistentEntityCriteriaBuilder cb, Iterator parameters, - Restrictions.Restriction restriction) { + Restrictions.Restriction restriction) { Expression property = null; if (restriction.getName().equals("Ids")) { property = root.id(); } - return restriction.find(root, + List> parameterExpressions = provideParams(parameters, + restriction.getRequiredParameters(), + restriction.getName(), cb, - provideParams(parameters, - restriction.getRequiredParameters(), - restriction.getName(), - cb, - property - ).toArray(new ParameterExpression[0]) + property ); + return restriction.find(root, cb, parameterExpressions); } private List> provideParams(Iterator parameters, @@ -488,9 +487,10 @@ private List> provideParams(Iterator Expression getProperty(PersistentEntityRoot root, String propertyName) { + protected final Expression getProperty(PersistentEntityRoot root, String propertyName) { if (TypeRole.ID.equals(NameUtils.decapitalize(propertyName)) && (root.getPersistentEntity().hasIdentity() || root.getPersistentEntity().hasCompositeIdentity())) { return root.id(); } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/ProcedureMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/ProcedureMethodMatcher.java index 5b24032e652..81ddd32bd87 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/ProcedureMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/ProcedureMethodMatcher.java @@ -142,15 +142,12 @@ public Map getAdditionalRequiredParameters() { } private SourceParameterExpressionImpl bindingParameter(MethodMatchContext matchContext, ParameterElement element) { - return bindingParameter(matchContext, element, false); - } - - private SourceParameterExpressionImpl bindingParameter(MethodMatchContext matchContext, ParameterElement element, boolean isEntityParameter) { return new SourceParameterExpressionImpl( Utils.getConfiguredDataTypes(matchContext.getRepositoryClass()), matchContext.getParameters(), element, - isEntityParameter); + false, + null); } } 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 0ead0fbb9e3..ee4408482a0 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 @@ -372,7 +372,8 @@ private static SourceParameterExpressionImpl bindingParameter(MethodMatchContext Utils.getConfiguredDataTypes(matchContext.getRepositoryClass()), matchContext.getParameters(), element, - isEntityParameter); + isEntityParameter, + null); } private static SourceParameterExpressionImpl bindingParameter(MethodMatchContext matchContext, @@ -381,7 +382,8 @@ private static SourceParameterExpressionImpl bindingParameter(MethodMatchContext return new SourceParameterExpressionImpl( Utils.getConfiguredDataTypes(matchContext.getRepositoryClass()), name, - type); + type, + null); } /** 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 9ff95a1d601..d60e511d891 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 @@ -97,8 +97,8 @@ public int getRequiredParameters() { @Override public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, - ParameterExpression[] parameters) { - return entityRoot.id().in(parameters[0]); + List> parameters) { + return entityRoot.id().in(parameters.get(0)); } } @@ -390,7 +390,7 @@ public String getName() { public static class PropertyIlike extends SinglePropertyExpressionRestriction { public PropertyIlike() { - super(PersistentEntityCriteriaBuilder::ilikeString); + super(PersistentEntityCriteriaBuilder::ilike); } @Override @@ -405,7 +405,7 @@ public String getName() { public static class PropertyRlike extends SinglePropertyExpressionRestriction { public PropertyRlike() { - super(PersistentEntityCriteriaBuilder::rlikeString); + super(PersistentEntityCriteriaBuilder::regex); } @Override @@ -449,8 +449,8 @@ public int getRequiredParameters() { public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, Expression expression, - ParameterExpression[] parameters) { - return expression.in(parameters[0]).not(); + List> parameters) { + return expression.in(parameters.get(0)).not(); } } @@ -489,8 +489,8 @@ public int getRequiredParameters() { public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, Expression expression, - ParameterExpression[] parameters) { - return expression.in(parameters[0]); + List> parameters) { + return expression.in(parameters.get(0)); } } @@ -622,8 +622,8 @@ public int getRequiredParameters() { public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, Expression expression, - ParameterExpression[] parameters) { - return cb.between(expression, parameters[0], parameters[1]); + List> parameters) { + return cb.between(expression, parameters.get(0), parameters.get(1)); } } @@ -733,7 +733,7 @@ public int getRequiredParameters() { public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, Expression expression, - ParameterExpression[] parameters) { + List> parameters) { return func.apply(cb, expression); } } @@ -755,8 +755,8 @@ public int getRequiredParameters() { public Predicate find(PersistentEntityRoot entityRoot, PersistentEntityCriteriaBuilder cb, Expression expression, - ParameterExpression[] parameters) { - return func.apply(cb, expression, parameters[0]); + List> parameters) { + return func.apply(cb, expression, parameters.get(0)); } } @@ -776,7 +776,7 @@ public interface PropertyRestriction { Predicate find(@NonNull PersistentEntityRoot entityRoot, @NonNull PersistentEntityCriteriaBuilder cb, @NonNull Expression expression, - @NonNull ParameterExpression[] parameters); + @NonNull List> parameters); } /** @@ -793,7 +793,7 @@ public interface Restriction { @NonNull Predicate find(@NonNull PersistentEntityRoot entityRoot, @NonNull PersistentEntityCriteriaBuilder cb, - @NonNull ParameterExpression[] parameters); + @NonNull List> parameters); } 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 16c25304421..5f0b48bf1b2 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 @@ -343,6 +343,27 @@ public static boolean doesMethodProducesIterableOfAnEntityOrDto(MethodElement me return TypeUtils.isIterableOfEntity(returnType) || TypeUtils.isIterableOfDto(returnType); } + /** + * Checks whether the return type is supported. + * + * @param methodElement The method + * @return True if it is supported + */ + public static boolean doesMethodProducesIterable(MethodElement methodElement) { + ClassElement returnType = methodElement.getGenericReturnType(); + if (TypeUtils.isReactiveType(returnType)) { + return true; + } + if (TypeUtils.isFutureType(returnType)) { + returnType = returnType.getFirstTypeArgument().orElse(null); + return returnType != null && returnType.isAssignable(Iterable.class); + } + if (methodElement.isSuspend()) { + returnType = TypeUtils.getKotlinCoroutineProducedType(methodElement); + } + return returnType.isAssignable(Iterable.class); + } + /** * Checks whether the return type is supported. * 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 5c352218fdc..5a1d2b81774 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 @@ -113,18 +113,18 @@ protected void addPropertiesToUpdate(List nonConsumedParam if (DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(matchContext.getAnnotationMetadata())) { AnnotationValue entityRepresentationAnnotationValue = rootEntity.getAnnotationMetadata().getAnnotation(EntityRepresentation.class); String columnName = entityRepresentationAnnotationValue.getRequiredValue("column", String.class); - query.set(columnName, cb.parameter(entityParameter)); + query.set(columnName, cb.parameter(entityParameter, null)); return; } Stream.concat(rootEntity.getPersistentProperties().stream(), Stream.of(rootEntity.getVersion())) .filter(p -> p != null && !(p instanceof Association association && association.isForeignKey()) && !p.isGenerated() && p.findAnnotation(AutoPopulated.class).map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)).orElse(true)) - .forEach(p -> query.set(p.getName(), cb.entityPropertyParameter(entityParam))); + .forEach(p -> query.set(p.getName(), cb.entityPropertyParameter(entityParam, new PersistentPropertyPath(p)))); if (((AbstractPersistentEntityCriteriaUpdate) query).getUpdateValues().isEmpty()) { // Workaround for only ID entities - query.set(rootEntity.getIdentity().getName(), cb.entityPropertyParameter(entityParam)); + query.set(rootEntity.getIdentity().getName(), cb.entityPropertyParameter(entityParam, new PersistentPropertyPath(rootEntity.getIdentity()))); } } @@ -199,7 +199,7 @@ protected void addPropertiesToUpdate(List nonConsumedParam if (prop.isGenerated()) { throw new MatchFailedException("Cannot update a generated property: " + name); } else { - query.set(name, cb.parameter(parameter)); + query.set(name, cb.parameter(parameter, new PersistentPropertyPath(prop))); } } } @@ -226,14 +226,14 @@ protected void addPropertiesToUpdate(List nonConsumedParam if (path != null) { PersistentProperty property = path.getProperty(); if (path.getAssociations().isEmpty()) { - query.set(property.getName(), cb.parameter(p)); + query.set(property.getName(), cb.parameter(p, path)); } else { // TODO: support embedded ID Association association = path.getAssociations().get(0); if (path.getAssociations().size() == 1 && PersistentEntityUtils.isAccessibleWithoutJoin(association, property)) { // Added Void type to satisfy the type check Path pp = root.join(association.getName()).get(property.getName()); - Expression parameter = cb.parameter(p); + Expression parameter = cb.parameter(p, path); query.set(pp, parameter); } else { throw new MatchFailedException("Cannot perform batch update for a property with an association: " + parameterName); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java index e466edfd28e..183654849bd 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.data.intercept.annotation.DataMethod; -import io.micronaut.data.model.Embedded; +import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaDelete; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; @@ -115,17 +115,17 @@ private void applyPredicates(MethodMatchContext matchContext, final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); if (rootEntity.getVersion() != null) { predicate = cb.and( - cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)), - cb.equal(root.version(), cb.entityPropertyParameter(entityParameter)) + cb.equal(root.id(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getIdentity()))), + cb.equal(root.version(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getVersion()))) ); } else { boolean generateInIdList = getEntitiesParameter() != null && !rootEntity.hasCompositeIdentity() && !rootEntity.getIdentity().isEmbedded(); if (generateInIdList) { - predicate = root.id().in(cb.entityPropertyParameter(entityParameter)); + predicate = root.id().in(cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getIdentity()))); } else { - predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)); + predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getIdentity()))); } } } else { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java index 43862d4d6d4..6250abe3815 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java @@ -20,6 +20,7 @@ import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.Version; import io.micronaut.data.intercept.annotation.DataMethod; +import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder; import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaUpdate; import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot; @@ -115,10 +116,10 @@ protected void apply(MethodMatchContext matchContext, // Add updatable auto-populated parameters entity.getPersistentProperties().stream() .filter(p -> p != null && p.findAnnotation(AutoPopulated.class).map(ap -> ap.getRequiredValue(AutoPopulated.UPDATEABLE, Boolean.class)).orElse(false)) - .forEach(p -> query.set(p.getName(), cb.parameter((ParameterElement) null))); + .forEach(p -> query.set(p.getName(), cb.parameter(null, new PersistentPropertyPath(p)))); if (entity.getVersion() != null && !entity.getVersion().isGenerated() && criteriaUpdate.hasVersionRestriction()) { - query.set(entity.getVersion().getName(), cb.parameter((ParameterElement) null)); + query.set(entity.getVersion().getName(), cb.parameter(null, new PersistentPropertyPath(entity.getVersion()))); } if (criteriaUpdate.getUpdateValues().isEmpty()) { @@ -166,16 +167,16 @@ protected Predicate interceptPredicate(MethodMatchContext matchContext, if (entityParameter == null) { entityParameter = getEntitiesParameter(); } + final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); Predicate predicate = null; if (entityParameter != null) { - final SourcePersistentEntity rootEntity = (SourcePersistentEntity) root.getPersistentEntity(); if (rootEntity.getVersion() != null) { predicate = cb.and( - cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)), - cb.equal(root.version(), cb.entityPropertyParameter(entityParameter)) + cb.equal(root.id(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getIdentity()))), + cb.equal(root.version(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getVersion()))) ); } else { - predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter)); + predicate = cb.equal(root.id(), cb.entityPropertyParameter(entityParameter, new PersistentPropertyPath(rootEntity.getIdentity()))); } } else { ParameterElement idParameter = notConsumedParameters.stream() @@ -184,11 +185,11 @@ protected Predicate interceptPredicate(MethodMatchContext matchContext, .filter(p -> p.hasAnnotation(Version.class)).findFirst().orElse(null); if (idParameter != null) { notConsumedParameters.remove(idParameter); - predicate = cb.equal(root.id(), cb.parameter(idParameter)); + predicate = cb.equal(root.id(), cb.parameter(idParameter, new PersistentPropertyPath(rootEntity.getIdentity()))); } if (versionParameter != null) { notConsumedParameters.remove(versionParameter); - Predicate versionPredicate = cb.equal(root.version(), cb.parameter(versionParameter)); + Predicate versionPredicate = cb.equal(root.version(), cb.parameter(versionParameter, new PersistentPropertyPath(rootEntity.getVersion()))); if (predicate != null) { predicate = cb.and(predicate, versionPredicate); } else { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/spec/FindAllSpecificationMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/spec/FindAllSpecificationMethodMatcher.java index b39277e92a1..3cf0d9a4cf3 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/spec/FindAllSpecificationMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/spec/FindAllSpecificationMethodMatcher.java @@ -52,7 +52,7 @@ public int getOrder() { @Override protected MethodMatch match(MethodMatchContext matchContext, Matcher matcher) { - if (TypeUtils.doesMethodProducesIterableOfAnEntityOrDto(matchContext.getMethodElement()) && isCorrectParameters(matchContext.getMethodElement())) { + if (TypeUtils.doesMethodProducesIterable(matchContext.getMethodElement()) && isCorrectParameters(matchContext.getMethodElement())) { if (isFirstParameterMicronautDataQuerySpecification(matchContext.getMethodElement())) { FindersUtils.InterceptorMatch e = FindersUtils.pickFindAllSpecInterceptor(matchContext, matchContext.getReturnType()); return mc -> new MethodMatchInfo(DataMethod.OperationType.QUERY, e.returnType(), e.interceptor()); diff --git a/data-processor/src/test/groovy/io/micronaut/data/annotation/WhereAnnotationSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/annotation/WhereAnnotationSpec.groovy index 1440bc38bf9..f807a189230 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/annotation/WhereAnnotationSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/annotation/WhereAnnotationSpec.groovy @@ -99,7 +99,7 @@ class Category { repository.getRequiredMethod("findById", Long) .stringValue(Query).get() == "SELECT user_ FROM test.User AS user_ WHERE (user_.id = :p1 AND user_.enabled = true)" repository.getRequiredMethod("deleteById", Long) - .stringValue(Query).get() == "DELETE test.User AS user_ WHERE (user_.id = :p1 AND (user_.enabled = true))" + .stringValue(Query).get() == "DELETE test.User AS user_ WHERE (user_.id = :p1 AND user_.enabled = true)" repository.getRequiredMethod("deleteAll") .stringValue(Query).get() == "DELETE test.User AS user_ WHERE (user_.enabled = true)" repository.getRequiredMethod("count") @@ -173,7 +173,7 @@ interface TestRepository extends CrudRepository { repository.getRequiredMethod("findById", Long) .stringValue(Query).get() == "SELECT person_ FROM $Person.name AS person_ WHERE (person_.id = :p1 AND person_.age > 18)" repository.getRequiredMethod("deleteById", Long) - .stringValue(Query).get() == "DELETE $Person.name AS person_ WHERE (person_.id = :p1 AND (person_.age > 18))" + .stringValue(Query).get() == "DELETE $Person.name AS person_ WHERE (person_.id = :p1 AND person_.age > 18)" repository.getRequiredMethod("deleteAll") .stringValue(Query).get() == "DELETE $Person.name AS person_ WHERE (person_.age > 18)" repository.getRequiredMethod("count") diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy index 9e7bb78c11d..e426fd56368 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy @@ -376,7 +376,7 @@ interface MyInterface extends CrudRepository { def method = beanDefinition.findPossibleMethods("update").findFirst().get() expect: - getQuery(method) == 'UPDATE "food" SET "key"=?,"carbohydrates"=?,"portion_grams"=?,"updated_on"=?,"fk_meal_id"=?,"fk_alt_meal"=?,"loooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name"=?,"fresh"=? WHERE ("fid" = ? AND (fresh = \'Y\'))' + getQuery(method) == 'UPDATE "food" SET "key"=?,"carbohydrates"=?,"portion_grams"=?,"updated_on"=?,"fk_meal_id"=?,"fk_alt_meal"=?,"loooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name"=?,"fresh"=? WHERE ("fid" = ? AND fresh = \'Y\')' getParameterPropertyPaths(method) == ["key", "carbohydrates", "portionGrams", "updatedOn", "meal.mid", "alternativeMeal.mid", "longName", "fresh", "fid"] as String[] } 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 a5550946a6a..e791f675904 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 @@ -36,6 +36,7 @@ import spock.lang.Unroll import static io.micronaut.data.processor.visitors.TestUtils.anyParameterExpandable import static io.micronaut.data.processor.visitors.TestUtils.getCountQuery +import static io.micronaut.data.processor.visitors.TestUtils.getDataInterceptor import static io.micronaut.data.processor.visitors.TestUtils.getDataTypes import static io.micronaut.data.processor.visitors.TestUtils.getJoins import static io.micronaut.data.processor.visitors.TestUtils.getParameterBindingIndexes @@ -1516,6 +1517,7 @@ interface BookRepository extends GenericRepository { Long findCountByCriteria3(PredicateSpecification spec); Long findOne(CriteriaQueryBuilder builder); + List findAll(CriteriaQueryBuilder builder); } """) @@ -1527,6 +1529,9 @@ interface BookRepository extends GenericRepository { getResultDataType(findAllBooksByCriteria1) == DataType.ENTITY getResultDataType(findAllBooksByCriteria2) == DataType.ENTITY getResultDataType(findAllBooksByCriteria3) == DataType.ENTITY + getDataInterceptor(findAllBooksByCriteria1) == "io.micronaut.data.runtime.intercept.criteria.FindAllSpecificationInterceptor" + getDataInterceptor(findAllBooksByCriteria2) == "io.micronaut.data.runtime.intercept.criteria.FindAllSpecificationInterceptor" + getDataInterceptor(findAllBooksByCriteria3) == "io.micronaut.data.runtime.intercept.criteria.FindAllSpecificationInterceptor" when: def findBookByCriteria1 = repository.findPossibleMethods("findBookByCriteria1").findFirst().get() @@ -1536,6 +1541,9 @@ interface BookRepository extends GenericRepository { getResultDataType(findBookByCriteria1) == DataType.ENTITY getResultDataType(findBookByCriteria2) == DataType.ENTITY getResultDataType(findBookByCriteria3) == DataType.ENTITY + getDataInterceptor(findBookByCriteria1) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" + getDataInterceptor(findBookByCriteria2) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" + getDataInterceptor(findBookByCriteria3) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" when: def findCountByCriteria1 = repository.findPossibleMethods("findCountByCriteria1").findFirst().get() @@ -1545,10 +1553,218 @@ interface BookRepository extends GenericRepository { getResultDataType(findCountByCriteria1) == DataType.LONG getResultDataType(findCountByCriteria2) == DataType.LONG getResultDataType(findCountByCriteria3) == DataType.LONG + getDataInterceptor(findCountByCriteria1) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" + getDataInterceptor(findCountByCriteria2) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" + getDataInterceptor(findCountByCriteria3) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" when: def findOne = repository.findPossibleMethods("findOne").findFirst().get() then: getResultDataType(findOne) == DataType.LONG + getDataInterceptor(findOne) == "io.micronaut.data.runtime.intercept.criteria.FindOneSpecificationInterceptor" + + when: + def findAll = repository.findPossibleMethods("findAll").findFirst().get() + then: + getResultDataType(findAll) == DataType.STRING + getDataInterceptor(findAll) == "io.micronaut.data.runtime.intercept.criteria.FindAllSpecificationInterceptor" + } + + void "test embedded id"() { + given: + def repository = buildRepository('test.SettlementRepository', """ + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.Join; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + +@JdbcRepository(dialect = io.micronaut.data.model.query.builder.sql.Dialect.H2) +interface SettlementRepository extends CrudRepository { + @Join(value = "settlementType") + @Join(value = "zone") + @Override + Optional findById(@NonNull SettlementPk settlementPk); + + @Join(value = "settlementType") + @Join(value = "zone") + @Join(value = "id.county") + Optional queryById(@NonNull SettlementPk settlementPk); + + @Join(value = "settlementType") + @Join(value = "zone") + @Join(value = "id.county") + List findAll(Pageable pageable); +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@MappedEntity("comp_state") +class State { + @Id + Integer id; + @MappedProperty + String stateName; + @MappedProperty("is_enabled") + Boolean enabled; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@Embeddable +class CountyPk { + @MappedProperty(value = "id") + Integer id; + @MappedProperty(value = "state_id") + @Relation(Relation.Kind.MANY_TO_ONE) + State state; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@MappedEntity("comp_country") +class County { + @EmbeddedId + CountyPk id; + @MappedProperty + String countyName; + @MappedProperty("is_enabled") + Boolean enabled; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@Embeddable +class SettlementPk { + @MappedProperty(value = "code") + String code; + + @MappedProperty(value = "code_id") + Integer codeId; + + @Relation(value = Relation.Kind.MANY_TO_ONE) + County county; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@MappedEntity("comp_settlement") +class Settlement { + @EmbeddedId + SettlementPk id; + @MappedProperty + String description; + @Relation(Relation.Kind.MANY_TO_ONE) + SettlementType settlementType; + @Relation(Relation.Kind.MANY_TO_ONE) + Zone zone; + @MappedProperty("is_enabled") + Boolean enabled; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@MappedEntity("comp_sett_type") +class SettlementType { + @Id + @GeneratedValue + Long id; + @MappedProperty + String name; +} + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@MappedEntity("comp_zone") +class Zone { + @Id + @GeneratedValue + Long id; + @MappedProperty + String name; +} + +""") + when: + def update = repository.findPossibleMethods("update").findFirst().get() + then: + getQuery(update) == "UPDATE `comp_settlement` SET `description`=?,`settlement_type_id`=?,`zone_id`=?,`is_enabled`=? WHERE (`code` = ? AND `code_id` = ? AND `id_county_id_id` = ? AND `id_county_id_state_id` = ?)" + } + + void "test combined id"() { + given: + def repository = buildRepository('test.EntityWithIdClassRepository', """ + +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.repository.GenericRepository;import io.micronaut.data.tck.entities.EntityIdClass; +import io.micronaut.data.tck.entities.EntityWithIdClass; + +import java.util.List; + +@Repository +interface EntityWithIdClassRepository extends GenericRepository { + List findById(EntityIdClass id); + List findById1(Long id1); + List findById2(Long id2); +} + + +""") + when: + def findById1 = repository.findPossibleMethods("findById1").findFirst().get() + def findById = repository.findPossibleMethods("findById").findFirst().get() + then: + getQuery(findById1) == "SELECT entityWithIdClass_ FROM io.micronaut.data.tck.entities.EntityWithIdClass AS entityWithIdClass_ WHERE (entityWithIdClass_.id1 = :p1)" + getParameterBindingPaths(findById1) == [""] as String[] + getParameterPropertyPaths(findById1) == ["id1"] as String[] + getQuery(findById) == "SELECT entityWithIdClass_ FROM io.micronaut.data.tck.entities.EntityWithIdClass AS entityWithIdClass_ WHERE (entityWithIdClass_.id1 = :p1 AND entityWithIdClass_.id2 = :p2)" + getParameterBindingPaths(findById) == ["id1", "id2"] as String[] + getParameterPropertyPaths(findById) == ["id1", "id2"] as String[] + } + + void "test projection"() { + given: + def repository = buildRepository('test.BookRepository', """ + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.tck.entities.Book; + +import java.util.List; + +@JdbcRepository(dialect = io.micronaut.data.model.query.builder.sql.Dialect.POSTGRES) +interface BookRepository extends GenericRepository { + List queryTop3ByAuthorNameOrderByTitle(String name); +} + +""") + when: + def queryTop3ByAuthorNameOrderByTitle = repository.findPossibleMethods("queryTop3ByAuthorNameOrderByTitle").findFirst().get() + then: + getQuery(queryTop3ByAuthorNameOrderByTitle) == '''SELECT book_."id",book_."author_id",book_."genre_id",book_."title",book_."total_pages",book_."publisher_id",book_."last_updated" FROM "book" book_ INNER JOIN "author" book_author_ ON book_."author_id"=book_author_."id" WHERE (book_author_."name" = ?) ORDER BY book_."title" ASC''' + getParameterBindingPaths(queryTop3ByAuthorNameOrderByTitle) == [""] as String[] + getParameterPropertyPaths(queryTop3ByAuthorNameOrderByTitle) == ["author.name"] as String[] + } + + void "test association projection"() { + given: + def repository = buildRepository('test.BookRepository', ''' +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.tck.repositories.AuthorRepository; + +@JdbcRepository(dialect = io.micronaut.data.model.query.builder.sql.Dialect.ANSI) +abstract class BookRepository extends io.micronaut.data.tck.repositories.BookRepository { + + public BookRepository(AuthorRepository authorRepository) { + super(authorRepository); + } +} + +''') + + when: + def method = repository.findPossibleMethods("findByAuthorIsNull").findFirst().get() + then: + getQuery(method) == '''SELECT book_."id",book_."author_id",book_."genre_id",book_."title",book_."total_pages",book_."publisher_id",book_."last_updated" FROM "book" book_ WHERE (book_."author_id" IS NULL)''' + } } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy index 8f406be2bbc..bdf0f880659 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy @@ -26,7 +26,6 @@ import spock.lang.Shared import static io.micronaut.data.processor.visitors.TestUtils.* -//@IgnoreIf({ !jvm.isJava8() }) class CompositePrimaryKeySpec extends AbstractDataSpec { @Shared SourcePersistentEntity entity 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 3be6fd566c2..4570a60f6f4 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 @@ -19,7 +19,6 @@ import groovy.transform.CompileStatic import io.micronaut.core.annotation.AnnotationMetadata import io.micronaut.core.annotation.AnnotationMetadataProvider import io.micronaut.core.annotation.AnnotationValue -import io.micronaut.core.convert.ConversionContext import io.micronaut.data.annotation.Join import io.micronaut.data.annotation.Query import io.micronaut.data.intercept.annotation.DataMethod diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy index 3bae50d78b5..273b8f26c47 100644 --- a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy @@ -16,19 +16,11 @@ package io.micronaut.data.r2dbc.h2 import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.* import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import io.micronaut.data.tck.tests.AbstractRepositorySpec -import spock.lang.AutoCleanup -import spock.lang.Shared class H2CursoredPaginationSpec extends AbstractCursoredPageSpec implements H2TestPropertyProvider { - @Shared - @AutoCleanup - ApplicationContext context - @Memoized @Override PersonRepository getPersonRepository() { @@ -40,9 +32,4 @@ class H2CursoredPaginationSpec extends AbstractCursoredPageSpec implements H2Tes BookRepository getBookRepository() { return context.getBean(H2BookRepository) } - - @Override - void init() { - context = ApplicationContext.run(properties) - } } diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy index 69849a7ce1d..3b3289da3dd 100644 --- a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy @@ -16,17 +16,12 @@ package io.micronaut.data.r2dbc.mysql import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Shared class MySqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements MySqlTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context - @Memoized @Override PersonRepository getPersonRepository() { @@ -39,9 +34,4 @@ class MySqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements My return context.getBean(MySqlBookRepository) } - @Override - void init() { - context = ApplicationContext.run(properties) - } - } diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy index d739f88256b..934a3a08619 100644 --- a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy @@ -16,17 +16,12 @@ package io.micronaut.data.r2dbc.oraclexe import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Shared class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements OracleXETestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context - @Override @Memoized PersonRepository getPersonRepository() { @@ -39,9 +34,4 @@ class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements return context.getBean(OracleXEBookRepository) } - @Override - void init() { - context = ApplicationContext.run(properties) - } - } diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy index 5ef146b2e94..0856e781a8f 100644 --- a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -16,17 +16,11 @@ package io.micronaut.data.r2dbc.postgres import groovy.transform.Memoized -import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec -import spock.lang.AutoCleanup -import spock.lang.Ignore -import spock.lang.Shared -//@Ignore("Causes error: 'FATAL: sorry, too many clients already'") class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { - @Shared @AutoCleanup ApplicationContext context @Memoized @Override @@ -40,8 +34,4 @@ class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements return context.getBean(PostgresBookRepository) } - @Override - void init() { - context = ApplicationContext.run(getProperties()) - } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/criteria/RuntimePersistentPropertyPathImpl.java b/data-runtime/src/main/java/io/micronaut/data/runtime/criteria/RuntimePersistentPropertyPathImpl.java index a8980f5581e..a5555ea4590 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/criteria/RuntimePersistentPropertyPathImpl.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/criteria/RuntimePersistentPropertyPathImpl.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.Association; -import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentPropertyPath; +import io.micronaut.data.model.jpa.criteria.impl.DefaultPersistentPropertyPath; import io.micronaut.data.model.runtime.RuntimePersistentProperty; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Path; @@ -33,7 +33,7 @@ * @since 3.2 */ @Internal -final class RuntimePersistentPropertyPathImpl extends AbstractPersistentPropertyPath { +final class RuntimePersistentPropertyPathImpl extends DefaultPersistentPropertyPath { private final Path parentPath; private final RuntimePersistentProperty runtimePersistentProperty; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index d9d77c15a9e..6415379b47c 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -86,6 +86,8 @@ @Internal public abstract class AbstractSpecificationInterceptor extends AbstractQueryInterceptor { + protected static final String PREPARED_QUERY_KEY = "PREPARED_QUERY"; + protected final CriteriaRepositoryOperations criteriaRepositoryOperations; private final Map sqlQueryBuilderForRepositories = new ConcurrentHashMap<>(); private final Map> methodsJoinPaths = new ConcurrentHashMap<>(); @@ -149,7 +151,9 @@ protected final Iterable findAll(RepositoryMethodKey methodKey, MethodInvocat } return criteriaRepositoryOperations.findAll(query); } - return operations.findAll(preparedQueryForCriteria(methodKey, context, type, methodJoinPaths)); + PreparedQuery preparedQuery = preparedQueryForCriteria(methodKey, context, type, methodJoinPaths); + context.setAttribute(PREPARED_QUERY_KEY, preparedQuery); + return operations.findAll(preparedQuery); } @NonNull diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java index 36419aa0c0b..8a7b7c60be6 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java @@ -19,9 +19,12 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.CollectionUtils; import io.micronaut.data.intercept.RepositoryMethodKey; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import java.util.List; @@ -68,7 +71,18 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext sqlPreparedQuery) { + List cursors = sqlPreparedQuery.createCursors(resultList, pageable); + page = CursoredPage.of(resultList, pageable, cursors, count); + } else { + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + } + } Class rt = context.getReturnType().getType(); if (rt.isInstance(page)) { return page; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java index 358de4a2e0b..faa2b22c6c6 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java @@ -22,6 +22,7 @@ import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.query.JoinPath; +import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; import io.micronaut.data.operations.reactive.ReactiveCapableRepository; import io.micronaut.data.operations.reactive.ReactiveCriteriaCapableRepository; @@ -84,7 +85,9 @@ protected final Publisher findAllReactive(RepositoryMethodKey methodKey, } return reactiveCriteriaOperations.findAll(criteriaQuery); } - return reactiveOperations.findAll(preparedQueryForCriteria(methodKey, context, type, methodJoinPaths)); + PreparedQuery preparedQuery = preparedQueryForCriteria(methodKey, context, type, methodJoinPaths); + context.setAttribute(PREPARED_QUERY_KEY, preparedQuery); + return (Publisher) reactiveOperations.findAll(preparedQuery); } @NonNull diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java index c2667321db3..125931bd34d 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java @@ -19,13 +19,18 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.async.publisher.Publishers; import io.micronaut.data.intercept.RepositoryMethodKey; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + /** * Runtime implementation of {@code Publisher find(Specification, Pageable)}. * @@ -61,12 +66,26 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext pageable.requestTotal() - ? Mono.from(countReactive(methodKey, context)).map(count -> Page.of(list, getPageable(context), count)) - : Mono.just(Page.of(list, getPageable(context), null)) + ? Mono.from(countReactive(methodKey, context)).map(count -> getPage(list, pageable, count, context)) + : Mono.just(getPage(list, pageable, null, context)) ); } return Publishers.convertPublisher(conversionService, result, context.getReturnType().getType()); - } + private Page getPage(List list, Pageable pageable, Long count, MethodInvocationContext context) { + Page page; + if (pageable.getMode() == Pageable.Mode.OFFSET) { + page = Page.of(list, pageable, count); + } else { + PreparedQuery preparedQuery = (PreparedQuery) context.getAttribute(PREPARED_QUERY_KEY).orElse(null); + if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors = sqlPreparedQuery.createCursors(list, pageable); + page = CursoredPage.of(list, pageable, cursors, count); + } else { + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + } + } + return page; + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/reactive/DefaultFindPageReactiveInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/reactive/DefaultFindPageReactiveInterceptor.java index 6453f8fdc28..72dcb34c274 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/reactive/DefaultFindPageReactiveInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/reactive/DefaultFindPageReactiveInterceptor.java @@ -20,12 +20,17 @@ import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.intercept.reactive.FindPageReactiveInterceptor; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import java.util.List; + /** * Default implementation of {@link FindPageReactiveInterceptor}. * @@ -52,8 +57,19 @@ public Publisher interceptPublisher(RepositoryMethodKey methodKey, MethodInvo return Flux.from(reactiveOperations.findOne(countQuery)) .flatMap(total -> { Flux resultList = Flux.from(reactiveOperations.findAll(preparedQuery)); - return resultList.collectList().map(list -> - Page.of(list, preparedQuery.getPageable(), total.longValue()) + return resultList.collectList().map(list -> { + Pageable pageable = preparedQuery.getPageable(); + Page page; + if (pageable.getMode() == Pageable.Mode.OFFSET) { + page = Page.of(list, pageable, total.longValue()); + } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors = sqlPreparedQuery.createCursors(list, pageable); + page = CursoredPage.of(list, pageable, cursors, total.longValue()); + } else { + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + } + return page; + } ); }); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java index 2d0f51fe3e0..b7c0a23ed14 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java @@ -491,7 +491,7 @@ private K readEntity(RS rs, MappingContext ctx, @Nullable Object parent, } else { final Relation.Kind kind = entityAssociation.getKind(); final boolean isInverse = parent != null && isAssociation && ctx.association.getOwner() == entityAssociation.getAssociatedEntity(); - if (isInverse && kind.isSingleEnded()) { + if (isInverse && kind.isSingleEnded() && mappedByMatchesOrEmpty(ctx.association, prop.getProperty())) { args[i] = parent; } else { MappingContext joinCtx = ctx.join(fetchJoinPaths, entityAssociation); @@ -590,7 +590,8 @@ private K readEntity(RS rs, MappingContext ctx, @Nullable Object parent, entity = setProperty(property, entity, value); } else { final boolean isInverse = parent != null && entityAssociation.getKind().isSingleEnded() && isAssociation && ctx.association.getOwner() == entityAssociation.getAssociatedEntity(); - if (isInverse) { + // Before setting property value, check if mappedBy is not different from the property name + if (isInverse && mappedByMatchesOrEmpty(ctx.association, property)) { entity = setProperty(property, entity, parent); } else { MappingContext joinCtx = ctx.join(fetchJoinPaths, entityAssociation); @@ -637,6 +638,23 @@ private K readEntity(RS rs, MappingContext ctx, @Nullable Object parent, } } + /** + * Checks if association mappedBy property is empty or matches with given property name. + * @param association the association + * @param property the bean property + * @return true if mappedBy is not set or else if matches with given property name + * @param The field type + */ + private boolean mappedByMatchesOrEmpty(Association association, BeanProperty property) { + String mappedBy = association.getAnnotationMetadata().stringValue(Relation.class, "mappedBy").orElse(null); + if (mappedBy == null) { + // If mappedBy not set then we don't have what to compare and assume association + // is related to the property being set + return true; + } + return mappedBy.equals(property.getName()); + } + private Object provideConstructorArgumentValue(@NonNull RS rs, @NonNull MappingContext ctx, @NonNull RuntimePersistentProperty property, 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 aa7ff20ea8e..31d7a654d35 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 @@ -228,8 +228,7 @@ public JsonDataType getJsonDataType() { } private Object resolveParameterValue(QueryParameterBinding queryParameterBinding, Object[] parameterArray) { - Object value; - value = parameterArray[queryParameterBinding.getParameterIndex()]; + Object value = parameterArray[queryParameterBinding.getParameterIndex()]; String[] parameterBindingPath = queryParameterBinding.getParameterBindingPath(); if (parameterBindingPath != null) { for (String prop : parameterBindingPath) { diff --git a/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/CriteriaSpec.groovy b/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/CriteriaSpec.groovy index fa41e5713f0..e7d10ac32c3 100644 --- a/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/CriteriaSpec.groovy +++ b/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/CriteriaSpec.groovy @@ -236,16 +236,16 @@ class CriteriaSpec extends AbstractCriteriaSpec { where: property1 | property2 | predicate | expectedWhereQuery - "enabled" | "enabled2" | "equal" | '(test_."enabled"!=test_."enabled2")' - "enabled" | "enabled2" | "notEqual" | '(test_."enabled"=test_."enabled2")' - "enabled" | "enabled2" | "greaterThan" | '(NOT(test_."enabled">test_."enabled2"))' - "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(NOT(test_."enabled">=test_."enabled2"))' - "enabled" | "enabled2" | "lessThan" | '(NOT(test_."enabled"test_."budget"))' - "amount" | "budget" | "ge" | '(NOT(test_."amount">=test_."budget"))' - "amount" | "budget" | "lt" | '(NOT(test_."amount" test_."enabled2"))' + "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(NOT(test_."enabled" >= test_."enabled2"))' + "enabled" | "enabled2" | "lessThan" | '(NOT(test_."enabled" < test_."enabled2"))' + "enabled" | "enabled2" | "lessThanOrEqualTo" | '(NOT(test_."enabled" <= test_."enabled2"))' + "amount" | "budget" | "gt" | '(NOT(test_."amount" > test_."budget"))' + "amount" | "budget" | "ge" | '(NOT(test_."amount" >= test_."budget"))' + "amount" | "budget" | "lt" | '(NOT(test_."amount" < test_."budget"))' + "amount" | "budget" | "le" | '(NOT(test_."amount" <= test_."budget"))' } @Unroll diff --git a/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/StaticCriteriaSpec.groovy b/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/StaticCriteriaSpec.groovy index f274b257b0d..1c0e021afed 100644 --- a/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/StaticCriteriaSpec.groovy +++ b/data-runtime/src/test/groovy/io/micronaut/data/runtime/criteria/StaticCriteriaSpec.groovy @@ -186,12 +186,12 @@ class StaticCriteriaSpec extends AbstractCriteriaSpec { 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" ' + 'FROM "test" test_ ' + 'INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"' + - ' WHERE (test_."amount"=test_others_."amount")', + ' WHERE (test_."amount" = test_others_."amount")', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" ' + 'FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id" ' + 'INNER JOIN "simple_entity" test_others_simple_ ON test_others_."simple_id"=test_others_simple_."id" ' + - 'WHERE (test_."amount"=test_others_."amount" AND test_."amount"=test_others_simple_."amount")', + 'WHERE (test_."amount" = test_others_."amount" AND test_."amount" = test_others_simple_."amount")', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"', diff --git a/data-runtime/src/test/kotlin/io/micronaut/data/runtime/criteria/ext/KCriteriaBuilderExtKtTest.kt b/data-runtime/src/test/kotlin/io/micronaut/data/runtime/criteria/ext/KCriteriaBuilderExtKtTest.kt index 2fbfc53a15c..0485bb21c37 100644 --- a/data-runtime/src/test/kotlin/io/micronaut/data/runtime/criteria/ext/KCriteriaBuilderExtKtTest.kt +++ b/data-runtime/src/test/kotlin/io/micronaut/data/runtime/criteria/ext/KCriteriaBuilderExtKtTest.kt @@ -140,7 +140,7 @@ class KCriteriaBuilderExtKtTest(private val runtimeCriteriaBuilder: RuntimeCrite val criteriaQuery = query.build(runtimeCriteriaBuilder) as QueryResultPersistentEntityCriteriaQuery val q = criteriaQuery.buildQuery(AnnotationMetadata.EMPTY_METADATA, SqlQueryBuilder()).query - Assertions.assertEquals("""SELECT MIN(test_entity_."name") FROM "test_entity" test_entity_ INNER JOIN "other_entity" test_entity_others_ ON test_entity_."id"=test_entity_others_."test_id" WHERE (NOT(test_entity_."enabled"=test_entity_others_."enabled" AND test_entity_."enabled"!=test_entity_others_."enabled" AND test_entity_."enabled"=test_entity_others_."enabled" AND test_entity_."enabled"!=test_entity_others_."enabled") AND (test_entity_."name"test_entity_others_."name" AND test_entity_."name">=test_entity_others_."name" AND (test_entity_."age">test_entity_others_."age" OR test_entity_."age">=test_entity_others_."age" OR test_entity_."age" test_entity_others_."name" AND test_entity_."name" >= test_entity_others_."name" AND (test_entity_."age" > test_entity_others_."age" OR test_entity_."age" >= test_entity_others_."age" OR test_entity_."age" < test_entity_others_."age" OR test_entity_."age" <= test_entity_others_."age"))""", q) } @Test @@ -157,7 +157,7 @@ class KCriteriaBuilderExtKtTest(private val runtimeCriteriaBuilder: RuntimeCrite val criteriaQuery = query.build(runtimeCriteriaBuilder) as QueryResultPersistentEntityCriteriaQuery val q = criteriaQuery.buildQuery(AnnotationMetadata.EMPTY_METADATA, SqlQueryBuilder()).query - Assertions.assertEquals("""SELECT test_entity_."name" FROM "test_entity" test_entity_ INNER JOIN "other_entity" test_entity_others_ ON test_entity_."id"=test_entity_others_."test_id" WHERE (test_entity_others_."id">test_entity_."id")""", q) + Assertions.assertEquals("""SELECT test_entity_."name" FROM "test_entity" test_entity_ INNER JOIN "other_entity" test_entity_others_ ON test_entity_."id"=test_entity_others_."test_id" WHERE (test_entity_others_."id" > test_entity_."id")""", q) } @Test diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCriteriaSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCriteriaSpec.groovy index f0b59260888..b2037f2e1e1 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCriteriaSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCriteriaSpec.groovy @@ -76,7 +76,7 @@ abstract class AbstractCriteriaSpec extends Specification { sqlQuery == 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" ' + 'FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id" ' + 'INNER JOIN "simple_entity" test_others_simple_ ON test_others_."simple_id"=test_others_simple_."id" ' + - 'WHERE (test_."amount"=test_others_."amount" AND test_."amount"=test_others_simple_."amount")' + 'WHERE (test_."amount" = test_others_."amount" AND test_."amount" = test_others_simple_."amount")' } @Unroll @@ -116,12 +116,12 @@ abstract class AbstractCriteriaSpec extends Specification { 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" ' + 'FROM "test" test_ ' + 'INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"' + - ' WHERE (test_."amount"=test_others_."amount")', + ' WHERE (test_."amount" = test_others_."amount")', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" ' + 'FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id" ' + 'INNER JOIN "simple_entity" test_others_simple_ ON test_others_."simple_id"=test_others_simple_."id" ' + - 'WHERE (test_."amount"=test_others_."amount" AND test_."amount"=test_others_simple_."amount")', + 'WHERE (test_."amount" = test_others_."amount" AND test_."amount" = test_others_simple_."amount")', 'SELECT test_."id",test_."name",test_."enabled2",test_."enabled",test_."age",test_."amount",test_."budget" FROM "test" test_ INNER JOIN "other_entity" test_others_ ON test_."id"=test_others_."test_id"' ] } @@ -184,16 +184,16 @@ abstract class AbstractCriteriaSpec extends Specification { where: property1 | property2 | predicate | expectedWhereQuery - "enabled" | "enabled2" | "equal" | '(test_."enabled"=test_."enabled2")' - "enabled" | "enabled2" | "notEqual" | '(test_."enabled"!=test_."enabled2")' - "enabled" | "enabled2" | "greaterThan" | '(test_."enabled">test_."enabled2")' - "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(test_."enabled">=test_."enabled2")' - "enabled" | "enabled2" | "lessThan" | '(test_."enabled"test_."budget")' - "amount" | "budget" | "ge" | '(test_."amount">=test_."budget")' - "amount" | "budget" | "lt" | '(test_."amount" test_."enabled2")' + "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(test_."enabled" >= test_."enabled2")' + "enabled" | "enabled2" | "lessThan" | '(test_."enabled" < test_."enabled2")' + "enabled" | "enabled2" | "lessThanOrEqualTo" | '(test_."enabled" <= test_."enabled2")' + "amount" | "budget" | "gt" | '(test_."amount" > test_."budget")' + "amount" | "budget" | "ge" | '(test_."amount" >= test_."budget")' + "amount" | "budget" | "lt" | '(test_."amount" < test_."budget")' + "amount" | "budget" | "le" | '(test_."amount" <= test_."budget")' } @Unroll @@ -208,16 +208,16 @@ abstract class AbstractCriteriaSpec extends Specification { where: property1 | property2 | predicate | expectedWhereQuery - "enabled" | "enabled2" | "equal" | '(test_."enabled"!=test_."enabled2")' - "enabled" | "enabled2" | "notEqual" | '(test_."enabled"=test_."enabled2")' - "enabled" | "enabled2" | "greaterThan" | '(NOT(test_."enabled">test_."enabled2"))' - "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(NOT(test_."enabled">=test_."enabled2"))' - "enabled" | "enabled2" | "lessThan" | '(NOT(test_."enabled"test_."budget"))' - "amount" | "budget" | "ge" | '(NOT(test_."amount">=test_."budget"))' - "amount" | "budget" | "lt" | '(NOT(test_."amount" test_."enabled2"))' + "enabled" | "enabled2" | "greaterThanOrEqualTo" | '(NOT(test_."enabled" >= test_."enabled2"))' + "enabled" | "enabled2" | "lessThan" | '(NOT(test_."enabled" < test_."enabled2"))' + "enabled" | "enabled2" | "lessThanOrEqualTo" | '(NOT(test_."enabled" <= test_."enabled2"))' + "amount" | "budget" | "gt" | '(NOT(test_."amount" > test_."budget"))' + "amount" | "budget" | "ge" | '(NOT(test_."amount" >= test_."budget"))' + "amount" | "budget" | "lt" | '(NOT(test_."amount" < test_."budget"))' + "amount" | "budget" | "le" | '(NOT(test_."amount" <= test_."budget"))' } void "test function projection 1"() { @@ -358,6 +358,37 @@ abstract class AbstractCriteriaSpec extends Specification { getSelectQueryPart(criteriaQuery) == 'CONCAT(?,?)' } + void "test like"() { + given: + PersistentEntityRoot entityRoot = createRoot(criteriaQuery) + + when: + criteriaQuery.where( + criteriaBuilder.like( + criteriaBuilder.parameter(String), + criteriaBuilder.parameter(String), + criteriaBuilder.parameter(Character), + ) + ) + then: + getWhereQueryPart(criteriaQuery) == '(? LIKE ? ESCAPE ?)' + } + + void "test like case insensitive"() { + given: + PersistentEntityRoot entityRoot = createRoot(criteriaQuery) + + when: + criteriaQuery.where( + criteriaBuilder.ilike( + criteriaBuilder.parameter(String), + criteriaBuilder.parameter(String), + ) + ) + then: + getWhereQueryPart(criteriaQuery) == '(LOWER(?) LIKE LOWER(?))' + } + @Unroll void "test select distinct #distinct #properties produces selection: #expectedSelectQuery"() { given: diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy index a8a967793f3..e05229525d0 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.data.tck.tests +import io.micronaut.context.ApplicationContext import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Page import io.micronaut.data.model.Pageable @@ -23,18 +24,23 @@ import io.micronaut.data.tck.entities.Book import io.micronaut.data.tck.entities.Person import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository +import spock.lang.AutoCleanup +import spock.lang.Shared import spock.lang.Specification +import java.util.function.Function + abstract class AbstractCursoredPageSpec extends Specification { + @AutoCleanup + @Shared + ApplicationContext context = ApplicationContext.run(properties) + abstract PersonRepository getPersonRepository() abstract BookRepository getBookRepository() - abstract void init() - def setup() { - init() // Create a repository that will look something like this: // id | name | age @@ -218,11 +224,11 @@ abstract class AbstractCursoredPageSpec extends Specification { Sort.of(Sort.Order.desc("name")) | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ06" | "ZZZZZ03" } - void "test pageable findBy"() { + void "test cursored pageable"(Function> resultFunction) { when: "People are searched for" def pageable = CursoredPageable.from(10, null) - Page page = personRepository.findByNameLike("A%", pageable) - Page page2 = personRepository.findPeople("A%", pageable) + def page = resultFunction.apply(pageable) + def page2 = personRepository.findPeople("A%", pageable) then: "The page is correct" page.offset == 0 @@ -233,7 +239,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content.name.every{ it.startsWith("A") } when: "The next page is retrieved" - page = personRepository.findByNameLike("A%", page.nextPageable()) + page = resultFunction.apply(page.nextPageable()) then: "it is correct" page.offset == 10 @@ -243,7 +249,7 @@ abstract class AbstractCursoredPageSpec extends Specification { when: "The previous page is selected" pageable = page.previousPageable() - page = personRepository.findByNameLike("A%", pageable) + page = resultFunction.apply(pageable) then: "it is correct" page.offset == 0 @@ -251,6 +257,12 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content.size() == 10 page.content.id == firstContent.id page.content.name.every{ it.startsWith("A") } + + where: + resultFunction << [ + (cursoredPageable) -> personRepository.findByNameLike("A%", (Pageable) cursoredPageable), + (cursoredPageable) -> personRepository.findAll(PersonRepository.Specifications.nameLike("A%"), (Pageable) cursoredPageable), + ] } void "test find with left join"() { diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractReactiveRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractReactiveRepositorySpec.groovy index 7cbb424c240..8cebd55a697 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractReactiveRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractReactiveRepositorySpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.data.tck.tests import io.micronaut.context.ApplicationContext +import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Page import io.micronaut.data.model.Pageable import io.micronaut.data.model.Sort @@ -37,8 +38,7 @@ import spock.lang.Shared import spock.lang.Specification import spock.lang.Stepwise -import java.util.stream.Collectors -import java.util.stream.IntStream +import java.util.function.Function import java.util.stream.LongStream import static io.micronaut.data.repository.jpa.criteria.QuerySpecification.where @@ -682,7 +682,7 @@ abstract class AbstractReactiveRepositorySpec extends Specification { personRepository.saveAll(people).collectList().block() } - void "test pageable list"() { + void "test pageable list"(Pageable pageable) { given: setupPersonsForPageableTest() when: "All the people are count" @@ -692,7 +692,6 @@ abstract class AbstractReactiveRepositorySpec extends Specification { count == 1300 when: "10 people are paged" - def pageable = Pageable.from(0, 10, Sort.of(Sort.Order.asc("id"))) Page page = personRepository.findAll(pageable).block() then: "The data is correct" @@ -724,6 +723,9 @@ abstract class AbstractReactiveRepositorySpec extends Specification { page.pageNumber == 0 page.content[0].name.startsWith("A") page.content.size() == 10 + + where: + pageable << [Pageable.from(0, 10, Sort.of(Sort.Order.asc("id"))), CursoredPageable.from(10, null)] } void "test pageable sort"() { @@ -760,12 +762,11 @@ abstract class AbstractReactiveRepositorySpec extends Specification { page.content[0].name.startsWith("Z") } - void "test pageable findBy"() { + void "test pageable findBy"(Function> resultFunction, Pageable pageable) { given: setupPersonsForPageableTest() when: "People are searched for" - def pageable = Pageable.from(0, 10) - Page page = personRepository.findByNameLike("A%", pageable).block() + Page page = resultFunction.apply(pageable) Page page2 = personRepository.findPeople("A%", pageable).block() then: "The page is correct" @@ -784,6 +785,13 @@ abstract class AbstractReactiveRepositorySpec extends Specification { page.totalSize == 50 page.nextPageable().offset == 20 page.nextPageable().number == 2 + + where: + pageable << [Pageable.from(0, 10), CursoredPageable.from(10, null)] + resultFunction << [ + (firstPageable) -> personRepository.findByNameLike("A%", firstPageable).block(), + (secondPageable) -> personRepository.findAll(PersonReactiveRepository.Specifications.nameLike("A%"), (Pageable) secondPageable).block() + ] } protected void savePersons(List names) { 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 9ead95e13b6..75acd593759 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 @@ -21,6 +21,7 @@ import io.micronaut.data.exceptions.EmptyResultException import io.micronaut.data.exceptions.OptimisticLockException import io.micronaut.data.model.Pageable import io.micronaut.data.model.Sort +import io.micronaut.data.model.jpa.criteria.PersistentEntityCriteriaBuilder import io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder import io.micronaut.data.repository.jpa.criteria.DeleteSpecification import io.micronaut.data.repository.jpa.criteria.PredicateSpecification @@ -60,6 +61,7 @@ import io.micronaut.transaction.TransactionStatus import jakarta.persistence.criteria.CriteriaBuilder import jakarta.persistence.criteria.CriteriaQuery import jakarta.persistence.criteria.CriteriaUpdate +import jakarta.persistence.criteria.Path import jakarta.persistence.criteria.Predicate import jakarta.persistence.criteria.Root import reactor.core.publisher.Mono @@ -2873,6 +2875,116 @@ abstract class AbstractRepositorySpec extends Specification { name == "Fred1Xyz" } + void "test like function"() { + when: + personRepository.deleteAll() + personRepository.save(new Person(name: "Fr_dA1", age: 50)) + personRepository.save(new Person(name: "Fr_dA2", age: 18)) + personRepository.save(new Person(name: "Fr_dB1", age: 18)) + personRepository.save(new Person(name: "Fr_dB2", age: 18)) + then: + def names = personRepository.findAll(new CriteriaQueryBuilder() { + @Override + CriteriaQuery build(CriteriaBuilder criteriaBuilder) { + def query = criteriaBuilder.createQuery(String) + def root = query.from(Person) + def name = root. get("name") + query.select(name) + query.where(criteriaBuilder.like( + name, + "%r\\_dA%", + '\\' as char + )) + return query + } + }) + names.toSet() == ["Fr_dA1", "Fr_dA2"].toSet() + + when: + def negatedNames = personRepository.findAll(new CriteriaQueryBuilder() { + @Override + CriteriaQuery build(CriteriaBuilder criteriaBuilder) { + def query = criteriaBuilder.createQuery(String) + def root = query.from(Person) + def name = root. get("name") + query.select(name) + query.where(criteriaBuilder.notLike( + name, + "%r\\_dA%", + '\\' as char + )) + return query + } + }) + then: + negatedNames.toSet() == ["Fr_dB1", "Fr_dB2"].toSet() + + when: + negatedNames = personRepository.findAll(new CriteriaQueryBuilder() { + @Override + CriteriaQuery build(CriteriaBuilder criteriaBuilder) { + def query = criteriaBuilder.createQuery(String) + def root = query.from(Person) + def name = root. get("name") + query.select(name) + query.where(criteriaBuilder.like( + name, + "%r\\_dA%", + '\\' as char + ).not()) + return query + } + }) + then: + negatedNames.toSet() == ["Fr_dB1", "Fr_dB2"].toSet() + + if (personRepository.getClass().getSimpleName().contains("MS")) { + // SQL server case sensitivity is based on the column configuration + return + } + if (personRepository.getClass().getSimpleName().contains("MySql")) { + // MySQL Like is case insensitive by default + return + } + + when: + def caseSensitiveNames = personRepository.findAll(new CriteriaQueryBuilder() { + @Override + CriteriaQuery build(CriteriaBuilder criteriaBuilder) { + def query = criteriaBuilder.createQuery(String) + def root = query.from(Person) + def name = root. get("name") + query.select(name) + query.where(criteriaBuilder.like( + name, + "%db%", + )) + return query + } + }) + then: + caseSensitiveNames.isEmpty() + + when: + def ilikeNames = personRepository.findAll(new CriteriaQueryBuilder() { + @Override + CriteriaQuery build(CriteriaBuilder criteriaBuilder) { + PersistentEntityCriteriaBuilder cb = criteriaBuilder + def query = criteriaBuilder.createQuery(String) + def root = query.from(Person) + def name = root. get("name") + query.select(name) + query.where(cb.ilike( + name, + "%db%", + )) + return query + } + }) + then: + ilikeNames.toSet() == ["Fr_dB1", "Fr_dB2"].toSet() + } + private GregorianCalendar getYearMonthDay(Date dateCreated) { def cal = dateCreated.toCalendar() def localDate = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)) diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonReactiveRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonReactiveRepository.java index db8e5dd2aa1..446f325619a 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonReactiveRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonReactiveRepository.java @@ -21,6 +21,7 @@ import io.micronaut.data.annotation.Query; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.repository.jpa.criteria.PredicateSpecification; import io.micronaut.data.repository.jpa.reactive.ReactorJpaSpecificationExecutor; import io.micronaut.data.repository.reactive.ReactorPageableRepository; import io.micronaut.data.tck.entities.Person; @@ -90,4 +91,13 @@ public interface PersonReactiveRepository extends ReactorPageableRepository deleteCustomSingleNoEntity(String xyz); + final class Specifications { + + private Specifications() { + } + + public static PredicateSpecification nameLike(String name) { + return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), name); + } + } } diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java index 617f2bc48da..69e2a1298e1 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java @@ -177,12 +177,19 @@ public interface PersonRepository extends CrudRepository, Pageable CursoredPage retrieve(@NonNull Pageable pageable); - class Specifications { + final class Specifications { + + private Specifications() { + } public static PredicateSpecification nameEquals(String name) { return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name); } + public static PredicateSpecification nameLike(String name) { + return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), name); + } + public static PredicateSpecification nameEqualsCaseInsensitive(String name) { return (root, criteriaBuilder) -> criteriaBuilder.equal(criteriaBuilder.lower(root.get("name")), name.toLowerCase()); } diff --git a/doc-examples/azure-cosmos-example-java/src/main/resources/application-example.yml b/doc-examples/azure-cosmos-example-java/src/main/resources/application-example.yml index 4079c644473..d283aadb486 100644 --- a/doc-examples/azure-cosmos-example-java/src/main/resources/application-example.yml +++ b/doc-examples/azure-cosmos-example-java/src/main/resources/application-example.yml @@ -8,6 +8,7 @@ azure: endpoint: https://localhost:8081 key: '' database: + disable-non-streaming-order-by: true throughput-settings: request-units: 1000 auto-scale: false diff --git a/gradle.properties b/gradle.properties index 0d0f6c65bc1..ef595545d65 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.8.2-SNAPSHOT +projectVersion=4.9.0-SNAPSHOT projectGroupId=io.micronaut.data title=Micronaut Data diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 258b0393a9d..5dd7b496b30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,26 @@ [versions] -micronaut = "4.5.0" +micronaut = "4.5.3" micronaut-platform = "4.4.2" micronaut-docs = "2.0.0" micronaut-gradle-plugin = "4.4.0" micronaut-testresources = "2.5.2" -micronaut-azure = "5.4.0" +micronaut-azure = "5.5.1" micronaut-reactor = "3.4.0" -micronaut-rxjava2 = "2.3.0" -micronaut-r2dbc = "5.4.0" +micronaut-rxjava2 = "2.4.0" +micronaut-r2dbc = "5.5.0" micronaut-serde = "2.10.2" -micronaut-sql = "5.6.0" -micronaut-spring = "5.6.0" +micronaut-sql = "5.7.0" +micronaut-spring = "5.7.0" micronaut-test = "4.3.0" micronaut-mongo = "5.3.0" micronaut-kotlin = "4.3.0" micronaut-multitenancy = "5.3.0" -micronaut-validation = "4.5.0" +micronaut-validation = "4.6.1" micronaut-logging = "1.3.0" micronaut-flyway = "7.3.0" -groovy = "4.0.21" +groovy = "4.0.22" managed-javax-persistence = "2.2" managed-jakarta-persistence-api = "3.1.0" @@ -40,8 +40,8 @@ jmh = "1.37" ksp-gradle-plugin = "2.0.0-1.0.22" kotlin-gradle-plugin = "2.0.0" jmh-gradle-plugin = "0.7.2" -spring-boot-gradle-plugin = "3.3.0" -spring-dependency-management-gradle-plugin = "1.1.4" +spring-boot-gradle-plugin = "3.3.1" +spring-dependency-management-gradle-plugin = "1.1.6" shadow-gradle-plugin = "8.0.0" # Dependency versions which are found in the platform BOM diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f0..a4413138c96 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42690..b740cf13397 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/settings.gradle b/settings.gradle index 1db88c21560..02432708257 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.1.3' + id 'io.micronaut.build.shared.settings' version '7.1.4' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/docs/guide/azureCosmos/azureCosmosConfiguration.adoc b/src/main/docs/guide/azureCosmos/azureCosmosConfiguration.adoc index e4b6ab8794f..b9edd7d0397 100644 --- a/src/main/docs/guide/azureCosmos/azureCosmosConfiguration.adoc +++ b/src/main/docs/guide/azureCosmos/azureCosmosConfiguration.adoc @@ -12,3 +12,4 @@ The below is an example application configuration showing container and db prope ---- include::doc-examples/azure-cosmos-example-java/src/main/resources/application-example.yml[] ---- +NOTE: `azure.cosmos.database.disable-non-streaming-order-by` needs to be set to true if the query runs against a region or emulator that has not yet been updated with the new NonStreamingOrderBy query feature.