From d0a337a6251ee03f4a36a59c11b41824f60fb0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:14:46 +0200 Subject: [PATCH 01/11] chore: upgrade to Grails 8.0.0, Java 21 and Gradle 9.8.0 Bump version to 9.0.0-SNAPSHOT and add sbglasius as developer. --- .sdkmanrc | 2 +- gradle.properties | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- plugin/build.gradle | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.sdkmanrc b/.sdkmanrc index 48cc71b..277b213 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,2 +1,2 @@ # Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env -java=17.0.15-librca +java=21.0.12-librca diff --git a/gradle.properties b/gradle.properties index f5a1317..0d92f8c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -projectVersion=8.0.1-SNAPSHOT +projectVersion=9.0.0-SNAPSHOT -grailsVersion=7.0.0 -javaVersion=17 +grailsVersion=8.0.0 +javaVersion=21 hibernate5Version=5.6.15.Final diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da..299848b 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.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/plugin/build.gradle b/plugin/build.gradle index de5f5d3..32f875e 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -41,6 +41,7 @@ grailsPublish { albertop19: 'albertop19', gtors: 'Andrey T', matrei: 'Mattias Reichel', + sbglasius: 'Søren Berg Glasius' ] } From 82d4c70422d22108162c3c4b180db7b60118cf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:14:46 +0200 Subject: [PATCH 02/11] fix: check for Enum explicitly when mapping array criteria values Under Grails 8's Groovy, casting a String to Enum calls Enum.valueOf(Enum, ...) and throws IllegalArgumentException instead of ClassCastException, so invalid enum values no longer produced the expected HibernateException. --- .../pgext/hibernate/criterion/array/PgArrayExpression.groovy | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy index d9c38df..9d7ac43 100644 --- a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy @@ -24,11 +24,10 @@ class PgArrayExpression implements Criterion { private static final PgArrayUtils.MapFunction MAP_TO_ENUM = new PgArrayUtils.MapFunction() { @Override Object map(Object o) { - try { + if (o instanceof Enum) { return ((Enum) o).ordinal() - } catch (ClassCastException e) { - throw new HibernateException("Unable to cast object $o to Enum", e) } + throw new HibernateException("Unable to cast object $o to Enum") } } From 6fb4a2b63753ea0303cb1c9f5dde1b9f906e7019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:14:46 +0200 Subject: [PATCH 03/11] ci: build with Java 21 --- .github/workflows/gradle.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 3a64c11..c4abade 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -52,7 +52,7 @@ jobs: - name: "☕️ Setup JDK" uses: actions/setup-java@v5 with: - java-version: 17 + java-version: 21 distribution: liberica - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v4 @@ -76,7 +76,7 @@ jobs: - name: "☕️ Setup JDK" uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: liberica - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cc7c12..1481381 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: types: [ published ] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - JAVA_VERSION: '17.0.15' # this must be a specific version for reproducible builds + JAVA_VERSION: '21.0.12' # this must be a specific version for reproducible builds RELEASE_TAG_PREFIX: 'v' jobs: publish: From fbf503cab28f49e9671cd209b7c6d865de4ca39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:56:13 +0200 Subject: [PATCH 04/11] refactor: split plugin into core and hibernate5 modules Move the Hibernate-independent code (hstore parsing, BidiEnumMap, PgArrayUtils) and its unit tests to a new grails-postgresql-extensions-core module, and the Hibernate 5 plugin from plugin/ to plugin-hibernate5/, keeping its grails-postgresql-extensions artifact. PgArrayUtils now throws IllegalArgumentException, which the Hibernate 5 criterion rethrows as HibernateException as before. The developer list moves to the root build so all published modules share it. --- build.gradle | 38 ++++++++- plugin-core/build.gradle | 41 ++++++++++ .../hibernate/usertype/BidiEnumMap.groovy | 0 .../hibernate/usertype/HstoreHelper.groovy | 0 .../usertype/HstoreParseException.groovy | 0 .../hibernate/usertype/HstoreParser.java | 0 .../pgext/hibernate/utils/PgArrayUtils.groovy | 4 +- .../usertype/HstoreHelperSpec.groovy | 0 .../usertype/HstoreParserSpec.groovy | 0 plugin-hibernate5/build.gradle | 36 +++++++++ ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 0 .../PostgresqlExtensionsDialect.groovy | 0 .../criterion/array/PgArrayExpression.groovy | 11 ++- .../array/PgArrayILikeFunction.groovy | 0 .../array/PgEmptinessExpression.groovy | 0 .../hstore/PgHstoreILikeValueFunction.groovy | 0 .../hstore/PgHstoreOperatorExpression.groovy | 0 .../hstore/PgHstoreValueFunction.groovy | 0 .../criterion/json/PgJsonExpression.groovy | 0 .../criterion/json/PgJsonbOperator.groovy | 0 .../hibernate/order/OrderByRandom.groovy | 0 .../hibernate/order/OrderBySqlFormula.groovy | 0 .../postgresql/criteria/ArrayCriterias.groovy | 0 .../criteria/HstoreCriterias.groovy | 0 .../postgresql/criteria/JsonCriterias.groovy | 0 .../pgext/hibernate/usertype/ArrayType.java | 0 .../hibernate/usertype/HstoreMapType.groovy | 0 .../hibernate/usertype/JsonMapType.groovy | 0 .../hibernate/usertype/JsonbMapType.groovy | 0 .../hibernate/utils/CriteriaUtils.groovy | 0 ...rg.codehaus.groovy.runtime.ExtensionModule | 0 plugin/build.gradle | 77 ------------------- settings.gradle | 7 +- 33 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 plugin-core/build.gradle rename {plugin => plugin-core}/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy (100%) rename {plugin => plugin-core}/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy (100%) rename {plugin => plugin-core}/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy (100%) rename {plugin => plugin-core}/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java (100%) rename {plugin => plugin-core}/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy (92%) rename {plugin => plugin-core}/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy (100%) rename {plugin => plugin-core}/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy (100%) create mode 100644 plugin-hibernate5/build.gradle rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy (86%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy (100%) rename {plugin => plugin-hibernate5}/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule (100%) delete mode 100644 plugin/build.gradle diff --git a/build.gradle b/build.gradle index 5538ee8..8d616e7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,42 @@ version = projectVersion group = 'io.github.gpc' +def publishedProjects = [ + 'grails-postgresql-extensions-core', + 'grails-postgresql-extensions', +] + +ext.pgextDevelopers = [ + ilopmar: 'Iván López', + alotor: 'Alonso Torres', + mattfeury: 'Matt Feury', + alvarosanchez: 'Alvaro Sanchez-Mariscal', + aeischeid: 'Aaron Eischeid', + mkobel: 'Moritz Kobel', + manuelvio: 'Manuel Unno Vio', + bameda: 'David Barragán Merino', + nobeans: 'Yasuharu Nakano', + burtbeckwith: 'Burt Beckwith', + timic: 'Timur Salyakhutdinov', + zlegein: 'Zach Legein', + tmarthal: 'Tom Marthaler', + pabloalba: 'Pablo Alba', + cornercase: 'Eamon Doyle', + poundex: 'Adam Pounder', + gregopet: 'Gregor Petrin', + unknown: 'Tom Potts', + sabst: 'Sabst', + donbeave: 'Alexey Zhokov', + jglapa: 'Jakub Glapa', + jamesdh: 'James Hardwick', + butters16: 'John Keith', + erichelgeson: 'Eric Helgeson', + albertop19: 'albertop19', + gtors: 'Andrey T', + matrei: 'Mattias Reichel', + sbglasius: 'Søren Berg Glasius' +] + subprojects { repositories { @@ -17,7 +53,7 @@ subprojects { } } - if (name == 'grails-postgresql-extensions') { + if (name in publishedProjects) { // This has to be applied here in the root project due to the nexus plugin requirements apply plugin: 'org.apache.grails.gradle.grails-publish' } diff --git a/plugin-core/build.gradle b/plugin-core/build.gradle new file mode 100644 index 0000000..43d65b0 --- /dev/null +++ b/plugin-core/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'groovy' + id 'java-library' +} + +version = projectVersion +group = 'io.github.gpc' + +grailsPublish { + artifactId = 'grails-postgresql-extensions-core' + githubSlug = 'gpc/grails-postgresql-extensions' + license.name = 'Apache-2.0' + title = 'Grails PostgreSQL Extensions Core' + desc = 'Hibernate-independent support code shared by the Grails PostgreSQL Extensions plugins.' + developers = rootProject.ext.pgextDevelopers +} + +dependencies { + + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + + compileOnly 'org.springframework:spring-core', { // Provided + // impl: Assert + } + + implementation 'org.apache.groovy:groovy' + implementation 'org.slf4j:slf4j-api' + api 'org.postgresql:postgresql' + + testImplementation 'org.spockframework:spock-core' + + testRuntimeOnly 'org.springframework:spring-core', { + // Assert + } +} + +compileJava.options.release = javaVersion.toInteger() + +apply { + from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') +} diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy b/plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy rename to plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy b/plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy rename to plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy b/plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy rename to plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java b/plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java rename to plugin-core/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy b/plugin-core/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy similarity index 92% rename from plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy rename to plugin-core/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy index 930c6a5..faad120 100644 --- a/plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy +++ b/plugin-core/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy @@ -4,8 +4,6 @@ import java.lang.reflect.Array import groovy.transform.CompileStatic -import org.hibernate.HibernateException - /** * Utils for the different criteria queries. */ @@ -43,7 +41,7 @@ class PgArrayUtils { if (mapFunction) { return mapFunction.map(o) } - throw new HibernateException("criteria doesn't support values of type: ${o?.class?.name}. Try: $expectedType or List<$expectedType> instead") + throw new IllegalArgumentException("criteria doesn't support values of type: ${o?.class?.name}. Try: $expectedType or List<$expectedType> instead") } converted.toArray((Object[]) Array.newInstance(expectedType, converted.size())) diff --git a/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy b/plugin-core/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy similarity index 100% rename from plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy rename to plugin-core/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy diff --git a/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy b/plugin-core/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy similarity index 100% rename from plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy rename to plugin-core/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy diff --git a/plugin-hibernate5/build.gradle b/plugin-hibernate5/build.gradle new file mode 100644 index 0000000..78f1309 --- /dev/null +++ b/plugin-hibernate5/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java-library' + id 'org.apache.grails.gradle.grails-plugin' +} + +version = projectVersion +group = 'io.github.gpc' + +grailsPublish { + artifactId = 'grails-postgresql-extensions' + githubSlug = 'gpc/grails-postgresql-extensions' + license.name = 'Apache-2.0' + title = 'Grails PostgreSQL Extensions Plugin' + desc = 'A Grails plugin that adds Hibernate UserTypes for PostgreSQL native types, ' + + 'like Array, HStore, JSON, JSONB, ..., and GORM criteria extensions to query them.' + developers = rootProject.ext.pgextDevelopers +} + +dependencies { + + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + + api project(':grails-postgresql-extensions-core') + + compileOnly 'org.apache.grails:grails-core', { // Provided + // impl: Plugin + } + compileOnly 'org.apache.grails:grails-data-hibernate5', { + // Must be provided by the Grails application + } + + implementation 'com.google.code.gson:gson' + implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" +} + +compileJava.options.release = javaVersion.toInteger() diff --git a/plugin/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy similarity index 86% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy index 9d7ac43..d9309bb 100644 --- a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy +++ b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy @@ -49,9 +49,14 @@ class PgArrayExpression implements Criterion { @Override TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { def arrayType = checkAndGetArrayType(criteria, criteriaQuery, propertyName) - def arrValue = arrayType.typeClass.isEnum() ? - PgArrayUtils.getValueAsArrayOfType(value, Integer, MAP_TO_ENUM) : - PgArrayUtils.getValueAsArrayOfType(value, arrayType.typeClass) + Object[] arrValue + try { + arrValue = arrayType.typeClass.isEnum() ? + PgArrayUtils.getValueAsArrayOfType(value, Integer, MAP_TO_ENUM) : + PgArrayUtils.getValueAsArrayOfType(value, arrayType.typeClass) + } catch (IllegalArgumentException e) { + throw new HibernateException(e.message, e) + } criteriaQuery.getTypedValue(criteria, propertyName, arrValue) as TypedValue[] } diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy similarity index 100% rename from plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy rename to plugin-hibernate5/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy diff --git a/plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule b/plugin-hibernate5/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule similarity index 100% rename from plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule rename to plugin-hibernate5/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule diff --git a/plugin/build.gradle b/plugin/build.gradle deleted file mode 100644 index 32f875e..0000000 --- a/plugin/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -plugins { - id 'java-library' - id 'org.apache.grails.gradle.grails-plugin' -} - -version = projectVersion -group = 'io.github.gpc' - -grailsPublish { - artifactId = 'grails-postgresql-extensions' - githubSlug = 'gpc/grails-postgresql-extensions' - license.name = 'Apache-2.0' - title = 'Grails PostgreSQL Extensions Plugin' - desc = 'A Grails plugin that adds Hibernate UserTypes for PostgreSQL native types, ' + - 'like Array, HStore, JSON, JSONB, ..., and GORM criteria extensions to query them.' - developers = [ - ilopmar: 'Iván López', - alotor: 'Alonso Torres', - mattfeury: 'Matt Feury', - alvarosanchez: 'Alvaro Sanchez-Mariscal', - aeischeid: 'Aaron Eischeid', - mkobel: 'Moritz Kobel', - manuelvio: 'Manuel Unno Vio', - bameda: 'David Barragán Merino', - nobeans: 'Yasuharu Nakano', - burtbeckwith: 'Burt Beckwith', - timic: 'Timur Salyakhutdinov', - zlegein: 'Zach Legein', - tmarthal: 'Tom Marthaler', - pabloalba: 'Pablo Alba', - cornercase: 'Eamon Doyle', - poundex: 'Adam Pounder', - gregopet: 'Gregor Petrin', - unknown: 'Tom Potts', - sabst: 'Sabst', - donbeave: 'Alexey Zhokov', - jglapa: 'Jakub Glapa', - jamesdh: 'James Hardwick', - butters16: 'John Keith', - erichelgeson: 'Eric Helgeson', - albertop19: 'albertop19', - gtors: 'Andrey T', - matrei: 'Mattias Reichel', - sbglasius: 'Søren Berg Glasius' - ] -} - -dependencies { - - implementation platform("org.apache.grails:grails-bom:$grailsVersion") - - compileOnly 'org.apache.grails:grails-core', { // Provided - // impl: Plugin - } - compileOnly 'org.apache.grails:grails-data-hibernate5', { - // Must be provided by the Grails application - } - compileOnly 'org.springframework:spring-core', { // Provided - // impl: Assert - } - - implementation 'com.google.code.gson:gson' - implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" - implementation 'org.postgresql:postgresql' - - testImplementation 'org.spockframework:spock-core' - - testRuntimeOnly 'org.springframework:spring-core', { - // Assert - } -} - -compileJava.options.release = javaVersion.toInteger() - -apply { - from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 8e1c6b3..9f039b5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,8 +33,11 @@ buildCache { rootProject.name = 'postgresql-extensions.ROOT' -include('plugin') -findProject(':plugin').name = 'grails-postgresql-extensions' +include('plugin-core') +findProject(':plugin-core').name = 'grails-postgresql-extensions-core' + +include('plugin-hibernate5') +findProject(':plugin-hibernate5').name = 'grails-postgresql-extensions' include('test-app1') project(':test-app1').projectDir = new File(settingsDir, 'test-apps/app1') From e108e30077c8f6f7d638ca0fbc28a95bd4c31d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:58:37 +0200 Subject: [PATCH 05/11] feat: add Hibernate 7 plugin Add grails-postgresql-extensions-hibernate7 with the same user types, dialect, criteria and orders as the Hibernate 5 plugin, in the same packages: - User types implement the Hibernate 7 UserType API, and the dialect registers the column types of their custom SQL type codes with the DdlTypeRegistry. - Hibernate 7 has no Criteria API, so the criteria are GORM criteria (PgCriterion) turned into JPA predicates by a PredicateGenerator.CriterionHandlerProvider. They render PostgreSQL SQL through a self-rendering function and bind their values as parameters. - GORM only orders by properties, so sqlFormula() and byRandom() add a criterion that appends the formula to the orders of entity queries. Document the plugin, and the GORM 8.0.0 workaround for the 'native' id generator not creating its sequences on PostgreSQL. --- README.md | 30 ++- build.gradle | 1 + plugin-hibernate7/build.gradle | 35 ++++ ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 57 +++++ .../PostgresqlExtensionsDialect.groovy | 42 ++++ .../hibernate/criterion/PgCriterion.groovy | 80 ++++++++ .../PgCriterionHandlerProvider.groovy | 34 +++ .../hibernate/criterion/PgSqlFunction.java | 92 +++++++++ .../criterion/PgSqlFunctionContributor.java | 18 ++ .../hibernate/order/OrderByRandom.groovy | 17 ++ .../hibernate/order/OrderBySqlFormula.groovy | 67 ++++++ .../postgresql/criteria/ArrayCriterias.groovy | 167 +++++++++++++++ .../criteria/HstoreCriterias.groovy | 51 +++++ .../postgresql/criteria/JsonCriterias.groovy | 83 ++++++++ .../postgresql/criteria/OrderCriterias.groovy | 23 +++ .../pgext/hibernate/usertype/ArrayType.java | 194 ++++++++++++++++++ .../hibernate/usertype/HstoreMapType.groovy | 80 ++++++++ .../hibernate/usertype/JsonMapType.groovy | 86 ++++++++ .../hibernate/usertype/JsonbMapType.groovy | 14 ++ .../hibernate/utils/CriteriaUtils.groovy | 54 +++++ ...rg.codehaus.groovy.runtime.ExtensionModule | 7 + ...redicateGenerator$CriterionHandlerProvider | 1 + ...g.hibernate.boot.model.FunctionContributor | 1 + settings.gradle | 3 + 24 files changed, 1235 insertions(+), 2 deletions(-) create mode 100644 plugin-hibernate7/build.gradle create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterionHandlerProvider.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunction.java create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunctionContributor.java create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy create mode 100644 plugin-hibernate7/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule create mode 100644 plugin-hibernate7/src/main/resources/META-INF/services/org.grails.orm.hibernate.query.PredicateGenerator$CriterionHandlerProvider create mode 100644 plugin-hibernate7/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor diff --git a/README.md b/README.md index 17ffe9a..2f0cd63 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,10 @@ More native types and query methods will be added in the future. ## Installation -The Grails 7 version of this plugin supports Hibernate 5. -In `build.gradle` add the following dependencies to install the plugin: +The Grails 8 version of this plugin supports both Hibernate 5 and Hibernate 7, with a plugin for each. +In `build.gradle` add the dependencies for the Hibernate version of your application. + +Hibernate 5: ```groovy dependencies { @@ -59,6 +61,30 @@ dependencies { } ``` +Hibernate 7: + +```groovy +dependencies { + //... + implementation 'io.github.gpc:grails-postgresql-extensions-hibernate7:' + implementation 'org.apache.grails:grails-data-hibernate7' + //... +} +``` + +Both plugins provide the same user types, dialect, criteria and orders, in the same packages, so the domain classes +and queries of an application do not change when it moves from Hibernate 5 to Hibernate 7. + +> [!NOTE] +> With GORM 8.0.0 for Hibernate 7, the default `native` id generator does not create its sequences on PostgreSQL. +> Until this is fixed in GORM, use the `sequence` generator in `grails-app/conf/application.groovy`: +> +> ```groovy +> grails.gorm.default.mapping = { +> id generator: 'sequence' +> } +> ``` + ## Configuration After installing the plugin you have to use a new PostgreSQL Hibernate Dialect in your application. Add it to the diff --git a/build.gradle b/build.gradle index 8d616e7..fe433fe 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ group = 'io.github.gpc' def publishedProjects = [ 'grails-postgresql-extensions-core', 'grails-postgresql-extensions', + 'grails-postgresql-extensions-hibernate7', ] ext.pgextDevelopers = [ diff --git a/plugin-hibernate7/build.gradle b/plugin-hibernate7/build.gradle new file mode 100644 index 0000000..3d8fe98 --- /dev/null +++ b/plugin-hibernate7/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java-library' + id 'org.apache.grails.gradle.grails-plugin' +} + +version = projectVersion +group = 'io.github.gpc' + +grailsPublish { + artifactId = 'grails-postgresql-extensions-hibernate7' + githubSlug = 'gpc/grails-postgresql-extensions' + license.name = 'Apache-2.0' + title = 'Grails PostgreSQL Extensions Plugin for Hibernate 7' + desc = 'A Grails plugin that adds Hibernate 7 UserTypes for PostgreSQL native types, ' + + 'like Array, HStore, JSON, JSONB, ..., and GORM criteria extensions to query them.' + developers = rootProject.ext.pgextDevelopers +} + +dependencies { + + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + + api project(':grails-postgresql-extensions-core') + + compileOnly 'org.apache.grails:grails-core', { // Provided + // impl: Plugin + } + compileOnly 'org.apache.grails:grails-data-hibernate7', { + // Must be provided by the Grails application + } + + api 'com.google.code.gson:gson' +} + +compileJava.options.release = javaVersion.toInteger() diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy new file mode 100644 index 0000000..9d5e17e --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy @@ -0,0 +1,57 @@ +package gpc.pgext + +import groovy.transform.CompileStatic + +import grails.plugins.Plugin + +@CompileStatic +class GrailsPostgresqlExtensionsGrailsPlugin extends Plugin { + + def grailsVersion = '8.0.0 > *' + def title = 'Grails PostgreSQL Extensions Plugin for Hibernate 7' + def author = 'Iván López' + def description = 'Provides Hibernate user types supporting PostgreSQL Native Types ' + + 'like Array, HStore, JSON, JSONB,... as well as new criteria to query ' + + 'these native types.' + def documentation = 'https://github.com/gpc/grails-postgresql-extensions' + def license = 'APACHE' + def organization = [ + name: 'GPC', + url: 'https://github.com/gpc' + ] + def developers = [ + [name: 'Alonso Torres'], + [name: 'Matt Feury'], + [name: 'Alvaro Sanchez-Mariscal'], + [name: 'Aaron Eischeid'], + [name: 'Moritz Kobel'], + [name: 'Manuel Unno Vio'], + [name: 'David Barragán Merino'], + [name: 'Yasuharu Nakano'], + [name: 'Burt Beckwith'], + [name: 'Timur Salyakhutdinov'], + [name: 'Zach Legein'], + [name: 'Tom Marthaler'], + [name: 'Pablo Alba'], + [name: 'Eamon Doyle'], + [name: 'Adam Pounder'], + [name: 'Gregor Petrin'], + [name: 'Tom Potts'], + [name: 'Sabst'], + [name: 'Alexey Zhokov'], + [name: 'Jakub Glapa'], + [name: 'James Hardwick'], + [name: 'John Keith'], + [name: 'Eric Helgeson'], + [name: 'albertop19'], + [name: 'Andrey T'], + [name: 'Mattias Reichel'], + ] + def issueManagement = [ + system: 'GITHUB', + url: 'https://github.com/gpc/grails-postgresql-extensions/issues' + ] + def scm = [ + url: 'https://github.com/gpc/grails-postgresql-extensions' + ] +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy new file mode 100644 index 0000000..da4e304 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy @@ -0,0 +1,42 @@ +package gpc.pgext.hibernate + +import groovy.transform.CompileStatic + +import org.hibernate.boot.model.TypeContributions +import org.hibernate.dialect.PostgreSQLDialect +import org.hibernate.service.ServiceRegistry +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl + +import gpc.pgext.hibernate.usertype.ArrayType +import gpc.pgext.hibernate.usertype.HstoreMapType +import gpc.pgext.hibernate.usertype.JsonMapType +import gpc.pgext.hibernate.usertype.JsonbMapType + +@CompileStatic +class PostgresqlExtensionsDialect extends PostgreSQLDialect { + + private static final Map COLUMN_TYPES = [ + (ArrayType.LONG_ARRAY) : '_int8', + (ArrayType.INTEGER_ARRAY) : '_int4', + (ArrayType.ENUM_INTEGER_ARRAY): '_int4', + (ArrayType.STRING_ARRAY) : '_varchar', + (ArrayType.DOUBLE_ARRAY) : '_float8', + (ArrayType.FLOAT_ARRAY) : '_float4', + (ArrayType.UUID_ARRAY) : '_uuid', + (HstoreMapType.SQLTYPE) : 'hstore', + (JsonMapType.SQLTYPE) : 'json', + (JsonbMapType.SQLTYPE) : 'jsonb', + ] as Map + + /** + * Register the column types of the postgresql user types + */ + @Override + void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes(typeContributions, serviceRegistry) + def ddlTypeRegistry = typeContributions.typeConfiguration.ddlTypeRegistry + COLUMN_TYPES.each { Integer sqlType, String typeName -> + ddlTypeRegistry.addDescriptor(new DdlTypeImpl(sqlType, typeName, this)) + } + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy new file mode 100644 index 0000000..daa8baa --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy @@ -0,0 +1,80 @@ +package gpc.pgext.hibernate.criterion + +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.Expression +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Join +import jakarta.persistence.criteria.Path +import jakarta.persistence.criteria.Predicate +import org.hibernate.query.criteria.HibernateCriteriaBuilder + +import org.grails.datastore.mapping.query.Query + +/** + * A GORM criterion that is turned into a JPA predicate by {@link PgCriterionHandlerProvider}. + * + * Hibernate 7 has no Criteria API to extend, so the criteria of this plugin are added to the GORM query + * and rendered as PostgreSQL SQL fragments through {@link PgSqlFunction}. + */ +@CompileStatic +class PgCriterion implements Query.Criterion { + + /** + * Creates the predicate of a criterion + */ + interface PredicateFactory { + + /** + * @param query The query the predicate is created for + * @param root The root (or association join) of the query + * @param cb The criteria builder + * @param property The path of the property of the criterion, or null if it has none + * @return The predicate + */ + Predicate create(AbstractQuery query, From root, HibernateCriteriaBuilder cb, Path property) + } + + /** The property path, with any alias replaced by its association path */ + final String propertyPath + + private final PredicateFactory factory + + PgCriterion(String propertyPath, PredicateFactory factory) { + this.propertyPath = propertyPath + this.factory = factory + } + + Predicate toPredicate(AbstractQuery query, From root, HibernateCriteriaBuilder cb) { + factory.create(query, root, cb, propertyPath ? resolvePath(root, propertyPath) : null) + } + + /** + * Creates a predicate from a PostgreSQL SQL fragment, where each {@code ?} is replaced by the next argument + */ + static Predicate sql(HibernateCriteriaBuilder cb, String sql, Expression... arguments) { + List> functionArguments = [cb.literal(sql)] as List> + functionArguments.addAll(arguments) + cb.isTrue(cb.function(PgSqlFunction.PREDICATE, Boolean, functionArguments as Expression[])) + } + + /** + * Resolves a property path like {@code like.favoriteNumbers}, reusing the joins already on the query + */ + private static Path resolvePath(From root, String propertyPath) { + String[] segments = propertyPath.split(/\./) + From from = root + for (int i = 0; i < segments.length - 1; i++) { + String segment = segments[i] + Join join = from.joins.find { Join j -> j.attribute.name == segment } + from = join ?: from.join(segment) + } + from.get(segments[segments.length - 1]) + } + + @Override + String toString() { + "PgCriterion($propertyPath)" + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterionHandlerProvider.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterionHandlerProvider.groovy new file mode 100644 index 0000000..0c950c6 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterionHandlerProvider.groovy @@ -0,0 +1,34 @@ +package gpc.pgext.hibernate.criterion + +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Predicate +import org.hibernate.query.criteria.HibernateCriteriaBuilder + +import org.grails.datastore.mapping.query.Query +import org.grails.orm.hibernate.query.PredicateGenerator + +/** + * Lets GORM turn a {@link PgCriterion} into a JPA predicate. Registered through + * {@code META-INF/services}. + */ +@CompileStatic +class PgCriterionHandlerProvider implements PredicateGenerator.CriterionHandlerProvider { + + @Override + Class criterionType() { + PgCriterion + } + + @Override + PredicateGenerator.CriterionHandler criterionHandler() { + new PredicateGenerator.CriterionHandler() { + @Override + Predicate handle(AbstractQuery query, From root, HibernateCriteriaBuilder cb, Query.Criterion criterion) { + ((PgCriterion) criterion).toPredicate(query, root, cb) + } + } + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunction.java b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunction.java new file mode 100644 index 0000000..7e491b9 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunction.java @@ -0,0 +1,92 @@ +package gpc.pgext.hibernate.criterion; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Renders a PostgreSQL SQL fragment given as the first (literal) argument. Every {@code ?} in the fragment + * is replaced by the rendering of the next argument, which is either a property path (rendered as its + * column) or a bound parameter. A {@code ?} inside quotes is not a placeholder. + */ +public class PgSqlFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + /** Renders a fragment that is a predicate, used by the criteria */ + public static final String PREDICATE = "pgext_sql_predicate"; + + /** Renders a fragment that is an expression, used by the orders */ + public static final String EXPRESSION = "pgext_sql_expression"; + + private final boolean predicate; + + public PgSqlFunction(String name, BasicTypeReference returnType, boolean predicate, TypeConfiguration typeConfiguration) { + super( + name, + StandardArgumentsValidators.min(1), + StandardFunctionReturnTypeResolvers.invariant(typeConfiguration.getBasicTypeRegistry().resolve(returnType)), + null + ); + this.predicate = predicate; + } + + static PgSqlFunction predicate(TypeConfiguration typeConfiguration) { + return new PgSqlFunction(PREDICATE, StandardBasicTypes.BOOLEAN, true, typeConfiguration); + } + + static PgSqlFunction expression(TypeConfiguration typeConfiguration) { + return new PgSqlFunction(EXPRESSION, StandardBasicTypes.OBJECT_TYPE, false, typeConfiguration); + } + + @Override + public boolean isPredicate() { + return predicate; + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if (!(arguments.get(0) instanceof Literal literal) || !(literal.getLiteralValue() instanceof String sql)) { + throw new IllegalArgumentException("The first argument of " + getName() + " must be the SQL as a literal"); + } + // A predicate is wrapped so it combines safely with other predicates. An order formula is not, + // as it may end with its own direction, like "(data->'name') desc" + if (predicate) { + sqlAppender.append('('); + } + int argument = 1; + boolean quoted = false; + for (int i = 0; i < sql.length(); i++) { + char c = sql.charAt(i); + if (c == '\'') { + quoted = !quoted; + } + if (c == '?' && !quoted) { + if (argument >= arguments.size()) { + throw new IllegalArgumentException("Missing argument for placeholder " + argument + " in: " + sql); + } + arguments.get(argument++).accept(walker); + } else { + sqlAppender.append(c); + } + } + if (argument != arguments.size()) { + throw new IllegalArgumentException("Expected " + (argument - 1) + " arguments, got " + (arguments.size() - 1) + " in: " + sql); + } + if (predicate) { + sqlAppender.append(')'); + } + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunctionContributor.java b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunctionContributor.java new file mode 100644 index 0000000..98fabb0 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgSqlFunctionContributor.java @@ -0,0 +1,18 @@ +package gpc.pgext.hibernate.criterion; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.FunctionContributor; + +/** + * Registers the {@link PgSqlFunction}s used by the criteria and orders. + */ +public class PgSqlFunctionContributor implements FunctionContributor { + + @Override + public void contributeFunctions(FunctionContributions functionContributions) { + var typeConfiguration = functionContributions.getTypeConfiguration(); + var registry = functionContributions.getFunctionRegistry(); + registry.register(PgSqlFunction.PREDICATE, PgSqlFunction.predicate(typeConfiguration)); + registry.register(PgSqlFunction.EXPRESSION, PgSqlFunction.expression(typeConfiguration)); + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy new file mode 100644 index 0000000..d493f51 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy @@ -0,0 +1,17 @@ +package gpc.pgext.hibernate.order + +import groovy.transform.CompileStatic + +@CompileStatic +class OrderByRandom extends OrderBySqlFormula { + + private static final String ORDER_RANDOM = 'random()' + + protected OrderByRandom() { + super(ORDER_RANDOM) + } + + static OrderBySqlFormula byRandom() { + new OrderByRandom() + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy new file mode 100644 index 0000000..7de1cfa --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy @@ -0,0 +1,67 @@ +package gpc.pgext.hibernate.order + +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Order +import jakarta.persistence.criteria.Path +import org.hibernate.query.criteria.HibernateCriteriaBuilder as JpaCriteriaBuilder + +import gpc.pgext.hibernate.criterion.PgCriterion +import gpc.pgext.hibernate.criterion.PgSqlFunction + +/** + * Allows ordering by an SQL formula passed by the user, with {@code order(sqlFormula('...'))} in a criteria. + * It simply appends the sqlFormula passed by the user to the resulting SQL query, + * without any verification. + * + * GORM for Hibernate 7 only orders by properties, so the order is added to the query as a {@link PgCriterion} + * that appends the formula to the orders of the query when the criteria is turned into SQL. + */ +@CompileStatic +class OrderBySqlFormula { + + final String sqlFormula + + /** + * Constructor for Order. + * @param sqlFormula an SQL formula that will be appended to the resulting SQL query + */ + protected OrderBySqlFormula(String sqlFormula) { + this.sqlFormula = sqlFormula + } + + @Override + String toString() { + sqlFormula + } + + /** + * @return The criterion that adds this order to the query + */ + PgCriterion toCriterion() { + String formula = sqlFormula + new PgCriterion(null, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + // Only queries for the entity are ordered, not count queries or other projections + if (query instanceof CriteriaQuery && ((CriteriaQuery) query).resultType == root.javaType) { + def criteriaQuery = (CriteriaQuery) query + List orders = new ArrayList<>(criteriaQuery.orderList) + orders << cb.asc(cb.function(PgSqlFunction.EXPRESSION, Object, cb.literal(formula))) + criteriaQuery.orderBy(orders) + } + cb.conjunction() + } as PgCriterion.PredicateFactory) + } + + /** + * Custom order + * + * @param sqlFormula an SQL formula that will be appended to the resulting SQL query + * @return Order + */ + static OrderBySqlFormula sqlFormula(String sqlFormula) { + new OrderBySqlFormula(sqlFormula) + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy new file mode 100644 index 0000000..9abdc2f --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy @@ -0,0 +1,167 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Path +import jakarta.persistence.criteria.Predicate +import org.hibernate.HibernateException +import org.hibernate.query.criteria.HibernateCriteriaBuilder as JpaCriteriaBuilder + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.mapping.query.api.Criteria + +import gpc.pgext.hibernate.criterion.PgCriterion +import gpc.pgext.hibernate.utils.PgArrayUtils + +import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName + +@CompileStatic +class ArrayCriterias { + + private static final PgArrayUtils.MapFunction MAP_TO_ENUM = new PgArrayUtils.MapFunction() { + @Override + Object map(Object o) { + if (o instanceof Enum) { + return ((Enum) o).ordinal() + } + throw new HibernateException("Unable to cast object $o to Enum") + } + } + + /** + * Creates a "contains in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '@>')) + } + + /** + * Creates a "is contained by in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayIsContainedBy(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<@')) + } + + /** + * Creates a "overlap in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayOverlaps(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '&&')) + } + + /** + * Creates an "is empty array" Criterion based on the specified property name + * @param propertyName The property name + * @return The criteria + */ + static Criteria pgArrayIsEmpty(HibernateCriteriaBuilder self, String propertyName) { + addToCriteria(self, emptinessExpression(calculatePropertyName(self, propertyName), '=')) + } + + /** + * Creates an "is not empty array" Criterion based on the specified property name + * @param propertyName The property name + * @return The criteria + */ + static Criteria pgArrayIsNotEmpty(HibernateCriteriaBuilder self, String propertyName) { + addToCriteria(self, emptinessExpression(calculatePropertyName(self, propertyName), '<>')) + } + + /** + * Creates a "contains in native array" or "is empty native array" Criterion based on the specified property name and value + * If the propertyValue is empty, the 'contains' operator is used and if the propertyValue is not empty, the 'isEmpty' + * operator is used. + * + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayIsEmptyOrContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + def name = calculatePropertyName(self, propertyName) + propertyValue ? + addToCriteria(self, arrayExpression(name, propertyValue, '@>')) : + addToCriteria(self, emptinessExpression(name, '=')) + } + + /** + * Creates a "equals in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '=')) + } + + /** + * Creates a "not equals in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayNotEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<>')) + } + + /** + * Creates a "ilike in native array" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgArrayILike(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + String value = (String) propertyValue + addToCriteria(self, new PgCriterion(calculatePropertyName(self, propertyName), { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, 'text(?) ilike ?', property, cb.value(value)) + } as PgCriterion.PredicateFactory)) + } + + private static PgCriterion arrayExpression(String propertyName, Object value, String op) { + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + Class typeClass = arrayTypeClass(property) + Object[] arrayValue = toArrayOfType(value, typeClass) + sql(cb, "? $op CAST(? as ${PgArrayUtils.getNativeSqlType(typeClass)}[])", property, cb.value(arrayValue)) + } as PgCriterion.PredicateFactory) + } + + private static PgCriterion emptinessExpression(String propertyName, String op) { + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + arrayTypeClass(property) + sql(cb, "? $op '{}'", property) + } as PgCriterion.PredicateFactory) + } + + private static Object[] toArrayOfType(Object value, Class typeClass) { + try { + if (typeClass.isEnum()) { + // Enums are stored by their id, so arrays of enums are converted too + def values = value instanceof Object[] ? ((Object[]) value).toList() : value + return PgArrayUtils.getValueAsArrayOfType(values, Integer, MAP_TO_ENUM) + } + PgArrayUtils.getValueAsArrayOfType(value, typeClass) + } catch (IllegalArgumentException e) { + throw new HibernateException(e.message, e) + } + } + + private static Class arrayTypeClass(Path property) { + Class javaType = property.javaType + if (!javaType?.isArray()) { + throw new HibernateException("Property is not an instance of the postgres type ArrayType. Type is: $javaType") + } + javaType.componentType + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy new file mode 100644 index 0000000..848adb2 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy @@ -0,0 +1,51 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Path +import org.hibernate.query.criteria.HibernateCriteriaBuilder as JpaCriteriaBuilder + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.mapping.query.api.Criteria + +import gpc.pgext.hibernate.criterion.PgCriterion +import gpc.pgext.hibernate.usertype.HstoreHelper + +import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName + +@CompileStatic +class HstoreCriterias { + + static Criteria pgHstoreContainsKey(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, valueFunction(calculatePropertyName(self, propertyName), 'exist(?, ?)', propertyValue)) + } + + static Criteria pgHstoreContains(HibernateCriteriaBuilder self, String propertyName, Map values) { + addToCriteria(self, operatorExpression(calculatePropertyName(self, propertyName), values, '@>')) + } + + static Criteria pgHstoreIsContained(HibernateCriteriaBuilder self, String propertyName, Map values) { + addToCriteria(self, operatorExpression(calculatePropertyName(self, propertyName), values, '<@')) + } + + static Criteria pgHstoreILikeValue(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, valueFunction(calculatePropertyName(self, propertyName), 'text(avals(?)) ilike ?', propertyValue)) + } + + private static PgCriterion valueFunction(String propertyName, String sqlFunction, Object value) { + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, sqlFunction, property, cb.value(String.valueOf(value))) + } as PgCriterion.PredicateFactory) + } + + private static PgCriterion operatorExpression(String propertyName, Map values, String operator) { + String hstore = HstoreHelper.toString(values as Map) + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, "? $operator CAST(? AS hstore)", property, cb.value(hstore)) + } as PgCriterion.PredicateFactory) + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy new file mode 100644 index 0000000..71e0a8c --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy @@ -0,0 +1,83 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import jakarta.persistence.criteria.AbstractQuery +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Path +import org.hibernate.query.criteria.HibernateCriteriaBuilder as JpaCriteriaBuilder + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.mapping.query.api.Criteria + +import gpc.pgext.hibernate.criterion.PgCriterion + +import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName + +@CompileStatic +class JsonCriterias { + + private static final Gson GSON = new GsonBuilder().serializeNulls().create() + + /** + * Creates a "json has field value" Criterion based on the specified property name and value + * @param propertyName The property name (json field) + * @param jsonAttribute The json attribute + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgJsonHasFieldValue(HibernateCriteriaBuilder self, String propertyName, String jsonAttribute, Object propertyValue) { + addToCriteria(self, jsonExpression(calculatePropertyName(self, propertyName), '->>', jsonAttribute, '=', propertyValue)) + } + + /** + * Creates a "json contains another json" Criterion based on the specified property name and value + * @param propertyName The property name (jsonb field) + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgJsonbContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '@>')) + } + + /** + * Creates a "json is contained in another json" Criterion based on the specified property name and value + * @param propertyName The property name (jsonb field) + * @param propertyValue The property value + * @return The criteria + */ + static Criteria pgJsonbIsContained(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '<@')) + } + + /** + * Creates a "json on field value" Criterion based on the specified property name and value + * @param propertyName The property name (json field) + * @param jsonAttribute The json attribute + * @param jsonOp The json operator (->>, #>, ...) + * @param propertyValue The property value + * @param sqlOp The sql operator (=, <>, ilike, ...) + * @return The criteria + */ + static Criteria pgJson(HibernateCriteriaBuilder self, String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object propertyValue) { + addToCriteria(self, jsonExpression(calculatePropertyName(self, propertyName), jsonOp, jsonAttribute, sqlOp, propertyValue)) + } + + private static PgCriterion jsonExpression(String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object value) { + String attribute = jsonAttribute.replace("'", "''") + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, "?$jsonOp'$attribute' $sqlOp ?", property, cb.value(String.valueOf(value))) + } as PgCriterion.PredicateFactory) + } + + private static PgCriterion jsonbOperator(String propertyName, Object value, String op) { + String json = GSON.toJson(value) + new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, "? $op CAST(? AS jsonb)", property, cb.value(json)) + } as PgCriterion.PredicateFactory) + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy new file mode 100644 index 0000000..c7e452e --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy @@ -0,0 +1,23 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.mapping.query.api.Criteria + +import gpc.pgext.hibernate.order.OrderBySqlFormula + +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria + +@CompileStatic +class OrderCriterias { + + /** + * Orders by an SQL formula, created with {@code sqlFormula('...')} or {@code byRandom()} + * @param order The order + * @return The criteria + */ + static Criteria order(HibernateCriteriaBuilder self, OrderBySqlFormula order) { + addToCriteria(self, order.toCriterion()) + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java new file mode 100644 index 0000000..23d45e2 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java @@ -0,0 +1,194 @@ +package gpc.pgext.hibernate.usertype; + +import java.io.Serializable; +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +import org.hibernate.HibernateException; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.usertype.ParameterizedType; +import org.hibernate.usertype.UserType; + +import gpc.pgext.hibernate.utils.PgArrayUtils; + +public class ArrayType implements UserType, ParameterizedType { + + public static final int INTEGER_ARRAY = 90001; + public static final int LONG_ARRAY = 90002; + public static final int STRING_ARRAY = 90003; + public static final int ENUM_INTEGER_ARRAY = 90004; + public static final int FLOAT_ARRAY = 90005; + public static final int DOUBLE_ARRAY = 90006; + public static final int UUID_ARRAY = 90007; + + private static final Map, Integer> CLASS_TO_SQL_CODE = new HashMap<>(); + + static { + CLASS_TO_SQL_CODE.put(Integer.class, INTEGER_ARRAY); + CLASS_TO_SQL_CODE.put(Long.class, LONG_ARRAY); + CLASS_TO_SQL_CODE.put(String.class, STRING_ARRAY); + CLASS_TO_SQL_CODE.put(Float.class, FLOAT_ARRAY); + CLASS_TO_SQL_CODE.put(Double.class, DOUBLE_ARRAY); + CLASS_TO_SQL_CODE.put(UUID.class, UUID_ARRAY); + } + + private Class typeClass; + private BidiEnumMap bidiMap; + + @Override + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return cached; + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return (Serializable) value; + } + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + if (x == y) { + return true; + } + if (x == null || y == null) { + return false; + } + if (x instanceof Object[] && y instanceof Object[]) { + return Arrays.deepEquals((Object[]) x, (Object[]) y); + } + return x.equals(y); + } + + @Override + public int hashCode(Object value) throws HibernateException { + return value == null ? 0 : value.hashCode(); + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + @Override + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return original; + } + + @Override + public void setParameterValues(Properties parameters) { + Object type = parameters.get("type"); + if (type instanceof Class clazz) { + typeClass = clazz; + } else if (type != null) { + try { + typeClass = Class.forName(type.toString(), true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new HibernateException("Unable to load the configured array type " + type, e); + } + } + if (typeClass == null) { + throw new RuntimeException("The user type needs to be configured with the type. None provided"); + } + } + + @Override + @SuppressWarnings("unchecked") + public Class returnedClass() { + return (Class) java.lang.reflect.Array.newInstance(typeClass, 0).getClass(); + } + + @Override + public int getSqlType() { + Integer type = CLASS_TO_SQL_CODE.get(typeClass); + if (type != null) { + return type; + } + if (typeClass.isEnum()) { + return ENUM_INTEGER_ARRAY; + } + throw new RuntimeException("The type " + typeClass + " is not a valid type"); + } + + @Override + public Object nullSafeGet(ResultSet rs, int position, WrapperOptions options) throws SQLException { + Array sqlArray = rs.getArray(position); + if (rs.wasNull() || sqlArray == null) { + return null; + } + Object array = sqlArray.getArray(); + if (typeClass.isEnum()) { + int length = array != null ? java.lang.reflect.Array.getLength(array) : 0; + Object result = java.lang.reflect.Array.newInstance(typeClass, length); + for (int i = 0; i < length; i++) { + java.lang.reflect.Array.set(result, i, idToEnum((Integer) java.lang.reflect.Array.get(array, i))); + } + return result; + } + return returnedClass().cast(array); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, WrapperOptions options) throws SQLException { + if (value == null) { + st.setNull(index, Types.ARRAY); + return; + } + + Object[] valueToSet = (Object[]) value; + Class typeArrayClass = returnedClass(); + + if (typeClass.isEnum()) { + typeArrayClass = Integer[].class; + Integer[] converted = new Integer[valueToSet.length]; + for (int i = 0; i < valueToSet.length; i++) { + if (valueToSet[i] instanceof Integer) { + converted[i] = (Integer) valueToSet[i]; + } else { + converted[i] = enumToId(valueToSet[i]); + } + } + valueToSet = converted; + } + + Array array = st.getConnection().createArrayOf(PgArrayUtils.getNativeSqlType(typeClass), (Object[]) typeArrayClass.cast(valueToSet)); + st.setArray(index, array); + } + + public Class getTypeClass() { + return typeClass; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void ensureBidiMapInitialized() throws HibernateException { + try { + if (bidiMap == null) { + bidiMap = new BidiEnumMap((Class) typeClass); + } + } catch (Exception e) { + throw new HibernateException("Unable to create bidirectional enum map for " + typeClass, e); + } + } + + private Object idToEnum(int id) throws HibernateException { + ensureBidiMapInitialized(); + return bidiMap.getEnumValue(id); + } + + private int enumToId(Object enumValue) throws HibernateException { + ensureBidiMapInitialized(); + return bidiMap.getKey(enumValue); + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy new file mode 100644 index 0000000..ce5d99c --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy @@ -0,0 +1,80 @@ +package gpc.pgext.hibernate.usertype + +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.Types + +import groovy.transform.CompileStatic + +import org.hibernate.HibernateException +import org.hibernate.type.descriptor.WrapperOptions +import org.hibernate.usertype.UserType + +@CompileStatic +class HstoreMapType implements UserType { + + static int SQLTYPE = 90011 + + @Override + int getSqlType() { + SQLTYPE + } + + @Override + Class returnedClass() { + Map + } + + @Override + boolean equals(Map x, Map y) throws HibernateException { + if (x == null) { + return y == null + } + x == y + } + + @Override + int hashCode(Map x) throws HibernateException { + x ? x.hashCode() : 0 + } + + @Override + Map nullSafeGet(ResultSet rs, int position, WrapperOptions options) throws SQLException { + HstoreHelper.toMap(rs.getString(position)) + } + + @Override + void nullSafeSet(PreparedStatement ps, Map value, int index, WrapperOptions options) throws SQLException { + ps.setObject( + index, + HstoreHelper.toString(value), + Types.OTHER + ) + } + + @Override + Map deepCopy(Map value) throws HibernateException { + value == null ? null : new HashMap(value) + } + + @Override + boolean isMutable() { + true + } + + @Override + Serializable disassemble(Map value) throws HibernateException { + value as Serializable + } + + @Override + Map assemble(Serializable cached, Object owner) throws HibernateException { + cached as Map + } + + @Override + Map replace(Map original, Map target, Object owner) throws HibernateException { + original + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy new file mode 100644 index 0000000..1ad0dc8 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy @@ -0,0 +1,86 @@ +package gpc.pgext.hibernate.usertype + +import java.lang.reflect.Type +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.Types + +import groovy.transform.CompileStatic + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.hibernate.HibernateException +import org.hibernate.type.descriptor.WrapperOptions +import org.hibernate.usertype.UserType +import org.postgresql.util.PGobject + +@CompileStatic +class JsonMapType implements UserType { + + static int SQLTYPE = 90021 + + private final Type userType = Map + private final Gson gson = new GsonBuilder().serializeNulls().create() + + @Override + int getSqlType() { + SQLTYPE + } + + @Override + Class returnedClass() { + Map + } + + @Override + boolean equals(Map x, Map y) throws HibernateException { + Objects.equals(x, y) + } + + @Override + int hashCode(Map x) throws HibernateException { + x ? x.hashCode() : 0 + } + + @Override + Map nullSafeGet(ResultSet rs, int position, WrapperOptions options) throws SQLException { + PGobject o = rs.getObject(position) as PGobject + String jsonString = o?.value + gson.fromJson(jsonString, userType) as Map + } + + @Override + void nullSafeSet(PreparedStatement st, Map value, int index, WrapperOptions options) throws SQLException { + if (value == null) { + st.setNull(index, Types.OTHER) + } else { + st.setObject(index, gson.toJson(value, userType), Types.OTHER) + } + } + + @Override + Map deepCopy(Map value) throws HibernateException { + value == null ? null : new HashMap(value) + } + + @Override + boolean isMutable() { + true + } + + @Override + Serializable disassemble(Map value) throws HibernateException { + gson.toJson(value, userType) + } + + @Override + Map assemble(Serializable cached, Object owner) throws HibernateException { + gson.fromJson((String) cached, userType) as Map + } + + @Override + Map replace(Map original, Map target, Object owner) throws HibernateException { + original + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy new file mode 100644 index 0000000..a2fcd8a --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy @@ -0,0 +1,14 @@ +package gpc.pgext.hibernate.usertype + +import groovy.transform.CompileStatic + +@CompileStatic +class JsonbMapType extends JsonMapType { + + static int SQLTYPE = 90022 + + @Override + int getSqlType() { + SQLTYPE + } +} diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy new file mode 100644 index 0000000..f9e6bb8 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy @@ -0,0 +1,54 @@ +package gpc.pgext.hibernate.utils + +import groovy.transform.CompileStatic + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria +import org.grails.datastore.mapping.query.Query +import org.grails.datastore.mapping.query.api.Criteria + +import gpc.pgext.hibernate.criterion.PgCriterion + +@CompileStatic +class CriteriaUtils { + + /** + * Adds the criterion to the query of the builder + */ + static Criteria addToCriteria(HibernateCriteriaBuilder self, PgCriterion criterion) { + self.hibernateQuery.add(criterion) + self + } + + /** + * Replaces an alias created with {@code createAlias} at the start of the property name by its association path, + * so {@code l.favoriteNumbers} becomes {@code like.favoriteNumbers} + */ + static String calculatePropertyName(HibernateCriteriaBuilder self, String propertyName) { + int dot = propertyName.indexOf('.') + if (dot < 0) { + return propertyName + } + String alias = propertyName.substring(0, dot) + String associationPath = self.hibernateQuery.aliases.find { it.alias() == alias }?.path() ?: + findAssociationPath(self.hibernateQuery.detachedCriteria.criteria, alias) + associationPath ? associationPath + propertyName.substring(dot) : propertyName + } + + private static String findAssociationPath(List criteria, String alias) { + for (Query.Criterion criterion : criteria) { + if (criterion instanceof DetachedAssociationCriteria) { + def associationCriteria = (DetachedAssociationCriteria) criterion + if (associationCriteria.alias == alias) { + return associationCriteria.associationPath + } + } else if (criterion instanceof Query.Junction) { + def associationPath = findAssociationPath(((Query.Junction) criterion).criteria, alias) + if (associationPath) { + return associationPath + } + } + } + null + } +} diff --git a/plugin-hibernate7/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule b/plugin-hibernate7/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 0000000..867060d --- /dev/null +++ b/plugin-hibernate7/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,7 @@ +moduleName=grails-postgresql-extensions-hibernate7 +moduleVersion=1.0 +extensionClasses=\ + gpc.pgext.hibernate.postgresql.criteria.ArrayCriterias,\ + gpc.pgext.hibernate.postgresql.criteria.HstoreCriterias,\ + gpc.pgext.hibernate.postgresql.criteria.JsonCriterias,\ + gpc.pgext.hibernate.postgresql.criteria.OrderCriterias diff --git a/plugin-hibernate7/src/main/resources/META-INF/services/org.grails.orm.hibernate.query.PredicateGenerator$CriterionHandlerProvider b/plugin-hibernate7/src/main/resources/META-INF/services/org.grails.orm.hibernate.query.PredicateGenerator$CriterionHandlerProvider new file mode 100644 index 0000000..20c6cb7 --- /dev/null +++ b/plugin-hibernate7/src/main/resources/META-INF/services/org.grails.orm.hibernate.query.PredicateGenerator$CriterionHandlerProvider @@ -0,0 +1 @@ +gpc.pgext.hibernate.criterion.PgCriterionHandlerProvider diff --git a/plugin-hibernate7/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor b/plugin-hibernate7/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor new file mode 100644 index 0000000..06a7524 --- /dev/null +++ b/plugin-hibernate7/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor @@ -0,0 +1 @@ +gpc.pgext.hibernate.criterion.PgSqlFunctionContributor diff --git a/settings.gradle b/settings.gradle index 9f039b5..491a308 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,5 +39,8 @@ findProject(':plugin-core').name = 'grails-postgresql-extensions-core' include('plugin-hibernate5') findProject(':plugin-hibernate5').name = 'grails-postgresql-extensions' +include('plugin-hibernate7') +findProject(':plugin-hibernate7').name = 'grails-postgresql-extensions-hibernate7' + include('test-app1') project(':test-app1').projectDir = new File(settingsDir, 'test-apps/app1') From a276e847bd1bd3fe75a92f65e94050f9c97c477c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:58:45 +0200 Subject: [PATCH 06/11] test: add test-app2 for the Hibernate 7 plugin A copy of test-app1 that uses grails-data-hibernate7 and the Hibernate 7 plugin, with the same domain classes, services and integration specs. It maps ids with the 'sequence' generator, as GORM 8.0.0 does not create the sequences of the default 'native' generator on PostgreSQL. --- settings.gradle | 3 + test-apps/app2/build.gradle | 21 ++ .../app2/grails-app/conf/application.groovy | 5 + .../app2/grails-app/conf/application.yml | 15 + .../app2/grails-app/conf/logback-spring.xml | 25 ++ .../grails-app/conf/spring/resources.groovy | 3 + .../domain/app/array/TestDouble.groovy | 22 ++ .../domain/app/array/TestEnum.groovy | 36 ++ .../domain/app/array/TestFloat.groovy | 22 ++ .../domain/app/array/TestInteger.groovy | 22 ++ .../domain/app/array/TestLong.groovy | 22 ++ .../domain/app/array/TestString.groovy | 22 ++ .../domain/app/array/TestUuid.groovy | 22 ++ .../domain/app/criteria/array/Like.groovy | 50 +++ .../domain/app/criteria/array/User.groovy | 18 + .../domain/app/hstore/TestHstoreMap.groovy | 26 ++ .../domain/app/json/TestMapJson.groovy | 22 ++ .../domain/app/json/TestMapJsonb.groovy | 22 ++ .../grails-app/init/app/Application.groovy | 13 + .../array/PgArrayTestSearchService.groovy | 52 +++ .../hstore/PgHstoreTestSearchService.groovy | 12 + .../json/PgJsonTestSearchService.groovy | 18 + .../json/PgJsonbTestSearchService.groovy | 24 ++ .../services/app/order/PgOrderService.groovy | 21 ++ ...sCriteriaTestServiceIntegrationSpec.groovy | 348 ++++++++++++++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 328 +++++++++++++++++ ...eCriteriaTestServiceIntegrationSpec.groovy | 47 +++ ...yCriteriaTestServiceIntegrationSpec.groovy | 344 +++++++++++++++++ ...yCriteriaTestServiceIntegrationSpec.groovy | 130 +++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 212 +++++++++++ ...yCriteriaTestServiceIntegrationSpec.groovy | 129 +++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 310 ++++++++++++++++ ...sCriteriaTestServiceIntegrationSpec.groovy | 303 +++++++++++++++ ...stgresqlArraysDomainIntegrationSpec.groovy | 141 +++++++ .../groovy/test/array/UuidBuilder.groovy | 15 + .../PgHstoreContainsIntegrationSpec.groovy | 115 ++++++ .../PgHstoreContainsKeyIntegrationSpec.groovy | 54 +++ ...reILikeValueFunctionIntegrationSpec.groovy | 54 +++ .../PgHstoreIsContainedIntegrationSpec.groovy | 81 ++++ ...resqlHstoreMapDomainIntegrationSpec.groovy | 138 +++++++ .../json/PgJsonEqualsIntegrationSpec.groovy | 43 +++ .../json/PgJsonPathsIntegrationSpec.groovy | 61 +++ .../json/PgJsonValuesIntegrationSpec.groovy | 79 ++++ .../PgJsonbContainsIntegrationSpec.groovy | 107 ++++++ .../json/PgJsonbEqualsIntegrationSpec.groovy | 39 ++ .../PgJsonbIsContainedIntegrationSpec.groovy | 90 +++++ .../json/PgJsonbPathsIntegrationSpec.groovy | 61 +++ .../json/PgJsonbValuesIntegrationSpec.groovy | 79 ++++ ...tgresqlJsonMapDomainIntegrationSpec.groovy | 57 +++ ...gresqlJsonbMapDomainIntegrationSpec.groovy | 57 +++ .../test/order/PgOrderIntegrationSpec.groovy | 46 +++ 51 files changed, 3986 insertions(+) create mode 100644 test-apps/app2/build.gradle create mode 100644 test-apps/app2/grails-app/conf/application.groovy create mode 100644 test-apps/app2/grails-app/conf/application.yml create mode 100644 test-apps/app2/grails-app/conf/logback-spring.xml create mode 100644 test-apps/app2/grails-app/conf/spring/resources.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestDouble.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestEnum.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestFloat.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestInteger.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestLong.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestString.groovy create mode 100644 test-apps/app2/grails-app/domain/app/array/TestUuid.groovy create mode 100644 test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy create mode 100644 test-apps/app2/grails-app/domain/app/criteria/array/User.groovy create mode 100644 test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy create mode 100644 test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy create mode 100644 test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy create mode 100644 test-apps/app2/grails-app/init/app/Application.groovy create mode 100644 test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy create mode 100644 test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy create mode 100644 test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy create mode 100644 test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy create mode 100644 test-apps/app2/grails-app/services/app/order/PgOrderService.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy create mode 100644 test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy diff --git a/settings.gradle b/settings.gradle index 491a308..d901650 100644 --- a/settings.gradle +++ b/settings.gradle @@ -44,3 +44,6 @@ findProject(':plugin-hibernate7').name = 'grails-postgresql-extensions-hibernate include('test-app1') project(':test-app1').projectDir = new File(settingsDir, 'test-apps/app1') + +include('test-app2') +project(':test-app2').projectDir = new File(settingsDir, 'test-apps/app2') diff --git a/test-apps/app2/build.gradle b/test-apps/app2/build.gradle new file mode 100644 index 0000000..a7d9a40 --- /dev/null +++ b/test-apps/app2/build.gradle @@ -0,0 +1,21 @@ +version = projectVersion +group = 'test' + +apply plugin: 'org.apache.grails.gradle.grails-web' + +dependencies { + + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + + implementation project(':grails-postgresql-extensions-hibernate7') + implementation 'org.apache.grails:grails-data-hibernate7' + implementation 'org.apache.grails:grails-dependencies-starter-web' + + integrationTestImplementation 'org.apache.grails:grails-dependencies-test' +} + +compileJava.options.release = javaVersion.toInteger() + +apply { + from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') +} diff --git a/test-apps/app2/grails-app/conf/application.groovy b/test-apps/app2/grails-app/conf/application.groovy new file mode 100644 index 0000000..f73cb96 --- /dev/null +++ b/test-apps/app2/grails-app/conf/application.groovy @@ -0,0 +1,5 @@ +// GORM 8.0.0 for Hibernate 7 never creates the sequence of the default 'native' generator on PostgreSQL +// (GrailsNativeGenerator swallows the initialization of its SequenceStyleGenerator delegate), so use 'sequence' +grails.gorm.default.mapping = { + id generator: 'sequence' +} diff --git a/test-apps/app2/grails-app/conf/application.yml b/test-apps/app2/grails-app/conf/application.yml new file mode 100644 index 0000000..466169f --- /dev/null +++ b/test-apps/app2/grails-app/conf/application.yml @@ -0,0 +1,15 @@ +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +dataSource: + pooled: true + jmxExport: true + driverClassName: org.postgresql.Driver + username: postgres_extensions + password: postgres_extensions + url: 'jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/pg_extensions_test' + dbCreate: create-drop +hibernate: + dialect: gpc.pgext.hibernate.PostgresqlExtensionsDialect diff --git a/test-apps/app2/grails-app/conf/logback-spring.xml b/test-apps/app2/grails-app/conf/logback-spring.xml new file mode 100644 index 0000000..e3b8d89 --- /dev/null +++ b/test-apps/app2/grails-app/conf/logback-spring.xml @@ -0,0 +1,25 @@ + + + + + + + false + + ${CONSOLE_LOG_THRESHOLD} + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + + + + + + + \ No newline at end of file diff --git a/test-apps/app2/grails-app/conf/spring/resources.groovy b/test-apps/app2/grails-app/conf/spring/resources.groovy new file mode 100644 index 0000000..fa95006 --- /dev/null +++ b/test-apps/app2/grails-app/conf/spring/resources.groovy @@ -0,0 +1,3 @@ +// Place your Spring DSL code here +beans = { +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy b/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy new file mode 100644 index 0000000..2f06ed8 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestDouble { + + Double[] doubleNumbers + + static mapping = { + doubleNumbers(type: ArrayType, params: [type: Double]) + } + + static constraints = { + doubleNumbers(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy b/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy new file mode 100644 index 0000000..8c9c259 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy @@ -0,0 +1,36 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestEnum { + + static enum Day { + MONDAY(0), + TUESDAY(1), + WEDNESDAY(2), + THURSDAY(3), + FRIDAY(4), + SATURDAY(5), + SUNDAY(6) + + final int id + + Day(int id) { this.id = id } + } + + Day[] days + + static mapping = { + days(type: ArrayType, params: [type: Day]) + } + + static constraints = { + days(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy b/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy new file mode 100644 index 0000000..2a6f488 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestFloat { + + Float[] floatNumbers + + static mapping = { + floatNumbers(type: ArrayType, params: [type: Float]) + } + + static constraints = { + floatNumbers(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy b/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy new file mode 100644 index 0000000..8b27811 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestInteger { + + Integer[] integerNumbers + + static mapping = { + integerNumbers(type: ArrayType, params: [type: Integer]) + } + + static constraints = { + integerNumbers(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestLong.groovy b/test-apps/app2/grails-app/domain/app/array/TestLong.groovy new file mode 100644 index 0000000..e8af71f --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestLong.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestLong { + + Long[] longNumbers + + static mapping = { + longNumbers(type: ArrayType, params: [type: Long]) + } + + static constraints = { + longNumbers(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestString.groovy b/test-apps/app2/grails-app/domain/app/array/TestString.groovy new file mode 100644 index 0000000..f4100bb --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestString.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestString { + + String[] stringArray + + static mapping = { + stringArray(type: ArrayType, params: [type: String]) + } + + static constraints = { + stringArray(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy b/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy new file mode 100644 index 0000000..1d3974c --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy @@ -0,0 +1,22 @@ +package app.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class TestUuid { + + UUID[] uuidArray + + static mapping = { + uuidArray(type: ArrayType, params: [type: UUID]) + } + + static constraints = { + uuidArray(nullable: true) + } +} diff --git a/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy b/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy new file mode 100644 index 0000000..0dc7d9d --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy @@ -0,0 +1,50 @@ +package app.criteria.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType + +@ToString +@GrailsCompileStatic +class Like { + + static belongsTo = User + + Integer[] favoriteNumbers = [] + Long[] favoriteLongNumbers = [] + String[] favoriteMovies = [] + Juice[] favoriteJuices = [] + Double[] favoriteDoubleNumbers = [] + Float[] favoriteFloatNumbers = [] + UUID[] favoriteMovieUUIDs = [] + + static enum Juice { + ORANGE(0), + APPLE(1), + GRAPE(2), + PINEAPPLE(3), + TOMATO(4), + GRAPEFRUIT(5), + CRANBERRY(6), + CARROT(7), + LEMON(8) + + private final int id + + Juice(int id) { this.id = id } + } + + static mapping = { + table('pg_extensions_like') + favoriteNumbers(type: ArrayType, params: [type: Integer]) + favoriteMovies(type: ArrayType, params: [type: String]) + favoriteLongNumbers(type: ArrayType, params: [type: Long]) + favoriteJuices(type: ArrayType, params: [type: Juice]) + favoriteFloatNumbers(type: ArrayType, params: [type: Float]) + favoriteDoubleNumbers(type: ArrayType, params: [type: Double]) + favoriteMovieUUIDs(type: ArrayType, params: [type: UUID]) + } + +} diff --git a/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy b/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy new file mode 100644 index 0000000..f9fba91 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy @@ -0,0 +1,18 @@ +package app.criteria.array + +import grails.compiler.GrailsCompileStatic + +@GrailsCompileStatic +class User { + + String name + Like like + + static mapping = { + table('pg_extensions_user') + } + + String toString() { + name + } +} diff --git a/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy b/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy new file mode 100644 index 0000000..fd24582 --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy @@ -0,0 +1,26 @@ +package app.hstore + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.HstoreMapType + +@ToString +@GrailsCompileStatic +class TestHstoreMap { + + String name + Integer luckyNumber + + Map testAttributes + + static constraints = { + name(nullable: true) + luckyNumber(nullable: true) + } + + static mapping = { + testAttributes(type: HstoreMapType) + } +} diff --git a/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy b/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy new file mode 100644 index 0000000..828cb7b --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy @@ -0,0 +1,22 @@ +package app.json + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.JsonMapType + +@ToString +@GrailsCompileStatic +class TestMapJson { + + Map data + + static constraints = { + data(nullable: true) + } + + static mapping = { + data(type: JsonMapType) + } +} diff --git a/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy b/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy new file mode 100644 index 0000000..5f40e4c --- /dev/null +++ b/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy @@ -0,0 +1,22 @@ +package app.json + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.JsonbMapType + +@ToString +@GrailsCompileStatic +class TestMapJsonb { + + Map data + + static constraints = { + data(nullable: true) + } + + static mapping = { + data(type: JsonbMapType) + } +} diff --git a/test-apps/app2/grails-app/init/app/Application.groovy b/test-apps/app2/grails-app/init/app/Application.groovy new file mode 100644 index 0000000..0931f30 --- /dev/null +++ b/test-apps/app2/grails-app/init/app/Application.groovy @@ -0,0 +1,13 @@ +package app + +import groovy.transform.CompileStatic + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration + +@CompileStatic +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} \ No newline at end of file diff --git a/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy new file mode 100644 index 0000000..c0357ea --- /dev/null +++ b/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy @@ -0,0 +1,52 @@ +package app.criteria.array + +class PgArrayTestSearchService { + + List search(String field, String criteriaName, Object value) { + Like.withCriteria { + "$criteriaName"(field, value) + } as List + } + + List search(String field, String criteriaName) { + Like.withCriteria { + "$criteriaName"(field) + } as List + } + + List searchWithJoin(String field, String criteriaName, Object value) { + User.withCriteria { + createAlias('like', 'l') + "$criteriaName"('l.' + field, value) + } as List + } + + List searchWithJoin(String field, String criteriaName) { + User.withCriteria { + createAlias('like', 'l') + "$criteriaName"('l.' + field) + } as List + } + + List searchWithJoinByStringOrInteger(Map params, String criteriaName) { + User.withCriteria { + createAlias('like', 'l') + or { + params.each { k, v -> + "$criteriaName"('l.' + k, v) + } + } + } as List + } + + List searchWithJoinAnd(Map params, String criteriaName) { + User.withCriteria { + createAlias('like', 'l') + and { + params.each { k, v -> + "$criteriaName"('l.' + k, v) + } + } + } as List + } +} diff --git a/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy new file mode 100644 index 0000000..b4961cb --- /dev/null +++ b/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy @@ -0,0 +1,12 @@ +package app.criteria.hstore + +import app.hstore.TestHstoreMap + +class PgHstoreTestSearchService { + + List search(String field, String criteriaName, Object value) { + TestHstoreMap.withCriteria { + "$criteriaName"(field, value) + } as List + } +} diff --git a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy new file mode 100644 index 0000000..e6ee0bd --- /dev/null +++ b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy @@ -0,0 +1,18 @@ +package app.criteria.json + +import app.json.TestMapJson + +class PgJsonTestSearchService { + + List search(String criteriaName, String field, String jsonAttribute, Object value) { + TestMapJson.withCriteria { + "$criteriaName"(field, jsonAttribute, value.toString()) + } as List + } + + List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, Object value) { + TestMapJson.withCriteria { + "$criteriaName"(field, jsonOp, jsonAttribute, sqlOp, value.toString()) + } as List + } +} diff --git a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy new file mode 100644 index 0000000..c323b57 --- /dev/null +++ b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy @@ -0,0 +1,24 @@ +package app.criteria.json + +import app.json.TestMapJsonb + +class PgJsonbTestSearchService { + + List search(String criteriaName, String field, Object value) { + TestMapJsonb.withCriteria { + "$criteriaName"(field, value) + } as List + } + + List search(String criteriaName, String field, String jsonAttribute, Object value) { + TestMapJsonb.withCriteria { + "$criteriaName"(field, jsonAttribute, value.toString()) + } as List + } + + List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, Object value) { + TestMapJsonb.withCriteria { + "$criteriaName"(field, jsonOp, jsonAttribute, sqlOp, value.toString()) + } as List + } +} diff --git a/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy b/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy new file mode 100644 index 0000000..8b28213 --- /dev/null +++ b/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy @@ -0,0 +1,21 @@ +package app.order + +import app.json.TestMapJsonb + +import static gpc.pgext.hibernate.order.OrderByRandom.byRandom +import static gpc.pgext.hibernate.order.OrderBySqlFormula.sqlFormula + +class PgOrderService { + + List orderByJson() { + TestMapJsonb.withCriteria { + order(sqlFormula('(data->\'name\') desc')) + } as List + } + + List orderByRandom() { + TestMapJsonb.withCriteria { + order(byRandom()) + } as List + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..d6f5881 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,348 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import org.hibernate.HibernateException +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'search #number in an array of integers'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [5, 17, 9, 6, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 3 | 2 + 17 | 1 + 9 | 2 + 4 | 2 + 1 | 0 + 20 | 4 + [3, 4] | 1 + [3, 4, 7] | 0 + [4] | 2 + [3, 20] | 2 + [] | 4 + 3 as Integer[] | 2 + [] as Integer[] | 4 + [3, 4] as Integer[] | 1 + } + + @Unroll + void 'search #number in an array of longs'() { + setup: + new Like(favoriteLongNumbers: [12383L, 2392348L, 3498239L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [12383L, 98978L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [-983893849L, 398432423L, 98978L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [12383L]).save(flush: true, failOnError: true) + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12383L | 3 + 98978L | 2 + -983893849L | 1 + 48574L | 0 + [12383L, 98978L] | 1 + [12383L] | 3 + [] | 4 + [12383L, 98978L] as Long[] | 1 + [12383L] as Long[] | 3 + [] as Long[] | 4 + } + + @Unroll + void 'search #number in an array of floats'() { + setup: + new Like(favoriteFloatNumbers: [12383f, 2392348f, 3498239f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12383f, 98978f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [-983893849f, 398432423f, 98978f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12383f]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12383f | 3 + 98978f | 2 + -983893849f | 1 + 48574f | 0 + [12383f, 98978f] | 1 + [12383f] | 3 + [] | 4 + [12383f, 98978f] as Float[] | 1 + [12383f] as Float[] | 3 + [] as Float[] | 4 + } + + @Unroll + void 'search #number in an array of double'() { + setup: + new Like(favoriteDoubleNumbers: [12383d, 2392348d, 3498239d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12383d, 98978d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [-983893849d, 398432423d, 98978d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12383d]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12383d | 3 + 98978d | 2 + -983893849d | 1 + 48574d | 0 + [12383d, 98978d] | 1 + [12383d] | 3 + [] | 4 + [12383d, 98978d] as Double[] | 1 + [12383d] as Double[] | 3 + [] as Double[] | 4 + } + + @Unroll + void 'search #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayContains', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + 'The Matrix' | 1 + 'The Lord of the Rings' | 2 + 'Blade Runner' | 2 + 'Star Wars' | 2 + 'The Usual Suspects' | 0 + ['Star Wars', 'Romeo & Juliet'] | 1 + ['The Lord of the Rings'] | 2 + [] | 4 + ['Star Wars', 'Romeo & Juliet'] as String[] | 1 + ['The Lord of the Rings'] as String[] | 2 + [] as String[] | 4 + } + + @Unroll + void 'search #movie in an array of UUIDs'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayContains', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + UuidBuilder.createUUID('The Matrix') | 1 + UuidBuilder.createUUID('The Lord of the Rings') | 2 + UuidBuilder.createUUID('Blade Runner') | 2 + UuidBuilder.createUUID('Star Wars') | 2 + UuidBuilder.createUUID('The Usual Suspects') | 0 + UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) | 1 + UuidBuilder.createUUIDs(['The Lord of the Rings']) | 2 + [] | 4 + UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) as UUID[] | 1 + UuidBuilder.createUUIDs(['The Lord of the Rings']) as UUID[] | 2 + [] as UUID[] | 4 + } + + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayContains', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 1 + Like.Juice.ORANGE | 2 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 2 + Like.Juice.PINEAPPLE | 1 + Like.Juice.TOMATO | 2 + Like.Juice.CARROT | 3 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 1 + [Like.Juice.CARROT] | 3 + [Like.Juice.CARROT, Like.Juice.TOMATO] | 2 + [] | 4 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayContains', movie) + + then: + result.size() == 2 + result.contains(user2) + result.contains(user3) + + where: + movie = 'Star Wars' + } + + void 'search in an array of strings with join with another domain class and or statement'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayContains', favoriteMovies: movie, favoriteNumbers: number) + + then: + result.size() == 3 + result.contains(user2) + result.contains(user3) + result.contains(user4) + + where: + movie = "Star Wars" + number = 4 + } + + void 'search an invalid list inside the array of integers'() { + when: + pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayContains', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] + } + + void 'search an invalid list inside the array of long'() { + when: + pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayContains', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] + } + + void 'search an invalid list inside the array of float'() { + when: + pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayContains', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] + } + + void 'search an invalid list inside the array of double'() { + when: + pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayContains', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] + } + + void 'search an invalid list inside the array of string'() { + when: + pgArrayTestSearchService.search('favoriteMovies', 'pgArrayContains', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] + } + + void 'search an invalid list inside the array of UUID'() { + when: + pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayContains', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + } + + void 'search an invalid list inside the array of enum'() { + when: + pgArrayTestSearchService.search('favoriteJuices', 'pgArrayContains', juice) + + then: + thrown HibernateException + + where: + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..b71b070 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,328 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import org.hibernate.HibernateException +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'check equals for #number in an array of integers'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [17]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 3 | 0 + 17 | 1 + [3, 7, 20] | 1 + [3, 4, 20] | 1 + [4, 20, 3] | 0 + [] | 1 + 17 as Integer[] | 1 + [] as Integer[] | 1 + [3, 4, 20] as Integer[] | 1 + } + + @Unroll + void 'check equals for #number in an array of longs'() { + setup: + new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [-9L, 16L, 7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1L | 1 + 7L | 1 + [-9L, 16L, 7L] | 1 + [16L, -9L, 7L] | 0 + [1L] | 1 + [] | 1 + 7L as Long[] | 1 + } + + @Unroll + void 'check equals for #number in an array of floats'() { + setup: + new Like(favoriteFloatNumbers: [12f, 23f, 34f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12f, 98f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [-98f, 39f, 97]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12f | 1 + [12f, 98f] | 1 + [98f, 12f] | 0 + [12f] | 1 + [] | 1 + [] as Float[] | 1 + [12f, 98f] as Float[] | 1 + } + + @Unroll + void 'check equals for #number in an array of doubles'() { + setup: + new Like(favoriteDoubleNumbers: [12d, 23d, 34d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12d, 98d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [-98d, 39d, 97d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12d | 1 + [12d] | 1 + [12d, 98d] | 1 + [] | 1 + [39d, 97d, -98d] | 0 + [12d] as Double[] | 1 + [] as Double[] | 1 + } + + @Unroll + void 'check equals for #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayEquals', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + 'Star Wars' | 1 + ['Star Wars'] | 1 + ['Star Wars'] as String[] | 1 + 'The Usual Suspects' | 0 + ['Spiderman', 'Blade Runner', 'Star Wars'] | 1 + [] | 1 + ['The Matrix', 'The Lord of the Rings'] as String[] | 1 + ['The Lord of the Rings', 'The Matrix'] as String[] | 0 + [] as String[] | 1 + } + + @Unroll + void 'check equals for #movie in an array of UUIDs'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayEquals', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + UuidBuilder.createUUID('Star Wars') | 1 + UuidBuilder.createUUIDs(['Star Wars']) | 1 + UuidBuilder.createUUIDs(['Star Wars']) as UUID[] | 1 + UuidBuilder.createUUID('The Usual Suspects') | 0 + UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars']) | 1 + [] | 1 + UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings']) as UUID[] | 1 + UuidBuilder.createUUIDs(['The Lord of the Rings', 'The Matrix']) as UUID[] | 0 + [] as UUID[] | 1 + } + + @Unroll + void 'check equals for #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayEquals', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.PINEAPPLE | 1 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 + [] | 1 + [Like.Juice.GRAPE, Like.Juice.ORANGE] | 0 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayEquals', movie) + + then: + result.size() == 2 + result.contains(user2) + result.contains(user3) + + where: + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] + } + + void 'search in an array of strings with join with another domain class and or statement'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayEquals', favoriteMovies: movie, favoriteNumbers: number) + + then: + result.size() == 3 + result.contains(user2) + result.contains(user3) + result.contains(user4) + + where: + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] + number = [9, 4] + } + + void 'search an invalid list inside the array of integers'() { + when: + pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] + } + + void 'search an invalid list inside the array of long'() { + when: + pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] + } + + void 'search an invalid list inside the array of float'() { + when: + pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] + } + + void 'search an invalid list inside the array of double'() { + when: + pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] + } + + void 'search an invalid list inside the array of string'() { + when: + pgArrayTestSearchService.search('favoriteMovies', 'pgArrayEquals', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] + } + + void 'search an invalid list inside the array of UUID'() { + when: + pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayEquals', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + } + + void 'search an invalid list inside the array of enum'() { + when: + pgArrayTestSearchService.search('favoriteJuices', 'pgArrayEquals', juice) + + then: + thrown HibernateException + + where: + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..1b19c85 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,47 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgILikeCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'check ilike for #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayILike', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + '%tar War%' | 2 + '%ider%' | 1 + '%Suspects%' | 0 + '' | 0 + '%' | 5 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..8ce46c3 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,344 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import org.hibernate.HibernateException +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'search #number in an array of integers'() { + setup: + new Like(favoriteNumbers: [1, 2, 3, 4, 5]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [4, 5, 6, 7]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [4, 5]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [1, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [2]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsContainedBy', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1 | 0 + 2 | 1 + [1, 20] | 1 + [4, 5, 6, 7] | 2 + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | 4 + [] | 0 + [1, 20] as Integer[] | 1 + [4, 5, 6, 7] as Integer[] | 2 + [] as Integer[] | 0 + } + + + @Unroll + void 'search #number in an array of long'() { + setup: + new Like(favoriteLongNumbers: [1L, 2L, 3L, 4L, 5L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [4L, 5L, 6L, 7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [4L, 5L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [1L, 20L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [2L]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsContainedBy', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1L | 0 + 2L | 1 + [1L, 20L] | 1 + [4L, 5L, 6L, 7L] | 2 + [1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L] | 4 + [] | 0 + [1L, 20L] as Long[] | 1 + [4L, 5L, 6L, 7L] as Long[] | 2 + [] as Long[] | 0 + } + + @Unroll + void 'search #number in an array of float'() { + setup: + new Like(favoriteFloatNumbers: [1f, 2f, 3f, 4f, 5f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [4f, 5f, 6f, 7f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [4f, 5f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [1f, 20f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [2f]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsContainedBy', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1f | 0 + 2f | 1 + [1f, 20f] | 1 + [4f, 5f, 6f, 7f] | 2 + [1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f] | 4 + [] | 0 + [1f, 20f] as Float[] | 1 + [4f, 5f, 6f, 7f] as Float[] | 2 + [] as Float[] | 0 + } + + + @Unroll + void 'search #number in an array of double'() { + setup: + new Like(favoriteDoubleNumbers: [1d, 2d, 3d, 4d, 5d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [4d, 5d, 6d, 7d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [4d, 5d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [1d, 20d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [2d]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsContainedBy', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1d | 0 + 2d | 1 + [1d, 20d] | 1 + [4d, 5d, 6d, 7d] | 2 + [1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d] | 4 + [] | 0 + [1d, 20d] as Double[] | 1 + [4d, 5d, 6d, 7d] as Double[] | 2 + [] as Double[] | 0 + } + + @Unroll + void 'search #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['A', 'B', 'C', 'D', 'E']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['D', 'E', 'F', 'G']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['A', 'Z']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['B']).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsContainedBy', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + 'A' | 0 + 'B' | 1 + ['A', 'Z'] | 1 + ['B', 'D', 'E', 'F', 'G'] | 2 + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Z'] | 4 + [] | 0 + ['A', 'Z'] as String[] | 1 + ['B', 'D', 'E', 'F', 'G'] as String[] | 2 + [] as String[] | 0 + } + + @Unroll + void 'search #movie in an array of UUIDs'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['A', 'B', 'C', 'D', 'E'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['D', 'E', 'F', 'G'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['A', 'Z'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['B'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsContainedBy', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + UuidBuilder.createUUID('A') | 0 + UuidBuilder.createUUID('B') | 1 + UuidBuilder.createUUIDs(['A', 'Z']) | 1 + UuidBuilder.createUUIDs(['B', 'D', 'E', 'F', 'G']) | 2 + UuidBuilder.createUUIDs(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Z']) | 4 + [] | 0 + UuidBuilder.createUUIDs(['A', 'Z']) as UUID[] | 1 + UuidBuilder.createUUIDs(['B', 'D', 'E', 'F', 'G']) as UUID[] | 2 + [] as UUID[] | 0 + } + + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.APPLE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.CARROT]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsContainedBy', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 0 + Like.Juice.ORANGE | 1 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 0 + Like.Juice.PINEAPPLE | 0 + Like.Juice.TOMATO | 0 + Like.Juice.CARROT | 0 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE, + Like.Juice.ORANGE, Like.Juice.CARROT] | 2 + [Like.Juice.APPLE] | 1 + [Like.Juice.CARROT, Like.Juice.ORANGE] | 2 + [] | 0 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'Abe', like: new Like(favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Bernard', like: new Like(favoriteMovies: ['A', 'C'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Carl', like: new Like(favoriteMovies: ['A'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Dave', like: new Like(favoriteMovies: ['A', 'C', 'D'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsContainedBy', movies) + + then: + result.size() == 2 + result.contains(user2) + result.contains(user3) + + where: + movies = ['A', 'B', 'C'] + } + + void 'search in an array of strings with join with another domain class and or statement'() { + setup: + def user1 = new User(name: 'Abe', like: new Like(favoriteNumbers: [1, 3], favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Bernard', like: new Like(favoriteNumbers: [1], favoriteMovies: ['A', 'C'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Carl', like: new Like(favoriteNumbers: [2], favoriteMovies: ['A'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Dave', like: new Like(favoriteNumbers: [1, 2], favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayIsContainedBy', favoriteMovies: movies, favoriteNumbers: numbers) + + then: + result.size() == 3 + result.contains(user2) + result.contains(user3) + result.contains(user4) + + where: + movies = ['A', 'B', 'C'] + numbers = [1, 2] + } + + void 'search a invalid list inside the array of integers'() { + when: + pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsContainedBy', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] + } + + void 'search a invalid list inside the array of long'() { + when: + pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsContainedBy', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] + } + + void 'search a invalid list inside the array of float'() { + when: + pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsContainedBy', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] + } + + void 'search a invalid list inside the array of double'() { + when: + pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsContainedBy', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] + } + + void 'search a invalid list inside the array of string'() { + when: + pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsContainedBy', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] + } + + void 'search an invalid list inside the array of UUID'() { + when: + pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsContainedBy', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + } + + void 'search an invalid list juice inside the array of enum'() { + when: + pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsContainedBy', juice) + + then: + thrown HibernateException + + where: + juice << [['Test'], ['Test', Like.Juice.ORANGE], [1L], ['Test', 1L]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..1a692d0 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,130 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + void 'search for empty integer arrays'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty long arrays'() { + setup: + new Like(favoriteLongNumbers: [3L, 7L, 20L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty float arrays'() { + setup: + new Like(favoriteFloatNumbers: [3f, 7f, 20f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty double arrays'() { + setup: + new Like(favoriteDoubleNumbers: [3d, 7d, 20d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty string arrays'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty UUID arrays'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search for empty enum arrays'() { + setup: + new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO]).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsEmpty') + + then: + result.size() == 2 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsEmpty') + + then: + result.size() == 2 + result.contains(user2) + result.contains(user3) + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..5b0ce99 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,212 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'search #number in an array of integers'() { + setup: + def l1 = new Like(favoriteNumbers: [3, 7]).save(flush: true, failOnError: true) + def l2 = new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + def l3 = new Like(favoriteNumbers: [3, 4]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsEmptyOrContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + [] | 1 + [3] | 2 + [4] | 1 + [3, 4] | 1 + [] as Integer[] | 1 + [3] as Integer[] | 2 + [3, 4] as Integer[] | 1 + } + + @Unroll + void 'search #number in an array of longs'() { + setup: + new Like(favoriteLongNumbers: [12383L, 2392348L, 3498239L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [12383L, 98978L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [12383L]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsEmptyOrContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + [12383L, 98978L] | 1 + [12383L] | 3 + [] | 1 + [12383L, 98978L] as Long[] | 1 + [12383L] as Long[] | 3 + [] as Long[] | 1 + } + + @Unroll + void 'search #number in an array of floats'() { + setup: + new Like(favoriteFloatNumbers: [12383f, 2392348f, 3498239f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12383f, 98978f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12383f]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsEmptyOrContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + [12383f, 98978f] | 1 + [12383f] | 3 + [] | 1 + [12383f, 98978f] as Float[] | 1 + [12383f] as Float[] | 3 + [] as Float[] | 1 + } + + @Unroll + void 'search #number in an array of doubles'() { + setup: + new Like(favoriteDoubleNumbers: [12383d, 2392348d, 3498239d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12383d, 98978d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12383d]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsEmptyOrContains', number) + + then: + result.size() == resultSize + + where: + number | resultSize + [12383d, 98978d] | 1 + [12383d] | 3 + [] | 1 + [12383d, 98978d] as Double[] | 1 + [12383d] as Double[] | 3 + [] as Double[] | 1 + } + + @Unroll + void 'search #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsEmptyOrContains', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + ['The Matrix'] | 1 + ['Star Wars', 'Romeo & Juliet'] | 1 + ['The Lord of the Rings'] | 2 + [] | 1 + ['Star Wars', 'Romeo & Juliet'] as String[] | 1 + ['The Lord of the Rings'] as String[] | 2 + [] as String[] | 1 + } + + @Unroll + void 'search #movie in an array of UUIDs'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsEmptyOrContains', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + UuidBuilder.createUUIDs(['The Matrix']) | 1 + UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) | 1 + UuidBuilder.createUUIDs(['The Lord of the Rings']) | 2 + [] | 1 + UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) as UUID[] | 1 + UuidBuilder.createUUIDs(['The Lord of the Rings']) as UUID[] | 2 + [] as UUID[] | 1 + } + + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsEmptyOrContains', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + [Like.Juice.ORANGE] | 2 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 1 + [] | 1 + } + + @Unroll + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'Spiderman'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsEmptyOrContains', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + ['The Matrix'] | 1 + ['Blade Runner', 'Spiderman'] | 2 + [] | 1 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..697837b --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,129 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + void 'search for empty integer arrays'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty long arrays'() { + setup: + new Like(favoriteLongNumbers: [3L, 7L, 20L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty float arrays'() { + setup: + new Like(favoriteFloatNumbers: [3f, 7f, 20f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty double arrays'() { + setup: + new Like(favoriteDoubleNumbers: [3d, 7d, 20d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty string arrays'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty UUID arrays'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search for empty enum arrays'() { + setup: + new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO]).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsNotEmpty') + + then: + result.size() == 1 + result.contains(user1) + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..e79b5f3 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,310 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import org.hibernate.HibernateException +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'check equals for #number in an array of integers'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [17]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayNotEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 3 | 4 + 17 | 3 + [3, 7, 20] | 3 + [7, 3, 20] | 4 + [] | 3 + 17 as Integer[] | 3 + [] as Integer[] | 3 + } + + @Unroll + void 'check equals for #number in an array of longs'() { + setup: + new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayNotEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1L | 3 + [1L, 23L, 34L] | 3 + [1L, 34L, 23L] | 4 + [] | 3 + 7L as Long[] | 3 + } + + @Unroll + void 'check equals for #number in an array of floats'() { + setup: + new Like(favoriteFloatNumbers: [12f, 23f, 34f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12f, 98f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [12f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayNotEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12f | 3 + [12f, 98f] | 3 + [98f, 12f] | 4 + [12f] | 3 + [] | 3 + [] as Float[] | 3 + } + + @Unroll + void 'check equals for #number in an array of doubles'() { + setup: + new Like(favoriteDoubleNumbers: [12d, 23d, 34d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12d, 98d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [12d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayNotEquals', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 12d | 3 + [12d] | 3 + [12d, 98d] | 3 + [98d, 12d] | 4 + [] | 3 + [12d] as Double[] | 3 + } + + @Unroll + void 'check equals for #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayNotEquals', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + 'Star Wars' | 3 + ['Star Wars'] | 3 + ['Star Wars'] as String[] | 3 + 'The Usual Suspects' | 4 + ['Spiderman', 'Blade Runner', 'Star Wars'] | 3 + ['Spiderman', 'Star Wars', 'Blade Runner'] | 4 + [] | 3 + } + + @Unroll + void 'check equals for #movie in an array of uuids'() { + setup: + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Star Wars'])).save(flush: true, failOnError: true) + new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayNotEquals', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + UuidBuilder.createUUID('Star Wars') | 3 + UuidBuilder.createUUIDs(['Star Wars']) | 3 + UuidBuilder.createUUIDs(['Star Wars']) as UUID[] | 3 + UuidBuilder.createUUID('The Usual Suspects') | 4 + UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars']) | 3 + UuidBuilder.createUUIDs(['Spiderman', 'Star Wars', 'Blade Runner']) | 4 + [] | 3 + } + + @Unroll + void 'check equals for #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + new Like(favoriteJuices: []).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayNotEquals', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.PINEAPPLE | 3 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 3 + [Like.Juice.GRAPE, Like.Juice.ORANGE] | 4 + [] | 3 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayNotEquals', movie) + + then: + result.size() == 2 + result.contains(user1) + result.contains(user4) + + where: + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] + } + + void 'search in an array of strings with join with another domain class and or statement'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayNotEquals', favoriteMovies: movie, favoriteNumbers: number) + + then: + result.size() == 2 + result.contains(user1) + result.contains(user4) + + where: + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] + number = [5, 17, 9, 6] + } + + void 'search an invalid list inside the array of integers'() { + when: + pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayNotEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] + } + + void 'search an invalid list inside the array of long'() { + when: + pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayNotEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] + } + + void 'search an invalid list inside the array of float'() { + when: + pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayNotEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] + } + + void 'search an invalid list inside the array of double'() { + when: + pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayNotEquals', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] + } + + void 'search an invalid list inside the array of string'() { + when: + pgArrayTestSearchService.search('favoriteMovies', 'pgArrayNotEquals', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', 1], [1L], ['Test', 1L], [UUID.randomUUID()]] + } + + void 'search an invalid list inside the array of UUID'() { + when: + pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayNotEquals', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + } + + void 'search an invalid list inside the array of enum'() { + when: + pgArrayTestSearchService.search('favoriteJuices', 'pgArrayNotEquals', juice) + + then: + thrown HibernateException + + where: + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..ba8ff80 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy @@ -0,0 +1,303 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User +import org.hibernate.HibernateException +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { + + @Autowired + PgArrayTestSearchService pgArrayTestSearchService + + def setup() { + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'overlaps #number in an array of integers'() { + setup: + new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [5, 17, 9, 6, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) + new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayOverlaps', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 3 | 2 + 17 | 1 + 9 | 2 + 4 | 2 + 1 | 0 + 20 | 4 + [3, 4] | 3 + [3, 4, 7] | 3 + [4] | 2 + [3, 20] | 4 + [] | 0 + [4] as Integer[] | 2 + [3, 20] as Integer[] | 4 + [] as Integer[] | 0 + } + + @Unroll + void 'overlaps #number in an array of longs'() { + setup: + new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [1L, 7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [-9L, 16L, 7L]).save(flush: true, failOnError: true) + new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayOverlaps', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1L | 3 + 7L | 2 + -9L | 1 + 100L | 0 + [1L, 7L] | 4 + [1L] | 3 + [] | 0 + [1L, 7L] as Long[] | 4 + [1L] as Long[] | 3 + [] as Long[] | 0 + } + + @Unroll + void 'overlaps #number in an array of floats'() { + setup: + new Like(favoriteFloatNumbers: [1f, 23f, 34f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [1f, 7f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [-9f, 16f, 7f]).save(flush: true, failOnError: true) + new Like(favoriteFloatNumbers: [1f]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayOverlaps', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1f | 3 + 7f | 2 + -9f | 1 + 100f | 0 + [1f, 7f] | 4 + [1f] | 3 + [] | 0 + [1f, 7f] as Float[] | 4 + [1f] as Float[] | 3 + [] as Float[] | 0 + } + + @Unroll + void 'overlaps #number in an array of double'() { + setup: + new Like(favoriteDoubleNumbers: [1d, 23d, 34d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [1d, 7d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [-9d, 16d, 7d]).save(flush: true, failOnError: true) + new Like(favoriteDoubleNumbers: [1d]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayOverlaps', number) + + then: + result.size() == resultSize + + where: + number | resultSize + 1d | 3 + 7d | 2 + -9d | 1 + 100d | 0 + [1d, 7d] | 4 + [1d] | 3 + [] | 0 + [1d, 7d] as Double[] | 4 + [1d] as Double[] | 3 + [] as Double[] | 0 + } + + @Unroll + void 'search #movie in an array of strings'() { + setup: + new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) + new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayOverlaps', movie) + + then: + result.size() == resultSize + + where: + movie | resultSize + 'The Matrix' | 1 + 'The Lord of the Rings' | 2 + 'Blade Runner' | 2 + 'Star Wars' | 2 + 'The Usual Suspects' | 0 + ['Star Wars', 'Romeo & Juliet'] | 3 + ['The Lord of the Rings'] | 2 + [] | 0 + ['Star Wars', 'Romeo & Juliet'] as String[] | 3 + ['The Lord of the Rings'] as String[] | 2 + [] as String[] | 0 + } + + @Unroll + void 'search #juice in an array of enums'() { + setup: + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayOverlaps', juice) + + then: + result.size() == resultSize + + where: + juice | resultSize + Like.Juice.CRANBERRY | 1 + Like.Juice.ORANGE | 2 + Like.Juice.LEMON | 0 + Like.Juice.APPLE | 1 + Like.Juice.GRAPE | 2 + Like.Juice.PINEAPPLE | 1 + Like.Juice.TOMATO | 2 + Like.Juice.CARROT | 3 + Like.Juice.GRAPEFRUIT | 0 + [Like.Juice.ORANGE, Like.Juice.GRAPE] | 3 + [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 2 + [Like.Juice.CARROT] | 3 + [Like.Juice.CARROT, Like.Juice.TOMATO] | 3 + [] | 0 + } + + void 'search in an array of strings with join with another domain class'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayOverlaps', movie) + + then: + result.size() == 3 + result.contains(user2) + result.contains(user3) + result.contains(user4) + + where: + movie = ['Star Wars', 'Romeo & Juliet'] + } + + void 'search in an array of strings with join with another domain class and or statement'() { + setup: + def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) + def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) + def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) + + when: + def result = pgArrayTestSearchService.searchWithJoinAnd('pgArrayOverlaps', favoriteMovies: movie, favoriteNumbers: number) + + then: + result.size() == 2 + result.contains(user2) + result.contains(user4) + + where: + movie = ['Star Wars', 'Romeo & Juliet'] + number = [9] + } + + @Unroll + void 'search a invalid list inside the array of integers'() { + when: + pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayOverlaps', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] + } + + @Unroll + void 'search a invalid list inside the array of long'() { + when: + pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayOverlaps', number) + + then: + thrown HibernateException + + where: + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] + } + + @Unroll + void 'search a invalid list inside the array of string'() { + when: + pgArrayTestSearchService.search('favoriteMovies', 'pgArrayOverlaps', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] + } + + @Unroll + void 'search an invalid list inside the array of UUID'() { + when: + pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayOverlaps', movie) + + then: + thrown HibernateException + + where: + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + } + + @Unroll + void 'search an invalid list inside the array of enum'() { + when: + pgArrayTestSearchService.search('favoriteJuices', 'pgArrayOverlaps', juice) + + then: + thrown HibernateException + + where: + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy new file mode 100644 index 0000000..3233536 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy @@ -0,0 +1,141 @@ +package test.array + +import app.array.TestDouble +import app.array.TestEnum +import app.array.TestFloat +import app.array.TestInteger +import app.array.TestLong +import app.array.TestString +import app.array.TestUuid +import spock.lang.Specification +import spock.lang.Unroll + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PostgresqlArraysDomainIntegrationSpec extends Specification { + + def setup() { + TestInteger.executeUpdate('delete from TestInteger') + TestLong.executeUpdate('delete from TestLong') + TestFloat.executeUpdate('delete from TestFloat') + TestDouble.executeUpdate('delete from TestDouble') + TestUuid.executeUpdate('delete from TestUuid') + TestString.executeUpdate('delete from TestString') + TestEnum.executeUpdate('delete from TestEnum') + } + + @Unroll + void 'save a domain class with an integer array value #numbers'() { + setup: + def testInt = new TestInteger(integerNumbers: numbers) + + when: + testInt.save(flush: true, failOnError: true) + + then: + testInt.hasErrors() == false + testInt.integerNumbers?.length == numbers?.size() + + where: + numbers << [null, [], [5], [3, -1], [-9, 4, -123, 0]] + } + + @Unroll + void 'save a domain class with a long array value #numbers'() { + setup: + def testLong = new TestLong(longNumbers: numbers) + + when: + testLong.save(flush: true, failOnError: true) + + then: + testLong.hasErrors() == false + testLong.longNumbers?.length == numbers?.size() + + where: + numbers << [null, [], [5L], [3L, -1L], [-9L, 4L, -123L, 0L]] + } + + @Unroll + void 'save a domain class with a Float array value #numbers'() { + setup: + def testFloat = new TestFloat(floatNumbers: numbers) + + when: + testFloat.save(flush: true, failOnError: true) + + then: + testFloat.hasErrors() == false + testFloat.floatNumbers?.length == numbers?.size() + + where: + numbers << [null, [], [5f], [3f, -1f], [-9f, 4f, -123f, 0f]] + } + + @Unroll + void 'save a domain class with a Double array value #numbers'() { + setup: + def testDouble = new TestDouble(doubleNumbers: numbers) + + when: + testDouble.save(flush: true, failOnError: true) + + then: + testDouble.hasErrors() == false + testDouble.doubleNumbers?.length == numbers?.size() + + where: + numbers << [null, [], [5d], [3d, -1d], [-9d, 4d, -123d, 0d]] + } + + @Unroll + void 'save a domain class with an string array value #strings'() { + setup: + def testString = new TestString(stringArray: strings) + + when: + testString.save(flush: true, failOnError: true) + + then: + testString.hasErrors() == false + testString.stringArray?.length == strings?.size() + + where: + strings << [null, [], ['string 1'], ['string 1', 'string 2'], ['string 1', 'string 2', 'string 3']] + } + + @Unroll + void 'save a domain class with an UUID array value #uuids'() { + setup: + def testUuid = new TestUuid(uuidArray: uuids) + + when: + testUuid.save(flush: true, failOnError: true) + + then: + testUuid.hasErrors() == false + testUuid.uuidArray?.length == uuids?.size() + + where: + uuids << [null, [], [UUID.randomUUID()], [UUID.randomUUID(), UUID.randomUUID()], [UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()]] + } + + @Unroll + void 'save a domain class with an enum array value #days'() { + when: + // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke + // nullSafeGet on the corresponding user type and will not use current session's cache. + def id = TestEnum.withNewTransaction { + new TestEnum(days: days).save(flush: true, failOnError: true).id + } + + then: + TestEnum.get(id).days as List == days + + where: + days << [null, [], [TestEnum.Day.MONDAY], [TestEnum.Day.SUNDAY, TestEnum.Day.SATURDAY], [TestEnum.Day.WEDNESDAY, TestEnum.Day.THURSDAY, TestEnum.Day.TUESDAY]] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy b/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy new file mode 100644 index 0000000..f9e66ae --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy @@ -0,0 +1,15 @@ +package test.array + +import groovy.transform.CompileStatic + +@CompileStatic +class UuidBuilder { + + static UUID createUUID(String name) { + UUID.nameUUIDFromBytes(name.bytes) + } + + static List createUUIDs(List names) { + names.collect { createUUID(it) } + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy new file mode 100644 index 0000000..2a0cfcc --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy @@ -0,0 +1,115 @@ +package test.hstore + +import app.criteria.hstore.PgHstoreTestSearchService +import app.hstore.TestHstoreMap +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgHstoreContainsIntegrationSpec extends Specification { + + @Autowired + PgHstoreTestSearchService pgHstoreTestSearchService + + def setup() { + TestHstoreMap.executeUpdate('delete from TestHstoreMap') + } + + void 'Test only one value result 2 different elements'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) + + then: + result.size() == 2 + result.find { it.name == 'test1' } + !result.find { it.name == 'test2' } + !result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + + where: + map = [b: '1'] + } + + void 'Test two values that matches partialy with one element'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) + + then: + result.size() == 1 + !result.find { it.name == 'test1' } + !result.find { it.name == 'test2' } + !result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + + where: + map = [b: '1', c: 'test'] + } + + void 'No matches with the same combination key/value'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) + + then: + result.size() == 0 + + where: + map = [b: '3'] + } + + void 'No matches with the same combination but one of the elements matches'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) + + then: + result.size() == 0 + + where: + map = [b: '1', c: 'test-otro'] + } + + void 'When empty map returns all elements'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) + + then: + result.size() == 4 + + where: + map = [:] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy new file mode 100644 index 0000000..9e7043e --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy @@ -0,0 +1,54 @@ +package test.hstore + +import app.criteria.hstore.PgHstoreTestSearchService +import app.hstore.TestHstoreMap +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgHstoreContainsKeyIntegrationSpec extends Specification { + + @Autowired + PgHstoreTestSearchService pgHstoreTestSearchService + + def setup() { + TestHstoreMap.executeUpdate('delete from TestHstoreMap') + } + + void 'Test find hstore that contains key'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContainsKey', 'b') + + then: + result.size() == 3 + result.find { it.name == 'test1' } + result.find { it.name == 'test2' } + !result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + } + + void 'Test find hstore that contains other key'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContainsKey', 'X') + + then: + result.size() == 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy new file mode 100644 index 0000000..a2d4a04 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy @@ -0,0 +1,54 @@ +package test.hstore + +import app.criteria.hstore.PgHstoreTestSearchService +import app.hstore.TestHstoreMap +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgHstoreILikeValueFunctionIntegrationSpec extends Specification { + + @Autowired + PgHstoreTestSearchService pgHstoreTestSearchService + + def setup() { + TestHstoreMap.executeUpdate('delete from TestHstoreMap') + } + + void 'Test find hstore that ilikes value'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreILikeValue', '%test%') + + then: + result.size() == 3 + result.find { it.name == 'test1' } + !result.find { it.name == 'test2' } + result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + } + + void 'Test find hstore that no ilikes value'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test2']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'Xa', b: '3']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreILikeValue', '%X') + + then: + result.size() == 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy new file mode 100644 index 0000000..f9c53fa --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy @@ -0,0 +1,81 @@ +package test.hstore + +import app.criteria.hstore.PgHstoreTestSearchService +import app.hstore.TestHstoreMap +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgHstoreIsContainedIntegrationSpec extends Specification { + + @Autowired + PgHstoreTestSearchService pgHstoreTestSearchService + + def setup() { + TestHstoreMap.executeUpdate('delete from TestHstoreMap') + } + + void 'No element matches with the empty set'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) + + then: + result.size() == 0 + + where: + map = [:] + } + + void 'All elements matches'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) + + then: + result.size() == 4 + result.find { it.name == 'test1' } + result.find { it.name == 'test2' } + result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + + where: + map = [a: 'test', b: '1', c: 'test', d: '10'] + } + + void 'Some elements matches'() { + setup: + new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) + new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) + + then: + result.size() == 3 + result.find { it.name == 'test1' } + !result.find { it.name == 'test2' } + result.find { it.name == 'test3' } + result.find { it.name == 'test4' } + + where: + map = [a: 'test', b: '1', c: 'test'] + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy new file mode 100644 index 0000000..c350214 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy @@ -0,0 +1,138 @@ +package test.hstore + +import app.hstore.TestHstoreMap +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.lang.Unroll + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { + + def setup() { + TestHstoreMap.executeUpdate('delete from TestHstoreMap') + } + + @Unroll + void 'save a domain class with a map. key: #data'() { + setup: + def testHstoreMap = new TestHstoreMap(testAttributes: data) + + when: + testHstoreMap.save(flush: true, failOnError: true) + + then: + !testHstoreMap.hasErrors() + testHstoreMap.testAttributes != null + testHstoreMap.testAttributes.size() == data.size() + testHstoreMap.testAttributes[attribute] == value + + where: + data | attribute | value + [foo: 'bar'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' + } + + @Unroll + void 'recover a domain class with a map. key: #data'() { + setup: + new TestHstoreMap(testAttributes: data).save(flush: true, failOnError: true) + + when: + def testHstoreMap = TestHstoreMap.findAll().first() + + then: + !testHstoreMap.hasErrors() + testHstoreMap.testAttributes != null + testHstoreMap.testAttributes.size() == data.size() + testHstoreMap.testAttributes[key] == value + + where: + data | key | value + [foo: 'bar', xxx: 'abc'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' + } + + @Unroll + void 'remove a key in a map. key: #data, valueToRemove: #valueToRemove'() { + setup: + def testHstoreMap = new TestHstoreMap(testAttributes: data) + testHstoreMap.save(flush: true, failOnError: true) + + when: + testHstoreMap.testAttributes.remove(valueToRemove) + + then: + !testHstoreMap.hasErrors() + testHstoreMap.testAttributes != null + testHstoreMap.testAttributes.size() == size + + where: + data | valueToRemove | size + [foo: 'bar', xxx: 'abc'] | 'xxx' | 1 + ['foo,bar': 'baz,qux'] | 'foo,bar' | 0 + [foo: 'bar'] | 'xxx' | 1 + } + + @Unroll + void 'save and delete a domain class with a map. key: #data'() { + setup: + def testHstoreMap = new TestHstoreMap(testAttributes: data) + + when: 'I save an instance' + testHstoreMap.save(flush: true, failOnError: true) + + and: 'The instance is saved' + assert !testHstoreMap.hasErrors() + + and: 'I try to delete it' + testHstoreMap.delete(flush: true, failOnError: true) + + then: 'It shouldn\'t be present in database anymore' + TestHstoreMap.count() == 0 + + where: + data | attribute | value + [foo: 'bar'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' + } + + void 'save a domain class with a empty map and validate that is not dirty right after retrieval'() { + setup: + def testHstoreMap = new TestHstoreMap(testAttributes: [:]) + + when: 'I save an instance' + testHstoreMap.save(flush: true, failOnError: true) + + and: 'The instance is saved' + assert !testHstoreMap.hasErrors() + + and: 'I retrieve it and check for dirty properties in a new session' + def retrievedTestHstoreMap = TestHstoreMap.get(testHstoreMap.id) + + then: 'It should not be dirty right after db retrieval' + !retrievedTestHstoreMap.isDirty() + } + + @PendingFeature(reason = 'seems dirty check does not work for Grails 3.3 with Hibernate 5.2 and GORM 6.1.9') + void 'save a domain class, modify it and validate that it is dirty'() { + setup: + def testHstoreMap = new TestHstoreMap(testAttributes: [:]) + + when: 'I save an instance' + testHstoreMap.save(flush: true, failOnError: true) + + and: 'The instance is saved' + assert !testHstoreMap.hasErrors() + + and: 'I retrieve it and modify a property' + def retrievedTestHstoreMap = TestHstoreMap.get(testHstoreMap.id) + retrievedTestHstoreMap.testAttributes << [foo: 'bar'] + + then: 'It should be dirty' + retrievedTestHstoreMap.isDirty() + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy new file mode 100644 index 0000000..8b2cf62 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy @@ -0,0 +1,43 @@ +package test.json + +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonEqualsIntegrationSpec extends Specification { + + @Autowired + PgJsonTestSearchService pgJsonTestSearchService + + def setup() { + TestMapJson.executeUpdate('delete from TestMapJson') + } + + @Unroll + void 'Test equals finding value: #value (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true, failOnError: true) + + when: + def result = pgJsonTestSearchService.search('pgJsonHasFieldValue', 'data', 'name', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy new file mode 100644 index 0000000..3ba9b3d --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy @@ -0,0 +1,61 @@ +package test.json + +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonPathsIntegrationSpec extends Specification { + + @Autowired + PgJsonTestSearchService pgJsonTestSearchService + + @Unroll + void 'Test equals finding nested values (json). sqlOp: equals'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) + + then: + result.size() == size + result.every { it.data.nested.a == value } + + where: + value || size + 1 || 2 // there are 2 items with nested.a equal to 1 + 2 || 1 + 3 || 0 + } + + @Unroll + void 'Test equals finding nested values (json). sqlOp: greater than'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) + + then: + result.size() == size + result.every { it.data.nested.b > value.toInteger() } + + where: + value || size + 1 || 3 // There are 3 items with nested.b > 1 + 3 || 1 + 6 || 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy new file mode 100644 index 0000000..0162b67 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy @@ -0,0 +1,79 @@ +package test.json + +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonValuesIntegrationSpec extends Specification { + + @Autowired + PgJsonTestSearchService pgJsonTestSearchService + + @Unroll + void 'Test equals finding value: #value with condition is ilike (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) + + then: + result.size() == size + result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } + + where: + value || size + '%iv%' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition equals (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition does not equal (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) + + then: + result.size() == size + result.every { it.data.name != value } + + where: + value || size + 'Iván' || 1 + 'John' || 3 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy new file mode 100644 index 0000000..0fae20d --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy @@ -0,0 +1,107 @@ +package test.json + +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonbContainsIntegrationSpec extends Specification { + + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService + + def setup() { + TestMapJsonb.executeUpdate('delete from TestMapJsonb') + } + + void 'Test only one value'() { + given: + def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: 1, d: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + def obj4 = new TestMapJsonb(data: [b: '1', a: 'foo', c: 'test',]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) + + then: + result.size() == 2 + result.contains(obj1) + result.contains(obj4) + + where: + map = [b: '1'] + } + + void 'Test multiple values'() { + given: + def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + def obj4 = new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) + + then: + result.size() == 2 + result.contains(obj1) + result.contains(obj4) + + where: + map = [a: 'foo', b: '1'] + } + + void 'No matches with the same combination key/value'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) + + then: + result.size() == 0 + + where: + map = [b: 3] + } + + void 'All matched with empty map'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) + + then: + result.size() == 4 + where: + map = [:] + } + + void 'None matched with null'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) + + then: + result.size() == 0 + + where: + map = null + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy new file mode 100644 index 0000000..0437c71 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy @@ -0,0 +1,39 @@ +package test.json + +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonbEqualsIntegrationSpec extends Specification { + + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService + + @Unroll + void 'Test equals finding value: #value (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonHasFieldValue', 'data', 'name', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy new file mode 100644 index 0000000..b8fd025 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy @@ -0,0 +1,90 @@ +package test.json + +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonbIsContainedIntegrationSpec extends Specification { + + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService + + def setup() { + TestMapJsonb.executeUpdate('delete from TestMapJsonb') + } + + void 'Test some values'() { + given: + def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: 1, d: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + def obj4 = new TestMapJsonb(data: [b: '1', a: 'foo', c: 'test',]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) + + then: + result.size() == 2 + result.contains(obj1) + result.contains(obj4) + + where: + map = [a: 'foo', b: '1', c: 'test'] + } + + void 'No matches with the same combination key/value'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) + + then: + result.size() == 0 + + where: + map = [a: 'foo'] + } + + void 'Only empty matched with empty map'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + def obj4 = new TestMapJsonb(data: [:]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) + + then: + result.size() == 1 + result.contains(obj4) + + where: + map = [:] + } + + void 'No data matches with null'() { + given: + new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [:]).save(flush: true, failOnError: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) + + then: + result.size() == 0 + + where: + map = null + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy new file mode 100644 index 0000000..09223aa --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy @@ -0,0 +1,61 @@ +package test.json + +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonbPathsIntegrationSpec extends Specification { + + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService + + @Unroll + void 'Test equals finding nested values (jsonb). sqlOp: equals'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) + + then: + result.size() == size + result.every { it.data.nested.a == value } + + where: + value || size + 1 || 2 // there are 2 items with nested.a equal to 1 + 2 || 1 + 3 || 0 + } + + @Unroll + void 'Test equals finding nested values (jsonb). sqlOp: greater than'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) + + then: + result.size() == size + result.every { it.data.nested.b > value.toInteger() } + + where: + value || size + 1 || 3 // There are 3 items with nested.b > 1 + 3 || 1 + 6 || 0 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy new file mode 100644 index 0000000..0623645 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy @@ -0,0 +1,79 @@ +package test.json + +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.annotation.Autowired + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgJsonbValuesIntegrationSpec extends Specification { + + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService + + @Unroll + void 'Test equals finding value: #value with condition is ilike (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) + + then: + result.size() == size + result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } + + where: + value || size + '%iv%' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition equals (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition does not equal (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) + + then: + result.size() == size + result.every { it.data.name != value } + + where: + value || size + 'Iván' || 1 + 'John' || 3 + } +} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy new file mode 100644 index 0000000..4265b36 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy @@ -0,0 +1,57 @@ +package test.json + +import app.json.TestMapJson +import spock.lang.Specification +import spock.lang.Unroll + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PostgresqlJsonMapDomainIntegrationSpec extends Specification { + + def setup() { + TestMapJson.executeUpdate('delete from TestMapJson') + } + + @Unroll + void 'save and read a domain class with a map #map to json'() { + setup: + def testMapJson = new TestMapJson(data: map) + + when: + // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke + // nullSafeGet on the corresponding user type and will not use current session's cache. + TestMapJson.withNewTransaction { + testMapJson.save(flush: true, failOnError: true) + } + + then: + testMapJson.hasErrors() == false + + and: + def obj = testMapJson.get(testMapJson.id) + obj.data == map + + where: + map << [null, [:], [name: 'Ivan', age: 34]] + } + + void 'save and read a domain class with json'() { + setup: + def value = [name: 'Ivan', age: 34, hasChildren: true, children: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] + def testMapJson = new TestMapJson(data: value) + + when: + testMapJson.save(flush: true, failOnError: true) + + then: + testMapJson.hasErrors() == false + + and: + def obj = testMapJson.get(testMapJson.id) + obj.data.keySet().collect { it.toString() } == ['name', 'age', 'hasChildren', 'children'] + obj.data.children.size() == 2 + } +} \ No newline at end of file diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy new file mode 100644 index 0000000..c072ee8 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy @@ -0,0 +1,57 @@ +package test.json + +import app.json.TestMapJsonb +import spock.lang.Specification +import spock.lang.Unroll + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PostgresqlJsonbMapDomainIntegrationSpec extends Specification { + + def setup() { + TestMapJsonb.executeUpdate('delete from TestMapJsonb') + } + + @Unroll + void 'save and read a domain class with a map #map to jsonb'() { + setup: + def testMapJsonb = new TestMapJsonb(data: map) + + when: + // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke + // nullSafeGet on the corresponding user type and will not use current session's cache. + TestMapJsonb.withNewTransaction { + testMapJsonb.save(flush: true, failOnError: true) + } + + then: + testMapJsonb.hasErrors() == false + + and: + def obj = testMapJsonb.get(testMapJsonb.id) + obj.data == map + + where: + map << [null, [:], [name: 'Ivan', age: 34]] + } + + void 'save and read a domain class with jsonb'() { + setup: + def value = [name: 'Ivan', age: 34, hasChilds: true, childs: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] + def testMapJsonb = new TestMapJsonb(data: value) + + when: + testMapJsonb.save(flush: true, failOnError: true) + + then: + testMapJsonb.hasErrors() == false + + and: + def obj = testMapJsonb.get(testMapJsonb.id) + obj.data.keySet().collect { it.toString() }.equals(['name', 'age', 'hasChilds', 'childs']) + obj.data.childs.size() == 2 + } +} \ No newline at end of file diff --git a/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy new file mode 100644 index 0000000..d1d9798 --- /dev/null +++ b/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy @@ -0,0 +1,46 @@ +package test.order + +import app.json.TestMapJsonb +import spock.lang.Specification + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgOrderIntegrationSpec extends Specification { + + def pgOrderService + + def setup() { + TestMapJsonb.executeUpdate('delete from TestMapJsonb') + } + + void 'Order by a json property'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [name: 'Ernesto', lastName: 'Pérez']).save(flush: true, failOnError: true) + + when: + def result = pgOrderService.orderByJson() + + then: + result + result.data.name == ['Iván', 'Ernesto', 'Alonso'] + } + + void 'Order by random'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) + new TestMapJsonb(data: [name: 'Ernesto', lastName: 'Pérez']).save(flush: true, failOnError: true) + + when: + def result = pgOrderService.orderByRandom() + + then: + result + result.size() == 3 + } +} \ No newline at end of file From 74676efdee980e933081cf5f6fddb2012e0ec3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 12:58:50 +0200 Subject: [PATCH 07/11] test: run integration tests on Testcontainers Each test app now starts its own PostgreSQL container with the hstore extension through the Testcontainers JDBC URL, instead of sharing one database. The apps run in parallel and interfered with each other's data in the shared database. The CI test job runs directly on the runner, so Testcontainers can use its Docker, and no longer needs the postgres service or the psql setup steps. --- .github/workflows/gradle.yml | 30 ------------------- test-apps/app1/build.gradle | 1 + .../app1/grails-app/conf/application.yml | 6 ++++ .../resources/init-pg-extensions.sql | 1 + test-apps/app2/build.gradle | 1 + .../app2/grails-app/conf/application.yml | 6 ++++ .../resources/init-pg-extensions.sql | 1 + 7 files changed, 16 insertions(+), 30 deletions(-) create mode 100644 test-apps/app1/src/integration-test/resources/init-pg-extensions.sql create mode 100644 test-apps/app2/src/integration-test/resources/init-pg-extensions.sql diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c4abade..df87897 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,34 +19,7 @@ jobs: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Test" runs-on: ubuntu-24.04 - container: ubuntu - services: - postgres: - image: postgres - env: - POSTGRES_DB: pg_extensions_test - POSTGRES_USER: postgres_extensions - POSTGRES_PASSWORD: postgres_extensions - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U postgres_extensions -d pg_extensions_test" - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: - - name: Install psql client - env: - DEBIAN_FRONTEND: noninteractive - run: | - apt-get update - apt-get install -y --no-install-recommends postgresql-client - - name: Create extensions - env: - PGPASSWORD: postgres_extensions - run: | - until pg_isready -h postgres -p 5432 -U postgres_extensions -d pg_extensions_test; do sleep 1; done - psql -h postgres -U postgres_extensions -d pg_extensions_test -c "CREATE EXTENSION IF NOT EXISTS hstore;" - name: "📥 Checkout repository" uses: actions/checkout@v5 - name: "☕️ Setup JDK" @@ -60,9 +33,6 @@ jobs: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "🏃‍♂️ Run Checks" id: build - env: - POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 run: ./gradlew check --continue publish_snapshot: needs: tests diff --git a/test-apps/app1/build.gradle b/test-apps/app1/build.gradle index 02cc950..1b1c57f 100644 --- a/test-apps/app1/build.gradle +++ b/test-apps/app1/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' integrationTestImplementation 'org.apache.grails:grails-dependencies-test' + integrationTestRuntimeOnly 'org.testcontainers:testcontainers-postgresql' } compileJava.options.release = javaVersion.toInteger() diff --git a/test-apps/app1/grails-app/conf/application.yml b/test-apps/app1/grails-app/conf/application.yml index 466169f..29a51b8 100644 --- a/test-apps/app1/grails-app/conf/application.yml +++ b/test-apps/app1/grails-app/conf/application.yml @@ -13,3 +13,9 @@ dataSource: dbCreate: create-drop hibernate: dialect: gpc.pgext.hibernate.PostgresqlExtensionsDialect +environments: + test: + dataSource: + # Each test run gets its own PostgreSQL container with the hstore extension + driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver + url: 'jdbc:tc:postgresql:17:///pg_extensions_test?TC_INITSCRIPT=init-pg-extensions.sql&TC_DAEMON=true' diff --git a/test-apps/app1/src/integration-test/resources/init-pg-extensions.sql b/test-apps/app1/src/integration-test/resources/init-pg-extensions.sql new file mode 100644 index 0000000..5ed9f15 --- /dev/null +++ b/test-apps/app1/src/integration-test/resources/init-pg-extensions.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS hstore; diff --git a/test-apps/app2/build.gradle b/test-apps/app2/build.gradle index a7d9a40..81a87ca 100644 --- a/test-apps/app2/build.gradle +++ b/test-apps/app2/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation 'org.apache.grails:grails-dependencies-starter-web' integrationTestImplementation 'org.apache.grails:grails-dependencies-test' + integrationTestRuntimeOnly 'org.testcontainers:testcontainers-postgresql' } compileJava.options.release = javaVersion.toInteger() diff --git a/test-apps/app2/grails-app/conf/application.yml b/test-apps/app2/grails-app/conf/application.yml index 466169f..29a51b8 100644 --- a/test-apps/app2/grails-app/conf/application.yml +++ b/test-apps/app2/grails-app/conf/application.yml @@ -13,3 +13,9 @@ dataSource: dbCreate: create-drop hibernate: dialect: gpc.pgext.hibernate.PostgresqlExtensionsDialect +environments: + test: + dataSource: + # Each test run gets its own PostgreSQL container with the hstore extension + driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver + url: 'jdbc:tc:postgresql:17:///pg_extensions_test?TC_INITSCRIPT=init-pg-extensions.sql&TC_DAEMON=true' diff --git a/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql b/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql new file mode 100644 index 0000000..5ed9f15 --- /dev/null +++ b/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS hstore; From 11d3f4f2c3c6284252523c2ea4289cf189cd6918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 14:30:37 +0200 Subject: [PATCH 08/11] test: share test-app1 sources with test-app2 test-app2 compiles the domain classes, services, init class and integration specs of test-app1 instead of keeping copies, so the Hibernate 5 and Hibernate 7 test apps run the same specs and cannot drift apart. --- Dockerfile | 7 - test-apps/app2/build.gradle | 23 ++ .../grails-app/conf/spring/resources.groovy | 3 - .../domain/app/array/TestDouble.groovy | 22 -- .../domain/app/array/TestEnum.groovy | 36 -- .../domain/app/array/TestFloat.groovy | 22 -- .../domain/app/array/TestInteger.groovy | 22 -- .../domain/app/array/TestLong.groovy | 22 -- .../domain/app/array/TestString.groovy | 22 -- .../domain/app/array/TestUuid.groovy | 22 -- .../domain/app/criteria/array/Like.groovy | 50 --- .../domain/app/criteria/array/User.groovy | 18 - .../domain/app/hstore/TestHstoreMap.groovy | 26 -- .../domain/app/json/TestMapJson.groovy | 22 -- .../domain/app/json/TestMapJsonb.groovy | 22 -- .../grails-app/init/app/Application.groovy | 13 - .../array/PgArrayTestSearchService.groovy | 52 --- .../hstore/PgHstoreTestSearchService.groovy | 12 - .../json/PgJsonTestSearchService.groovy | 18 - .../json/PgJsonbTestSearchService.groovy | 24 -- .../services/app/order/PgOrderService.groovy | 21 -- ...sCriteriaTestServiceIntegrationSpec.groovy | 348 ------------------ ...sCriteriaTestServiceIntegrationSpec.groovy | 328 ----------------- ...eCriteriaTestServiceIntegrationSpec.groovy | 47 --- ...yCriteriaTestServiceIntegrationSpec.groovy | 344 ----------------- ...yCriteriaTestServiceIntegrationSpec.groovy | 130 ------- ...sCriteriaTestServiceIntegrationSpec.groovy | 212 ----------- ...yCriteriaTestServiceIntegrationSpec.groovy | 129 ------- ...sCriteriaTestServiceIntegrationSpec.groovy | 310 ---------------- ...sCriteriaTestServiceIntegrationSpec.groovy | 303 --------------- ...stgresqlArraysDomainIntegrationSpec.groovy | 141 ------- .../groovy/test/array/UuidBuilder.groovy | 15 - .../PgHstoreContainsIntegrationSpec.groovy | 115 ------ .../PgHstoreContainsKeyIntegrationSpec.groovy | 54 --- ...reILikeValueFunctionIntegrationSpec.groovy | 54 --- .../PgHstoreIsContainedIntegrationSpec.groovy | 81 ---- ...resqlHstoreMapDomainIntegrationSpec.groovy | 138 ------- .../json/PgJsonEqualsIntegrationSpec.groovy | 43 --- .../json/PgJsonPathsIntegrationSpec.groovy | 61 --- .../json/PgJsonValuesIntegrationSpec.groovy | 79 ---- .../PgJsonbContainsIntegrationSpec.groovy | 107 ------ .../json/PgJsonbEqualsIntegrationSpec.groovy | 39 -- .../PgJsonbIsContainedIntegrationSpec.groovy | 90 ----- .../json/PgJsonbPathsIntegrationSpec.groovy | 61 --- .../json/PgJsonbValuesIntegrationSpec.groovy | 79 ---- ...tgresqlJsonMapDomainIntegrationSpec.groovy | 57 --- ...gresqlJsonbMapDomainIntegrationSpec.groovy | 57 --- .../test/order/PgOrderIntegrationSpec.groovy | 46 --- .../resources/init-pg-extensions.sql | 1 - 49 files changed, 23 insertions(+), 3925 deletions(-) delete mode 100644 Dockerfile delete mode 100644 test-apps/app2/grails-app/conf/spring/resources.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestDouble.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestEnum.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestFloat.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestInteger.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestLong.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestString.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/array/TestUuid.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/criteria/array/User.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy delete mode 100644 test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy delete mode 100644 test-apps/app2/grails-app/init/app/Application.groovy delete mode 100644 test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy delete mode 100644 test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy delete mode 100644 test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy delete mode 100644 test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy delete mode 100644 test-apps/app2/grails-app/services/app/order/PgOrderService.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy delete mode 100644 test-apps/app2/src/integration-test/resources/init-pg-extensions.sql diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 76557cd..0000000 --- a/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM postgres - -ENV POSTGRES_PASSWORD=postgres_extensions -ENV POSTGRES_USER=postgres_extensions -ENV POSTGRES_DB=pg_extensions_test - -RUN echo "CREATE EXTENSION hstore" >> /docker-entrypoint-initdb.d/hstore.sql \ No newline at end of file diff --git a/test-apps/app2/build.gradle b/test-apps/app2/build.gradle index 81a87ca..c5a77c3 100644 --- a/test-apps/app2/build.gradle +++ b/test-apps/app2/build.gradle @@ -15,6 +15,29 @@ dependencies { integrationTestRuntimeOnly 'org.testcontainers:testcontainers-postgresql' } +// test-app2 runs the domain classes, services and integration specs of test-app1 on Hibernate 7 +def app1 = rootProject.layout.projectDirectory.dir('test-apps/app1') +sourceSets { + main { + groovy { + srcDir app1.dir('grails-app/domain') + srcDir app1.dir('grails-app/services') + srcDir app1.dir('grails-app/init') + } + resources { + srcDir app1.dir('grails-app/conf/spring') + } + } + integrationTest { + groovy { + srcDir app1.dir('src/integration-test/groovy') + } + resources { + srcDir app1.dir('src/integration-test/resources') + } + } +} + compileJava.options.release = javaVersion.toInteger() apply { diff --git a/test-apps/app2/grails-app/conf/spring/resources.groovy b/test-apps/app2/grails-app/conf/spring/resources.groovy deleted file mode 100644 index fa95006..0000000 --- a/test-apps/app2/grails-app/conf/spring/resources.groovy +++ /dev/null @@ -1,3 +0,0 @@ -// Place your Spring DSL code here -beans = { -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy b/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy deleted file mode 100644 index 2f06ed8..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestDouble.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestDouble { - - Double[] doubleNumbers - - static mapping = { - doubleNumbers(type: ArrayType, params: [type: Double]) - } - - static constraints = { - doubleNumbers(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy b/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy deleted file mode 100644 index 8c9c259..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestEnum.groovy +++ /dev/null @@ -1,36 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestEnum { - - static enum Day { - MONDAY(0), - TUESDAY(1), - WEDNESDAY(2), - THURSDAY(3), - FRIDAY(4), - SATURDAY(5), - SUNDAY(6) - - final int id - - Day(int id) { this.id = id } - } - - Day[] days - - static mapping = { - days(type: ArrayType, params: [type: Day]) - } - - static constraints = { - days(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy b/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy deleted file mode 100644 index 2a6f488..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestFloat.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestFloat { - - Float[] floatNumbers - - static mapping = { - floatNumbers(type: ArrayType, params: [type: Float]) - } - - static constraints = { - floatNumbers(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy b/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy deleted file mode 100644 index 8b27811..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestInteger.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestInteger { - - Integer[] integerNumbers - - static mapping = { - integerNumbers(type: ArrayType, params: [type: Integer]) - } - - static constraints = { - integerNumbers(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestLong.groovy b/test-apps/app2/grails-app/domain/app/array/TestLong.groovy deleted file mode 100644 index e8af71f..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestLong.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestLong { - - Long[] longNumbers - - static mapping = { - longNumbers(type: ArrayType, params: [type: Long]) - } - - static constraints = { - longNumbers(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestString.groovy b/test-apps/app2/grails-app/domain/app/array/TestString.groovy deleted file mode 100644 index f4100bb..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestString.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestString { - - String[] stringArray - - static mapping = { - stringArray(type: ArrayType, params: [type: String]) - } - - static constraints = { - stringArray(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy b/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy deleted file mode 100644 index 1d3974c..0000000 --- a/test-apps/app2/grails-app/domain/app/array/TestUuid.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class TestUuid { - - UUID[] uuidArray - - static mapping = { - uuidArray(type: ArrayType, params: [type: UUID]) - } - - static constraints = { - uuidArray(nullable: true) - } -} diff --git a/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy b/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy deleted file mode 100644 index 0dc7d9d..0000000 --- a/test-apps/app2/grails-app/domain/app/criteria/array/Like.groovy +++ /dev/null @@ -1,50 +0,0 @@ -package app.criteria.array - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.ArrayType - -@ToString -@GrailsCompileStatic -class Like { - - static belongsTo = User - - Integer[] favoriteNumbers = [] - Long[] favoriteLongNumbers = [] - String[] favoriteMovies = [] - Juice[] favoriteJuices = [] - Double[] favoriteDoubleNumbers = [] - Float[] favoriteFloatNumbers = [] - UUID[] favoriteMovieUUIDs = [] - - static enum Juice { - ORANGE(0), - APPLE(1), - GRAPE(2), - PINEAPPLE(3), - TOMATO(4), - GRAPEFRUIT(5), - CRANBERRY(6), - CARROT(7), - LEMON(8) - - private final int id - - Juice(int id) { this.id = id } - } - - static mapping = { - table('pg_extensions_like') - favoriteNumbers(type: ArrayType, params: [type: Integer]) - favoriteMovies(type: ArrayType, params: [type: String]) - favoriteLongNumbers(type: ArrayType, params: [type: Long]) - favoriteJuices(type: ArrayType, params: [type: Juice]) - favoriteFloatNumbers(type: ArrayType, params: [type: Float]) - favoriteDoubleNumbers(type: ArrayType, params: [type: Double]) - favoriteMovieUUIDs(type: ArrayType, params: [type: UUID]) - } - -} diff --git a/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy b/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy deleted file mode 100644 index f9fba91..0000000 --- a/test-apps/app2/grails-app/domain/app/criteria/array/User.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package app.criteria.array - -import grails.compiler.GrailsCompileStatic - -@GrailsCompileStatic -class User { - - String name - Like like - - static mapping = { - table('pg_extensions_user') - } - - String toString() { - name - } -} diff --git a/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy b/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy deleted file mode 100644 index fd24582..0000000 --- a/test-apps/app2/grails-app/domain/app/hstore/TestHstoreMap.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package app.hstore - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.HstoreMapType - -@ToString -@GrailsCompileStatic -class TestHstoreMap { - - String name - Integer luckyNumber - - Map testAttributes - - static constraints = { - name(nullable: true) - luckyNumber(nullable: true) - } - - static mapping = { - testAttributes(type: HstoreMapType) - } -} diff --git a/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy b/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy deleted file mode 100644 index 828cb7b..0000000 --- a/test-apps/app2/grails-app/domain/app/json/TestMapJson.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.json - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.JsonMapType - -@ToString -@GrailsCompileStatic -class TestMapJson { - - Map data - - static constraints = { - data(nullable: true) - } - - static mapping = { - data(type: JsonMapType) - } -} diff --git a/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy b/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy deleted file mode 100644 index 5f40e4c..0000000 --- a/test-apps/app2/grails-app/domain/app/json/TestMapJsonb.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package app.json - -import groovy.transform.ToString - -import grails.compiler.GrailsCompileStatic - -import gpc.pgext.hibernate.usertype.JsonbMapType - -@ToString -@GrailsCompileStatic -class TestMapJsonb { - - Map data - - static constraints = { - data(nullable: true) - } - - static mapping = { - data(type: JsonbMapType) - } -} diff --git a/test-apps/app2/grails-app/init/app/Application.groovy b/test-apps/app2/grails-app/init/app/Application.groovy deleted file mode 100644 index 0931f30..0000000 --- a/test-apps/app2/grails-app/init/app/Application.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package app - -import groovy.transform.CompileStatic - -import grails.boot.GrailsApp -import grails.boot.config.GrailsAutoConfiguration - -@CompileStatic -class Application extends GrailsAutoConfiguration { - static void main(String[] args) { - GrailsApp.run(Application, args) - } -} \ No newline at end of file diff --git a/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy deleted file mode 100644 index c0357ea..0000000 --- a/test-apps/app2/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy +++ /dev/null @@ -1,52 +0,0 @@ -package app.criteria.array - -class PgArrayTestSearchService { - - List search(String field, String criteriaName, Object value) { - Like.withCriteria { - "$criteriaName"(field, value) - } as List - } - - List search(String field, String criteriaName) { - Like.withCriteria { - "$criteriaName"(field) - } as List - } - - List searchWithJoin(String field, String criteriaName, Object value) { - User.withCriteria { - createAlias('like', 'l') - "$criteriaName"('l.' + field, value) - } as List - } - - List searchWithJoin(String field, String criteriaName) { - User.withCriteria { - createAlias('like', 'l') - "$criteriaName"('l.' + field) - } as List - } - - List searchWithJoinByStringOrInteger(Map params, String criteriaName) { - User.withCriteria { - createAlias('like', 'l') - or { - params.each { k, v -> - "$criteriaName"('l.' + k, v) - } - } - } as List - } - - List searchWithJoinAnd(Map params, String criteriaName) { - User.withCriteria { - createAlias('like', 'l') - and { - params.each { k, v -> - "$criteriaName"('l.' + k, v) - } - } - } as List - } -} diff --git a/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy deleted file mode 100644 index b4961cb..0000000 --- a/test-apps/app2/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy +++ /dev/null @@ -1,12 +0,0 @@ -package app.criteria.hstore - -import app.hstore.TestHstoreMap - -class PgHstoreTestSearchService { - - List search(String field, String criteriaName, Object value) { - TestHstoreMap.withCriteria { - "$criteriaName"(field, value) - } as List - } -} diff --git a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy deleted file mode 100644 index e6ee0bd..0000000 --- a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package app.criteria.json - -import app.json.TestMapJson - -class PgJsonTestSearchService { - - List search(String criteriaName, String field, String jsonAttribute, Object value) { - TestMapJson.withCriteria { - "$criteriaName"(field, jsonAttribute, value.toString()) - } as List - } - - List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, Object value) { - TestMapJson.withCriteria { - "$criteriaName"(field, jsonOp, jsonAttribute, sqlOp, value.toString()) - } as List - } -} diff --git a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy b/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy deleted file mode 100644 index c323b57..0000000 --- a/test-apps/app2/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package app.criteria.json - -import app.json.TestMapJsonb - -class PgJsonbTestSearchService { - - List search(String criteriaName, String field, Object value) { - TestMapJsonb.withCriteria { - "$criteriaName"(field, value) - } as List - } - - List search(String criteriaName, String field, String jsonAttribute, Object value) { - TestMapJsonb.withCriteria { - "$criteriaName"(field, jsonAttribute, value.toString()) - } as List - } - - List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, Object value) { - TestMapJsonb.withCriteria { - "$criteriaName"(field, jsonOp, jsonAttribute, sqlOp, value.toString()) - } as List - } -} diff --git a/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy b/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy deleted file mode 100644 index 8b28213..0000000 --- a/test-apps/app2/grails-app/services/app/order/PgOrderService.groovy +++ /dev/null @@ -1,21 +0,0 @@ -package app.order - -import app.json.TestMapJsonb - -import static gpc.pgext.hibernate.order.OrderByRandom.byRandom -import static gpc.pgext.hibernate.order.OrderBySqlFormula.sqlFormula - -class PgOrderService { - - List orderByJson() { - TestMapJsonb.withCriteria { - order(sqlFormula('(data->\'name\') desc')) - } as List - } - - List orderByRandom() { - TestMapJsonb.withCriteria { - order(byRandom()) - } as List - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index d6f5881..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,348 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import org.hibernate.HibernateException -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'search #number in an array of integers'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [5, 17, 9, 6, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 3 | 2 - 17 | 1 - 9 | 2 - 4 | 2 - 1 | 0 - 20 | 4 - [3, 4] | 1 - [3, 4, 7] | 0 - [4] | 2 - [3, 20] | 2 - [] | 4 - 3 as Integer[] | 2 - [] as Integer[] | 4 - [3, 4] as Integer[] | 1 - } - - @Unroll - void 'search #number in an array of longs'() { - setup: - new Like(favoriteLongNumbers: [12383L, 2392348L, 3498239L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [12383L, 98978L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [-983893849L, 398432423L, 98978L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [12383L]).save(flush: true, failOnError: true) - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12383L | 3 - 98978L | 2 - -983893849L | 1 - 48574L | 0 - [12383L, 98978L] | 1 - [12383L] | 3 - [] | 4 - [12383L, 98978L] as Long[] | 1 - [12383L] as Long[] | 3 - [] as Long[] | 4 - } - - @Unroll - void 'search #number in an array of floats'() { - setup: - new Like(favoriteFloatNumbers: [12383f, 2392348f, 3498239f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12383f, 98978f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [-983893849f, 398432423f, 98978f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12383f]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12383f | 3 - 98978f | 2 - -983893849f | 1 - 48574f | 0 - [12383f, 98978f] | 1 - [12383f] | 3 - [] | 4 - [12383f, 98978f] as Float[] | 1 - [12383f] as Float[] | 3 - [] as Float[] | 4 - } - - @Unroll - void 'search #number in an array of double'() { - setup: - new Like(favoriteDoubleNumbers: [12383d, 2392348d, 3498239d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12383d, 98978d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [-983893849d, 398432423d, 98978d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12383d]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12383d | 3 - 98978d | 2 - -983893849d | 1 - 48574d | 0 - [12383d, 98978d] | 1 - [12383d] | 3 - [] | 4 - [12383d, 98978d] as Double[] | 1 - [12383d] as Double[] | 3 - [] as Double[] | 4 - } - - @Unroll - void 'search #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayContains', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - 'The Matrix' | 1 - 'The Lord of the Rings' | 2 - 'Blade Runner' | 2 - 'Star Wars' | 2 - 'The Usual Suspects' | 0 - ['Star Wars', 'Romeo & Juliet'] | 1 - ['The Lord of the Rings'] | 2 - [] | 4 - ['Star Wars', 'Romeo & Juliet'] as String[] | 1 - ['The Lord of the Rings'] as String[] | 2 - [] as String[] | 4 - } - - @Unroll - void 'search #movie in an array of UUIDs'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayContains', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - UuidBuilder.createUUID('The Matrix') | 1 - UuidBuilder.createUUID('The Lord of the Rings') | 2 - UuidBuilder.createUUID('Blade Runner') | 2 - UuidBuilder.createUUID('Star Wars') | 2 - UuidBuilder.createUUID('The Usual Suspects') | 0 - UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) | 1 - UuidBuilder.createUUIDs(['The Lord of the Rings']) | 2 - [] | 4 - UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) as UUID[] | 1 - UuidBuilder.createUUIDs(['The Lord of the Rings']) as UUID[] | 2 - [] as UUID[] | 4 - } - - @Unroll - void 'search #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayContains', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - Like.Juice.CRANBERRY | 1 - Like.Juice.ORANGE | 2 - Like.Juice.LEMON | 0 - Like.Juice.APPLE | 1 - Like.Juice.GRAPE | 2 - Like.Juice.PINEAPPLE | 1 - Like.Juice.TOMATO | 2 - Like.Juice.CARROT | 3 - Like.Juice.GRAPEFRUIT | 0 - [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 - [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 1 - [Like.Juice.CARROT] | 3 - [Like.Juice.CARROT, Like.Juice.TOMATO] | 2 - [] | 4 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayContains', movie) - - then: - result.size() == 2 - result.contains(user2) - result.contains(user3) - - where: - movie = 'Star Wars' - } - - void 'search in an array of strings with join with another domain class and or statement'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayContains', favoriteMovies: movie, favoriteNumbers: number) - - then: - result.size() == 3 - result.contains(user2) - result.contains(user3) - result.contains(user4) - - where: - movie = "Star Wars" - number = 4 - } - - void 'search an invalid list inside the array of integers'() { - when: - pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayContains', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1, 'Test'], [1L], [1, 1L]] - } - - void 'search an invalid list inside the array of long'() { - when: - pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayContains', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1L, 'Test'], [1], [1L, 1]] - } - - void 'search an invalid list inside the array of float'() { - when: - pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayContains', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1f, 'Test'], [1], [1f, 1]] - } - - void 'search an invalid list inside the array of double'() { - when: - pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayContains', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1d, 'Test'], [1], [1d, 1]] - } - - void 'search an invalid list inside the array of string'() { - when: - pgArrayTestSearchService.search('favoriteMovies', 'pgArrayContains', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', 1], [1L], ['Test', 1L]] - } - - void 'search an invalid list inside the array of UUID'() { - when: - pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayContains', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] - } - - void 'search an invalid list inside the array of enum'() { - when: - pgArrayTestSearchService.search('favoriteJuices', 'pgArrayContains', juice) - - then: - thrown HibernateException - - where: - juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index b71b070..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,328 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import org.hibernate.HibernateException -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'check equals for #number in an array of integers'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [17]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 3 | 0 - 17 | 1 - [3, 7, 20] | 1 - [3, 4, 20] | 1 - [4, 20, 3] | 0 - [] | 1 - 17 as Integer[] | 1 - [] as Integer[] | 1 - [3, 4, 20] as Integer[] | 1 - } - - @Unroll - void 'check equals for #number in an array of longs'() { - setup: - new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [-9L, 16L, 7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1L | 1 - 7L | 1 - [-9L, 16L, 7L] | 1 - [16L, -9L, 7L] | 0 - [1L] | 1 - [] | 1 - 7L as Long[] | 1 - } - - @Unroll - void 'check equals for #number in an array of floats'() { - setup: - new Like(favoriteFloatNumbers: [12f, 23f, 34f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12f, 98f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [-98f, 39f, 97]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12f | 1 - [12f, 98f] | 1 - [98f, 12f] | 0 - [12f] | 1 - [] | 1 - [] as Float[] | 1 - [12f, 98f] as Float[] | 1 - } - - @Unroll - void 'check equals for #number in an array of doubles'() { - setup: - new Like(favoriteDoubleNumbers: [12d, 23d, 34d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12d, 98d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [-98d, 39d, 97d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12d | 1 - [12d] | 1 - [12d, 98d] | 1 - [] | 1 - [39d, 97d, -98d] | 0 - [12d] as Double[] | 1 - [] as Double[] | 1 - } - - @Unroll - void 'check equals for #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayEquals', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - 'Star Wars' | 1 - ['Star Wars'] | 1 - ['Star Wars'] as String[] | 1 - 'The Usual Suspects' | 0 - ['Spiderman', 'Blade Runner', 'Star Wars'] | 1 - [] | 1 - ['The Matrix', 'The Lord of the Rings'] as String[] | 1 - ['The Lord of the Rings', 'The Matrix'] as String[] | 0 - [] as String[] | 1 - } - - @Unroll - void 'check equals for #movie in an array of UUIDs'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayEquals', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - UuidBuilder.createUUID('Star Wars') | 1 - UuidBuilder.createUUIDs(['Star Wars']) | 1 - UuidBuilder.createUUIDs(['Star Wars']) as UUID[] | 1 - UuidBuilder.createUUID('The Usual Suspects') | 0 - UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars']) | 1 - [] | 1 - UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings']) as UUID[] | 1 - UuidBuilder.createUUIDs(['The Lord of the Rings', 'The Matrix']) as UUID[] | 0 - [] as UUID[] | 1 - } - - @Unroll - void 'check equals for #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayEquals', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - Like.Juice.PINEAPPLE | 1 - [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 - [] | 1 - [Like.Juice.GRAPE, Like.Juice.ORANGE] | 0 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayEquals', movie) - - then: - result.size() == 2 - result.contains(user2) - result.contains(user3) - - where: - movie = ['Spiderman', 'Blade Runner', 'Star Wars'] - } - - void 'search in an array of strings with join with another domain class and or statement'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayEquals', favoriteMovies: movie, favoriteNumbers: number) - - then: - result.size() == 3 - result.contains(user2) - result.contains(user3) - result.contains(user4) - - where: - movie = ['Spiderman', 'Blade Runner', 'Star Wars'] - number = [9, 4] - } - - void 'search an invalid list inside the array of integers'() { - when: - pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1, 'Test'], [1L], [1, 1L]] - } - - void 'search an invalid list inside the array of long'() { - when: - pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1L, 'Test'], [1], [1L, 1]] - } - - void 'search an invalid list inside the array of float'() { - when: - pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1f, 'Test'], [1], [1f, 1]] - } - - void 'search an invalid list inside the array of double'() { - when: - pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1d, 'Test'], [1], [1d, 1]] - } - - void 'search an invalid list inside the array of string'() { - when: - pgArrayTestSearchService.search('favoriteMovies', 'pgArrayEquals', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', 1], [1L], ['Test', 1L]] - } - - void 'search an invalid list inside the array of UUID'() { - when: - pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayEquals', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] - } - - void 'search an invalid list inside the array of enum'() { - when: - pgArrayTestSearchService.search('favoriteJuices', 'pgArrayEquals', juice) - - then: - thrown HibernateException - - where: - juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index 1b19c85..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgILikeCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'check ilike for #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayILike', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - '%tar War%' | 2 - '%ider%' | 1 - '%Suspects%' | 0 - '' | 0 - '%' | 5 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index 8ce46c3..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,344 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import org.hibernate.HibernateException -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'search #number in an array of integers'() { - setup: - new Like(favoriteNumbers: [1, 2, 3, 4, 5]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [4, 5, 6, 7]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [4, 5]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [1, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [2]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsContainedBy', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1 | 0 - 2 | 1 - [1, 20] | 1 - [4, 5, 6, 7] | 2 - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | 4 - [] | 0 - [1, 20] as Integer[] | 1 - [4, 5, 6, 7] as Integer[] | 2 - [] as Integer[] | 0 - } - - - @Unroll - void 'search #number in an array of long'() { - setup: - new Like(favoriteLongNumbers: [1L, 2L, 3L, 4L, 5L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [4L, 5L, 6L, 7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [4L, 5L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [1L, 20L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [2L]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsContainedBy', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1L | 0 - 2L | 1 - [1L, 20L] | 1 - [4L, 5L, 6L, 7L] | 2 - [1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L] | 4 - [] | 0 - [1L, 20L] as Long[] | 1 - [4L, 5L, 6L, 7L] as Long[] | 2 - [] as Long[] | 0 - } - - @Unroll - void 'search #number in an array of float'() { - setup: - new Like(favoriteFloatNumbers: [1f, 2f, 3f, 4f, 5f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [4f, 5f, 6f, 7f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [4f, 5f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [1f, 20f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [2f]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsContainedBy', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1f | 0 - 2f | 1 - [1f, 20f] | 1 - [4f, 5f, 6f, 7f] | 2 - [1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f] | 4 - [] | 0 - [1f, 20f] as Float[] | 1 - [4f, 5f, 6f, 7f] as Float[] | 2 - [] as Float[] | 0 - } - - - @Unroll - void 'search #number in an array of double'() { - setup: - new Like(favoriteDoubleNumbers: [1d, 2d, 3d, 4d, 5d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [4d, 5d, 6d, 7d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [4d, 5d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [1d, 20d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [2d]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsContainedBy', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1d | 0 - 2d | 1 - [1d, 20d] | 1 - [4d, 5d, 6d, 7d] | 2 - [1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d] | 4 - [] | 0 - [1d, 20d] as Double[] | 1 - [4d, 5d, 6d, 7d] as Double[] | 2 - [] as Double[] | 0 - } - - @Unroll - void 'search #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['A', 'B', 'C', 'D', 'E']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['D', 'E', 'F', 'G']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['A', 'Z']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['B']).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsContainedBy', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - 'A' | 0 - 'B' | 1 - ['A', 'Z'] | 1 - ['B', 'D', 'E', 'F', 'G'] | 2 - ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Z'] | 4 - [] | 0 - ['A', 'Z'] as String[] | 1 - ['B', 'D', 'E', 'F', 'G'] as String[] | 2 - [] as String[] | 0 - } - - @Unroll - void 'search #movie in an array of UUIDs'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['A', 'B', 'C', 'D', 'E'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['D', 'E', 'F', 'G'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['A', 'Z'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['B'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsContainedBy', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - UuidBuilder.createUUID('A') | 0 - UuidBuilder.createUUID('B') | 1 - UuidBuilder.createUUIDs(['A', 'Z']) | 1 - UuidBuilder.createUUIDs(['B', 'D', 'E', 'F', 'G']) | 2 - UuidBuilder.createUUIDs(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Z']) | 4 - [] | 0 - UuidBuilder.createUUIDs(['A', 'Z']) as UUID[] | 1 - UuidBuilder.createUUIDs(['B', 'D', 'E', 'F', 'G']) as UUID[] | 2 - [] as UUID[] | 0 - } - - @Unroll - void 'search #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.APPLE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.CARROT]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsContainedBy', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - Like.Juice.CRANBERRY | 0 - Like.Juice.ORANGE | 1 - Like.Juice.LEMON | 0 - Like.Juice.APPLE | 1 - Like.Juice.GRAPE | 0 - Like.Juice.PINEAPPLE | 0 - Like.Juice.TOMATO | 0 - Like.Juice.CARROT | 0 - Like.Juice.GRAPEFRUIT | 0 - [Like.Juice.ORANGE, Like.Juice.GRAPE] | 1 - [Like.Juice.GRAPE, Like.Juice.PINEAPPLE, - Like.Juice.ORANGE, Like.Juice.CARROT] | 2 - [Like.Juice.APPLE] | 1 - [Like.Juice.CARROT, Like.Juice.ORANGE] | 2 - [] | 0 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'Abe', like: new Like(favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Bernard', like: new Like(favoriteMovies: ['A', 'C'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Carl', like: new Like(favoriteMovies: ['A'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Dave', like: new Like(favoriteMovies: ['A', 'C', 'D'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsContainedBy', movies) - - then: - result.size() == 2 - result.contains(user2) - result.contains(user3) - - where: - movies = ['A', 'B', 'C'] - } - - void 'search in an array of strings with join with another domain class and or statement'() { - setup: - def user1 = new User(name: 'Abe', like: new Like(favoriteNumbers: [1, 3], favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Bernard', like: new Like(favoriteNumbers: [1], favoriteMovies: ['A', 'C'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Carl', like: new Like(favoriteNumbers: [2], favoriteMovies: ['A'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Dave', like: new Like(favoriteNumbers: [1, 2], favoriteMovies: ['A', 'B', 'D'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayIsContainedBy', favoriteMovies: movies, favoriteNumbers: numbers) - - then: - result.size() == 3 - result.contains(user2) - result.contains(user3) - result.contains(user4) - - where: - movies = ['A', 'B', 'C'] - numbers = [1, 2] - } - - void 'search a invalid list inside the array of integers'() { - when: - pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsContainedBy', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1, 'Test'], [1L], [1, 1L]] - } - - void 'search a invalid list inside the array of long'() { - when: - pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsContainedBy', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1L, 'Test'], [1], [1L, 1]] - } - - void 'search a invalid list inside the array of float'() { - when: - pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsContainedBy', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1f, 'Test'], [1], [1f, 1]] - } - - void 'search a invalid list inside the array of double'() { - when: - pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsContainedBy', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1d, 'Test'], [1], [1d, 1]] - } - - void 'search a invalid list inside the array of string'() { - when: - pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsContainedBy', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', 1], [1L], ['Test', 1L]] - } - - void 'search an invalid list inside the array of UUID'() { - when: - pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsContainedBy', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] - } - - void 'search an invalid list juice inside the array of enum'() { - when: - pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsContainedBy', juice) - - then: - thrown HibernateException - - where: - juice << [['Test'], ['Test', Like.Juice.ORANGE], [1L], ['Test', 1L]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index 1a692d0..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,130 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - void 'search for empty integer arrays'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty long arrays'() { - setup: - new Like(favoriteLongNumbers: [3L, 7L, 20L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty float arrays'() { - setup: - new Like(favoriteFloatNumbers: [3f, 7f, 20f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty double arrays'() { - setup: - new Like(favoriteDoubleNumbers: [3d, 7d, 20d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty string arrays'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty UUID arrays'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search for empty enum arrays'() { - setup: - new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO]).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsEmpty') - - then: - result.size() == 2 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsEmpty') - - then: - result.size() == 2 - result.contains(user2) - result.contains(user3) - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index 5b0ce99..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,212 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'search #number in an array of integers'() { - setup: - def l1 = new Like(favoriteNumbers: [3, 7]).save(flush: true, failOnError: true) - def l2 = new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - def l3 = new Like(favoriteNumbers: [3, 4]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsEmptyOrContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - [] | 1 - [3] | 2 - [4] | 1 - [3, 4] | 1 - [] as Integer[] | 1 - [3] as Integer[] | 2 - [3, 4] as Integer[] | 1 - } - - @Unroll - void 'search #number in an array of longs'() { - setup: - new Like(favoriteLongNumbers: [12383L, 2392348L, 3498239L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [12383L, 98978L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [12383L]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsEmptyOrContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - [12383L, 98978L] | 1 - [12383L] | 3 - [] | 1 - [12383L, 98978L] as Long[] | 1 - [12383L] as Long[] | 3 - [] as Long[] | 1 - } - - @Unroll - void 'search #number in an array of floats'() { - setup: - new Like(favoriteFloatNumbers: [12383f, 2392348f, 3498239f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12383f, 98978f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12383f]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsEmptyOrContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - [12383f, 98978f] | 1 - [12383f] | 3 - [] | 1 - [12383f, 98978f] as Float[] | 1 - [12383f] as Float[] | 3 - [] as Float[] | 1 - } - - @Unroll - void 'search #number in an array of doubles'() { - setup: - new Like(favoriteDoubleNumbers: [12383d, 2392348d, 3498239d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12383d, 98978d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12383d]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsEmptyOrContains', number) - - then: - result.size() == resultSize - - where: - number | resultSize - [12383d, 98978d] | 1 - [12383d] | 3 - [] | 1 - [12383d, 98978d] as Double[] | 1 - [12383d] as Double[] | 3 - [] as Double[] | 1 - } - - @Unroll - void 'search #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsEmptyOrContains', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - ['The Matrix'] | 1 - ['Star Wars', 'Romeo & Juliet'] | 1 - ['The Lord of the Rings'] | 2 - [] | 1 - ['Star Wars', 'Romeo & Juliet'] as String[] | 1 - ['The Lord of the Rings'] as String[] | 2 - [] as String[] | 1 - } - - @Unroll - void 'search #movie in an array of UUIDs'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsEmptyOrContains', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - UuidBuilder.createUUIDs(['The Matrix']) | 1 - UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) | 1 - UuidBuilder.createUUIDs(['The Lord of the Rings']) | 2 - [] | 1 - UuidBuilder.createUUIDs(['Star Wars', 'Romeo & Juliet']) as UUID[] | 1 - UuidBuilder.createUUIDs(['The Lord of the Rings']) as UUID[] | 2 - [] as UUID[] | 1 - } - - @Unroll - void 'search #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsEmptyOrContains', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - [Like.Juice.ORANGE] | 2 - [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 1 - [] | 1 - } - - @Unroll - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'Spiderman'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsEmptyOrContains', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - ['The Matrix'] | 1 - ['Blade Runner', 'Spiderman'] | 2 - [] | 1 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index 697837b..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,129 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - void 'search for empty integer arrays'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty long arrays'() { - setup: - new Like(favoriteLongNumbers: [3L, 7L, 20L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty float arrays'() { - setup: - new Like(favoriteFloatNumbers: [3f, 7f, 20f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty double arrays'() { - setup: - new Like(favoriteDoubleNumbers: [3d, 7d, 20d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty string arrays'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty UUID arrays'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search for empty enum arrays'() { - setup: - new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO]).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: [])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayIsNotEmpty') - - then: - result.size() == 1 - result.contains(user1) - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index e79b5f3..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,310 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import org.hibernate.HibernateException -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'check equals for #number in an array of integers'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [17]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayNotEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 3 | 4 - 17 | 3 - [3, 7, 20] | 3 - [7, 3, 20] | 4 - [] | 3 - 17 as Integer[] | 3 - [] as Integer[] | 3 - } - - @Unroll - void 'check equals for #number in an array of longs'() { - setup: - new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayNotEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1L | 3 - [1L, 23L, 34L] | 3 - [1L, 34L, 23L] | 4 - [] | 3 - 7L as Long[] | 3 - } - - @Unroll - void 'check equals for #number in an array of floats'() { - setup: - new Like(favoriteFloatNumbers: [12f, 23f, 34f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12f, 98f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [12f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayNotEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12f | 3 - [12f, 98f] | 3 - [98f, 12f] | 4 - [12f] | 3 - [] | 3 - [] as Float[] | 3 - } - - @Unroll - void 'check equals for #number in an array of doubles'() { - setup: - new Like(favoriteDoubleNumbers: [12d, 23d, 34d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12d, 98d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [12d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayNotEquals', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 12d | 3 - [12d] | 3 - [12d, 98d] | 3 - [98d, 12d] | 4 - [] | 3 - [12d] as Double[] | 3 - } - - @Unroll - void 'check equals for #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayNotEquals', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - 'Star Wars' | 3 - ['Star Wars'] | 3 - ['Star Wars'] as String[] | 3 - 'The Usual Suspects' | 4 - ['Spiderman', 'Blade Runner', 'Star Wars'] | 3 - ['Spiderman', 'Star Wars', 'Blade Runner'] | 4 - [] | 3 - } - - @Unroll - void 'check equals for #movie in an array of uuids'() { - setup: - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(['Star Wars'])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayNotEquals', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - UuidBuilder.createUUID('Star Wars') | 3 - UuidBuilder.createUUIDs(['Star Wars']) | 3 - UuidBuilder.createUUIDs(['Star Wars']) as UUID[] | 3 - UuidBuilder.createUUID('The Usual Suspects') | 4 - UuidBuilder.createUUIDs(['Spiderman', 'Blade Runner', 'Star Wars']) | 3 - UuidBuilder.createUUIDs(['Spiderman', 'Star Wars', 'Blade Runner']) | 4 - [] | 3 - } - - @Unroll - void 'check equals for #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - new Like(favoriteJuices: []).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayNotEquals', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - Like.Juice.PINEAPPLE | 3 - [Like.Juice.ORANGE, Like.Juice.GRAPE] | 3 - [Like.Juice.GRAPE, Like.Juice.ORANGE] | 4 - [] | 3 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayNotEquals', movie) - - then: - result.size() == 2 - result.contains(user1) - result.contains(user4) - - where: - movie = ['Spiderman', 'Blade Runner', 'Star Wars'] - } - - void 'search in an array of strings with join with another domain class and or statement'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoinByStringOrInteger('pgArrayNotEquals', favoriteMovies: movie, favoriteNumbers: number) - - then: - result.size() == 2 - result.contains(user1) - result.contains(user4) - - where: - movie = ['Spiderman', 'Blade Runner', 'Star Wars'] - number = [5, 17, 9, 6] - } - - void 'search an invalid list inside the array of integers'() { - when: - pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayNotEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1, 'Test'], [1L], [1, 1L]] - } - - void 'search an invalid list inside the array of long'() { - when: - pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayNotEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1L, 'Test'], [1], [1L, 1]] - } - - void 'search an invalid list inside the array of float'() { - when: - pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayNotEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1f, 'Test'], [1], [1f, 1]] - } - - void 'search an invalid list inside the array of double'() { - when: - pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayNotEquals', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1d, 'Test'], [1], [1d, 1]] - } - - void 'search an invalid list inside the array of string'() { - when: - pgArrayTestSearchService.search('favoriteMovies', 'pgArrayNotEquals', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', 1], [1L], ['Test', 1L], [UUID.randomUUID()]] - } - - void 'search an invalid list inside the array of UUID'() { - when: - pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayNotEquals', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] - } - - void 'search an invalid list inside the array of enum'() { - when: - pgArrayTestSearchService.search('favoriteJuices', 'pgArrayNotEquals', juice) - - then: - thrown HibernateException - - where: - juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index ba8ff80..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,303 +0,0 @@ -package test.array - -import app.criteria.array.Like -import app.criteria.array.PgArrayTestSearchService -import app.criteria.array.User -import org.hibernate.HibernateException -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { - - @Autowired - PgArrayTestSearchService pgArrayTestSearchService - - def setup() { - User.executeUpdate('delete from User') - Like.executeUpdate('delete from Like') - } - - @Unroll - void 'overlaps #number in an array of integers'() { - setup: - new Like(favoriteNumbers: [3, 7, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [5, 17, 9, 6, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [3, 4, 20]).save(flush: true, failOnError: true) - new Like(favoriteNumbers: [9, 4, 20]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayOverlaps', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 3 | 2 - 17 | 1 - 9 | 2 - 4 | 2 - 1 | 0 - 20 | 4 - [3, 4] | 3 - [3, 4, 7] | 3 - [4] | 2 - [3, 20] | 4 - [] | 0 - [4] as Integer[] | 2 - [3, 20] as Integer[] | 4 - [] as Integer[] | 0 - } - - @Unroll - void 'overlaps #number in an array of longs'() { - setup: - new Like(favoriteLongNumbers: [1L, 23L, 34L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [1L, 7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [-9L, 16L, 7L]).save(flush: true, failOnError: true) - new Like(favoriteLongNumbers: [1L]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayOverlaps', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1L | 3 - 7L | 2 - -9L | 1 - 100L | 0 - [1L, 7L] | 4 - [1L] | 3 - [] | 0 - [1L, 7L] as Long[] | 4 - [1L] as Long[] | 3 - [] as Long[] | 0 - } - - @Unroll - void 'overlaps #number in an array of floats'() { - setup: - new Like(favoriteFloatNumbers: [1f, 23f, 34f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [1f, 7f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [-9f, 16f, 7f]).save(flush: true, failOnError: true) - new Like(favoriteFloatNumbers: [1f]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteFloatNumbers', 'pgArrayOverlaps', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1f | 3 - 7f | 2 - -9f | 1 - 100f | 0 - [1f, 7f] | 4 - [1f] | 3 - [] | 0 - [1f, 7f] as Float[] | 4 - [1f] as Float[] | 3 - [] as Float[] | 0 - } - - @Unroll - void 'overlaps #number in an array of double'() { - setup: - new Like(favoriteDoubleNumbers: [1d, 23d, 34d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [1d, 7d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [-9d, 16d, 7d]).save(flush: true, failOnError: true) - new Like(favoriteDoubleNumbers: [1d]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteDoubleNumbers', 'pgArrayOverlaps', number) - - then: - result.size() == resultSize - - where: - number | resultSize - 1d | 3 - 7d | 2 - -9d | 1 - 100d | 0 - [1d, 7d] | 4 - [1d] | 3 - [] | 0 - [1d, 7d] as Double[] | 4 - [1d] as Double[] | 3 - [] as Double[] | 0 - } - - @Unroll - void 'search #movie in an array of strings'() { - setup: - new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars']).save(flush: true, failOnError: true) - new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings']).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteMovies', 'pgArrayOverlaps', movie) - - then: - result.size() == resultSize - - where: - movie | resultSize - 'The Matrix' | 1 - 'The Lord of the Rings' | 2 - 'Blade Runner' | 2 - 'Star Wars' | 2 - 'The Usual Suspects' | 0 - ['Star Wars', 'Romeo & Juliet'] | 3 - ['The Lord of the Rings'] | 2 - [] | 0 - ['Star Wars', 'Romeo & Juliet'] as String[] | 3 - ['The Lord of the Rings'] as String[] | 2 - [] as String[] | 0 - } - - @Unroll - void 'search #juice in an array of enums'() { - setup: - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.GRAPE]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.PINEAPPLE, Like.Juice.GRAPE, Like.Juice.CARROT, Like.Juice.CRANBERRY]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.APPLE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - new Like(favoriteJuices: [Like.Juice.ORANGE, Like.Juice.TOMATO, Like.Juice.CARROT]).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.search('favoriteJuices', 'pgArrayOverlaps', juice) - - then: - result.size() == resultSize - - where: - juice | resultSize - Like.Juice.CRANBERRY | 1 - Like.Juice.ORANGE | 2 - Like.Juice.LEMON | 0 - Like.Juice.APPLE | 1 - Like.Juice.GRAPE | 2 - Like.Juice.PINEAPPLE | 1 - Like.Juice.TOMATO | 2 - Like.Juice.CARROT | 3 - Like.Juice.GRAPEFRUIT | 0 - [Like.Juice.ORANGE, Like.Juice.GRAPE] | 3 - [Like.Juice.GRAPE, Like.Juice.PINEAPPLE] | 2 - [Like.Juice.CARROT] | 3 - [Like.Juice.CARROT, Like.Juice.TOMATO] | 3 - [] | 0 - } - - void 'search in an array of strings with join with another domain class'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoin('favoriteMovies', 'pgArrayOverlaps', movie) - - then: - result.size() == 3 - result.contains(user2) - result.contains(user3) - result.contains(user4) - - where: - movie = ['Star Wars', 'Romeo & Juliet'] - } - - void 'search in an array of strings with join with another domain class and or statement'() { - setup: - def user1 = new User(name: 'John', like: new Like(favoriteNumbers: [3, 7], favoriteMovies: ['The Matrix', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - def user2 = new User(name: 'Peter', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ['Spiderman', 'Blade Runner', 'Star Wars'])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ['Romeo & Juliet', 'Casablanca', 'Star Wars'])).save(flush: true, failOnError: true) - def user4 = new User(name: 'Jonhny', like: new Like(favoriteNumbers: [9, 4], favoriteMovies: ['Romeo & Juliet', 'Blade Runner', 'The Lord of the Rings'])).save(flush: true, failOnError: true) - - when: - def result = pgArrayTestSearchService.searchWithJoinAnd('pgArrayOverlaps', favoriteMovies: movie, favoriteNumbers: number) - - then: - result.size() == 2 - result.contains(user2) - result.contains(user4) - - where: - movie = ['Star Wars', 'Romeo & Juliet'] - number = [9] - } - - @Unroll - void 'search a invalid list inside the array of integers'() { - when: - pgArrayTestSearchService.search('favoriteNumbers', 'pgArrayOverlaps', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1, 'Test'], [1L], [1, 1L]] - } - - @Unroll - void 'search a invalid list inside the array of long'() { - when: - pgArrayTestSearchService.search('favoriteLongNumbers', 'pgArrayOverlaps', number) - - then: - thrown HibernateException - - where: - number << [['Test'], [1L, 'Test'], [1], [1L, 1]] - } - - @Unroll - void 'search a invalid list inside the array of string'() { - when: - pgArrayTestSearchService.search('favoriteMovies', 'pgArrayOverlaps', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', 1], [1L], ['Test', 1L]] - } - - @Unroll - void 'search an invalid list inside the array of UUID'() { - when: - pgArrayTestSearchService.search('favoriteMovieUUIDs', 'pgArrayOverlaps', movie) - - then: - thrown HibernateException - - where: - movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] - } - - @Unroll - void 'search an invalid list inside the array of enum'() { - when: - pgArrayTestSearchService.search('favoriteJuices', 'pgArrayOverlaps', juice) - - then: - thrown HibernateException - - where: - juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy deleted file mode 100644 index 3233536..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy +++ /dev/null @@ -1,141 +0,0 @@ -package test.array - -import app.array.TestDouble -import app.array.TestEnum -import app.array.TestFloat -import app.array.TestInteger -import app.array.TestLong -import app.array.TestString -import app.array.TestUuid -import spock.lang.Specification -import spock.lang.Unroll - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PostgresqlArraysDomainIntegrationSpec extends Specification { - - def setup() { - TestInteger.executeUpdate('delete from TestInteger') - TestLong.executeUpdate('delete from TestLong') - TestFloat.executeUpdate('delete from TestFloat') - TestDouble.executeUpdate('delete from TestDouble') - TestUuid.executeUpdate('delete from TestUuid') - TestString.executeUpdate('delete from TestString') - TestEnum.executeUpdate('delete from TestEnum') - } - - @Unroll - void 'save a domain class with an integer array value #numbers'() { - setup: - def testInt = new TestInteger(integerNumbers: numbers) - - when: - testInt.save(flush: true, failOnError: true) - - then: - testInt.hasErrors() == false - testInt.integerNumbers?.length == numbers?.size() - - where: - numbers << [null, [], [5], [3, -1], [-9, 4, -123, 0]] - } - - @Unroll - void 'save a domain class with a long array value #numbers'() { - setup: - def testLong = new TestLong(longNumbers: numbers) - - when: - testLong.save(flush: true, failOnError: true) - - then: - testLong.hasErrors() == false - testLong.longNumbers?.length == numbers?.size() - - where: - numbers << [null, [], [5L], [3L, -1L], [-9L, 4L, -123L, 0L]] - } - - @Unroll - void 'save a domain class with a Float array value #numbers'() { - setup: - def testFloat = new TestFloat(floatNumbers: numbers) - - when: - testFloat.save(flush: true, failOnError: true) - - then: - testFloat.hasErrors() == false - testFloat.floatNumbers?.length == numbers?.size() - - where: - numbers << [null, [], [5f], [3f, -1f], [-9f, 4f, -123f, 0f]] - } - - @Unroll - void 'save a domain class with a Double array value #numbers'() { - setup: - def testDouble = new TestDouble(doubleNumbers: numbers) - - when: - testDouble.save(flush: true, failOnError: true) - - then: - testDouble.hasErrors() == false - testDouble.doubleNumbers?.length == numbers?.size() - - where: - numbers << [null, [], [5d], [3d, -1d], [-9d, 4d, -123d, 0d]] - } - - @Unroll - void 'save a domain class with an string array value #strings'() { - setup: - def testString = new TestString(stringArray: strings) - - when: - testString.save(flush: true, failOnError: true) - - then: - testString.hasErrors() == false - testString.stringArray?.length == strings?.size() - - where: - strings << [null, [], ['string 1'], ['string 1', 'string 2'], ['string 1', 'string 2', 'string 3']] - } - - @Unroll - void 'save a domain class with an UUID array value #uuids'() { - setup: - def testUuid = new TestUuid(uuidArray: uuids) - - when: - testUuid.save(flush: true, failOnError: true) - - then: - testUuid.hasErrors() == false - testUuid.uuidArray?.length == uuids?.size() - - where: - uuids << [null, [], [UUID.randomUUID()], [UUID.randomUUID(), UUID.randomUUID()], [UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()]] - } - - @Unroll - void 'save a domain class with an enum array value #days'() { - when: - // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke - // nullSafeGet on the corresponding user type and will not use current session's cache. - def id = TestEnum.withNewTransaction { - new TestEnum(days: days).save(flush: true, failOnError: true).id - } - - then: - TestEnum.get(id).days as List == days - - where: - days << [null, [], [TestEnum.Day.MONDAY], [TestEnum.Day.SUNDAY, TestEnum.Day.SATURDAY], [TestEnum.Day.WEDNESDAY, TestEnum.Day.THURSDAY, TestEnum.Day.TUESDAY]] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy b/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy deleted file mode 100644 index f9e66ae..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/array/UuidBuilder.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package test.array - -import groovy.transform.CompileStatic - -@CompileStatic -class UuidBuilder { - - static UUID createUUID(String name) { - UUID.nameUUIDFromBytes(name.bytes) - } - - static List createUUIDs(List names) { - names.collect { createUUID(it) } - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy deleted file mode 100644 index 2a0cfcc..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy +++ /dev/null @@ -1,115 +0,0 @@ -package test.hstore - -import app.criteria.hstore.PgHstoreTestSearchService -import app.hstore.TestHstoreMap -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgHstoreContainsIntegrationSpec extends Specification { - - @Autowired - PgHstoreTestSearchService pgHstoreTestSearchService - - def setup() { - TestHstoreMap.executeUpdate('delete from TestHstoreMap') - } - - void 'Test only one value result 2 different elements'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) - - then: - result.size() == 2 - result.find { it.name == 'test1' } - !result.find { it.name == 'test2' } - !result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - - where: - map = [b: '1'] - } - - void 'Test two values that matches partialy with one element'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) - - then: - result.size() == 1 - !result.find { it.name == 'test1' } - !result.find { it.name == 'test2' } - !result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - - where: - map = [b: '1', c: 'test'] - } - - void 'No matches with the same combination key/value'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) - - then: - result.size() == 0 - - where: - map = [b: '3'] - } - - void 'No matches with the same combination but one of the elements matches'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) - - then: - result.size() == 0 - - where: - map = [b: '1', c: 'test-otro'] - } - - void 'When empty map returns all elements'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContains', map) - - then: - result.size() == 4 - - where: - map = [:] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy deleted file mode 100644 index 9e7043e..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package test.hstore - -import app.criteria.hstore.PgHstoreTestSearchService -import app.hstore.TestHstoreMap -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgHstoreContainsKeyIntegrationSpec extends Specification { - - @Autowired - PgHstoreTestSearchService pgHstoreTestSearchService - - def setup() { - TestHstoreMap.executeUpdate('delete from TestHstoreMap') - } - - void 'Test find hstore that contains key'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContainsKey', 'b') - - then: - result.size() == 3 - result.find { it.name == 'test1' } - result.find { it.name == 'test2' } - !result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - } - - void 'Test find hstore that contains other key'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreContainsKey', 'X') - - then: - result.size() == 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy deleted file mode 100644 index a2d4a04..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package test.hstore - -import app.criteria.hstore.PgHstoreTestSearchService -import app.hstore.TestHstoreMap -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgHstoreILikeValueFunctionIntegrationSpec extends Specification { - - @Autowired - PgHstoreTestSearchService pgHstoreTestSearchService - - def setup() { - TestHstoreMap.executeUpdate('delete from TestHstoreMap') - } - - void 'Test find hstore that ilikes value'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '3']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreILikeValue', '%test%') - - then: - result.size() == 3 - result.find { it.name == 'test1' } - !result.find { it.name == 'test2' } - result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - } - - void 'Test find hstore that no ilikes value'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [b: '2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test2']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'Xa', b: '3']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreILikeValue', '%X') - - then: - result.size() == 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy deleted file mode 100644 index f9c53fa..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy +++ /dev/null @@ -1,81 +0,0 @@ -package test.hstore - -import app.criteria.hstore.PgHstoreTestSearchService -import app.hstore.TestHstoreMap -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgHstoreIsContainedIntegrationSpec extends Specification { - - @Autowired - PgHstoreTestSearchService pgHstoreTestSearchService - - def setup() { - TestHstoreMap.executeUpdate('delete from TestHstoreMap') - } - - void 'No element matches with the empty set'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) - - then: - result.size() == 0 - - where: - map = [:] - } - - void 'All elements matches'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) - - then: - result.size() == 4 - result.find { it.name == 'test1' } - result.find { it.name == 'test2' } - result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - - where: - map = [a: 'test', b: '1', c: 'test', d: '10'] - } - - void 'Some elements matches'() { - setup: - new TestHstoreMap(name: 'test1', testAttributes: [a: 'test', b: '1']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test2', testAttributes: [d: '10']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test3', testAttributes: [a: 'test']).save(flush: true, failOnError: true) - new TestHstoreMap(name: 'test4', testAttributes: [c: 'test', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgHstoreTestSearchService.search('testAttributes', 'pgHstoreIsContained', map) - - then: - result.size() == 3 - result.find { it.name == 'test1' } - !result.find { it.name == 'test2' } - result.find { it.name == 'test3' } - result.find { it.name == 'test4' } - - where: - map = [a: 'test', b: '1', c: 'test'] - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy deleted file mode 100644 index c350214..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy +++ /dev/null @@ -1,138 +0,0 @@ -package test.hstore - -import app.hstore.TestHstoreMap -import spock.lang.PendingFeature -import spock.lang.Specification -import spock.lang.Unroll - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { - - def setup() { - TestHstoreMap.executeUpdate('delete from TestHstoreMap') - } - - @Unroll - void 'save a domain class with a map. key: #data'() { - setup: - def testHstoreMap = new TestHstoreMap(testAttributes: data) - - when: - testHstoreMap.save(flush: true, failOnError: true) - - then: - !testHstoreMap.hasErrors() - testHstoreMap.testAttributes != null - testHstoreMap.testAttributes.size() == data.size() - testHstoreMap.testAttributes[attribute] == value - - where: - data | attribute | value - [foo: 'bar'] | 'foo' | 'bar' - ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' - } - - @Unroll - void 'recover a domain class with a map. key: #data'() { - setup: - new TestHstoreMap(testAttributes: data).save(flush: true, failOnError: true) - - when: - def testHstoreMap = TestHstoreMap.findAll().first() - - then: - !testHstoreMap.hasErrors() - testHstoreMap.testAttributes != null - testHstoreMap.testAttributes.size() == data.size() - testHstoreMap.testAttributes[key] == value - - where: - data | key | value - [foo: 'bar', xxx: 'abc'] | 'foo' | 'bar' - ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' - } - - @Unroll - void 'remove a key in a map. key: #data, valueToRemove: #valueToRemove'() { - setup: - def testHstoreMap = new TestHstoreMap(testAttributes: data) - testHstoreMap.save(flush: true, failOnError: true) - - when: - testHstoreMap.testAttributes.remove(valueToRemove) - - then: - !testHstoreMap.hasErrors() - testHstoreMap.testAttributes != null - testHstoreMap.testAttributes.size() == size - - where: - data | valueToRemove | size - [foo: 'bar', xxx: 'abc'] | 'xxx' | 1 - ['foo,bar': 'baz,qux'] | 'foo,bar' | 0 - [foo: 'bar'] | 'xxx' | 1 - } - - @Unroll - void 'save and delete a domain class with a map. key: #data'() { - setup: - def testHstoreMap = new TestHstoreMap(testAttributes: data) - - when: 'I save an instance' - testHstoreMap.save(flush: true, failOnError: true) - - and: 'The instance is saved' - assert !testHstoreMap.hasErrors() - - and: 'I try to delete it' - testHstoreMap.delete(flush: true, failOnError: true) - - then: 'It shouldn\'t be present in database anymore' - TestHstoreMap.count() == 0 - - where: - data | attribute | value - [foo: 'bar'] | 'foo' | 'bar' - ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' - } - - void 'save a domain class with a empty map and validate that is not dirty right after retrieval'() { - setup: - def testHstoreMap = new TestHstoreMap(testAttributes: [:]) - - when: 'I save an instance' - testHstoreMap.save(flush: true, failOnError: true) - - and: 'The instance is saved' - assert !testHstoreMap.hasErrors() - - and: 'I retrieve it and check for dirty properties in a new session' - def retrievedTestHstoreMap = TestHstoreMap.get(testHstoreMap.id) - - then: 'It should not be dirty right after db retrieval' - !retrievedTestHstoreMap.isDirty() - } - - @PendingFeature(reason = 'seems dirty check does not work for Grails 3.3 with Hibernate 5.2 and GORM 6.1.9') - void 'save a domain class, modify it and validate that it is dirty'() { - setup: - def testHstoreMap = new TestHstoreMap(testAttributes: [:]) - - when: 'I save an instance' - testHstoreMap.save(flush: true, failOnError: true) - - and: 'The instance is saved' - assert !testHstoreMap.hasErrors() - - and: 'I retrieve it and modify a property' - def retrievedTestHstoreMap = TestHstoreMap.get(testHstoreMap.id) - retrievedTestHstoreMap.testAttributes << [foo: 'bar'] - - then: 'It should be dirty' - retrievedTestHstoreMap.isDirty() - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy deleted file mode 100644 index 8b2cf62..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonTestSearchService -import app.json.TestMapJson -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonEqualsIntegrationSpec extends Specification { - - @Autowired - PgJsonTestSearchService pgJsonTestSearchService - - def setup() { - TestMapJson.executeUpdate('delete from TestMapJson') - } - - @Unroll - void 'Test equals finding value: #value (json)'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true, failOnError: true) - - when: - def result = pgJsonTestSearchService.search('pgJsonHasFieldValue', 'data', 'name', value) - - then: - result.size() == size - result.every { it.data.name == value } - - where: - value || size - 'Iván' || 2 - 'John' || 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy deleted file mode 100644 index 3ba9b3d..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonTestSearchService -import app.json.TestMapJson -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonPathsIntegrationSpec extends Specification { - - @Autowired - PgJsonTestSearchService pgJsonTestSearchService - - @Unroll - void 'Test equals finding nested values (json). sqlOp: equals'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) - - when: - def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) - - then: - result.size() == size - result.every { it.data.nested.a == value } - - where: - value || size - 1 || 2 // there are 2 items with nested.a equal to 1 - 2 || 1 - 3 || 0 - } - - @Unroll - void 'Test equals finding nested values (json). sqlOp: greater than'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) - - when: - def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) - - then: - result.size() == size - result.every { it.data.nested.b > value.toInteger() } - - where: - value || size - 1 || 3 // There are 3 items with nested.b > 1 - 3 || 1 - 6 || 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy deleted file mode 100644 index 0162b67..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy +++ /dev/null @@ -1,79 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonTestSearchService -import app.json.TestMapJson -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonValuesIntegrationSpec extends Specification { - - @Autowired - PgJsonTestSearchService pgJsonTestSearchService - - @Unroll - void 'Test equals finding value: #value with condition is ilike (json)'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) - - then: - result.size() == size - result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } - - where: - value || size - '%iv%' || 2 - 'John' || 0 - } - - @Unroll - void 'Test equals finding value: #value with condition equals (json)'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) - - then: - result.size() == size - result.every { it.data.name == value } - - where: - value || size - 'Iván' || 2 - 'John' || 0 - } - - @Unroll - void 'Test equals finding value: #value with condition does not equal (json)'() { - setup: - new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) - - then: - result.size() == size - result.every { it.data.name != value } - - where: - value || size - 'Iván' || 1 - 'John' || 3 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy deleted file mode 100644 index 0fae20d..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy +++ /dev/null @@ -1,107 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonbTestSearchService -import app.json.TestMapJsonb -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonbContainsIntegrationSpec extends Specification { - - @Autowired - PgJsonbTestSearchService pgJsonbTestSearchService - - def setup() { - TestMapJsonb.executeUpdate('delete from TestMapJsonb') - } - - void 'Test only one value'() { - given: - def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: 1, d: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - def obj4 = new TestMapJsonb(data: [b: '1', a: 'foo', c: 'test',]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) - - then: - result.size() == 2 - result.contains(obj1) - result.contains(obj4) - - where: - map = [b: '1'] - } - - void 'Test multiple values'() { - given: - def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - def obj4 = new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) - - then: - result.size() == 2 - result.contains(obj1) - result.contains(obj4) - - where: - map = [a: 'foo', b: '1'] - } - - void 'No matches with the same combination key/value'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) - - then: - result.size() == 0 - - where: - map = [b: 3] - } - - void 'All matched with empty map'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) - - then: - result.size() == 4 - where: - map = [:] - } - - void 'None matched with null'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'foo', b: '1', c: 'test',]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbContains', 'data', map) - - then: - result.size() == 0 - - where: - map = null - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy deleted file mode 100644 index 0437c71..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonbTestSearchService -import app.json.TestMapJsonb -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonbEqualsIntegrationSpec extends Specification { - - @Autowired - PgJsonbTestSearchService pgJsonbTestSearchService - - @Unroll - void 'Test equals finding value: #value (jsonb)'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonHasFieldValue', 'data', 'name', value) - - then: - result.size() == size - result.every { it.data.name == value } - - where: - value || size - 'Iván' || 2 - 'John' || 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy deleted file mode 100644 index b8fd025..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy +++ /dev/null @@ -1,90 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonbTestSearchService -import app.json.TestMapJsonb -import spock.lang.Specification - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonbIsContainedIntegrationSpec extends Specification { - - @Autowired - PgJsonbTestSearchService pgJsonbTestSearchService - - def setup() { - TestMapJsonb.executeUpdate('delete from TestMapJsonb') - } - - void 'Test some values'() { - given: - def obj1 = new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: 1, d: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - def obj4 = new TestMapJsonb(data: [b: '1', a: 'foo', c: 'test',]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) - - then: - result.size() == 2 - result.contains(obj1) - result.contains(obj4) - - where: - map = [a: 'foo', b: '1', c: 'test'] - } - - void 'No matches with the same combination key/value'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) - - then: - result.size() == 0 - - where: - map = [a: 'foo'] - } - - void 'Only empty matched with empty map'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - def obj4 = new TestMapJsonb(data: [:]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) - - then: - result.size() == 1 - result.contains(obj4) - - where: - map = [:] - } - - void 'No data matches with null'() { - given: - new TestMapJsonb(data: [a: 'foo', b: '1']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [b: '2']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [a: 'test']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [:]).save(flush: true, failOnError: true) - - when: - def result = pgJsonbTestSearchService.search('pgJsonbIsContained', 'data', map) - - then: - result.size() == 0 - - where: - map = null - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy deleted file mode 100644 index 09223aa..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonbTestSearchService -import app.json.TestMapJsonb -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonbPathsIntegrationSpec extends Specification { - - @Autowired - PgJsonbTestSearchService pgJsonbTestSearchService - - @Unroll - void 'Test equals finding nested values (jsonb). sqlOp: equals'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) - - then: - result.size() == size - result.every { it.data.nested.a == value } - - where: - value || size - 1 || 2 // there are 2 items with nested.a equal to 1 - 2 || 1 - 3 || 0 - } - - @Unroll - void 'Test equals finding nested values (jsonb). sqlOp: greater than'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) - - then: - result.size() == size - result.every { it.data.nested.b > value.toInteger() } - - where: - value || size - 1 || 3 // There are 3 items with nested.b > 1 - 3 || 1 - 6 || 0 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy deleted file mode 100644 index 0623645..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy +++ /dev/null @@ -1,79 +0,0 @@ -package test.json - -import app.criteria.json.PgJsonbTestSearchService -import app.json.TestMapJsonb -import spock.lang.Specification -import spock.lang.Unroll - -import org.springframework.beans.factory.annotation.Autowired - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgJsonbValuesIntegrationSpec extends Specification { - - @Autowired - PgJsonbTestSearchService pgJsonbTestSearchService - - @Unroll - void 'Test equals finding value: #value with condition is ilike (jsonb)'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) - - then: - result.size() == size - result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } - - where: - value || size - '%iv%' || 2 - 'John' || 0 - } - - @Unroll - void 'Test equals finding value: #value with condition equals (jsonb)'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) - - then: - result.size() == size - result.every { it.data.name == value } - - where: - value || size - 'Iván' || 2 - 'John' || 0 - } - - @Unroll - void 'Test equals finding value: #value with condition does not equal (jsonb)'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) - new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) - - when: - def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) - - then: - result.size() == size - result.every { it.data.name != value } - - where: - value || size - 'Iván' || 1 - 'John' || 3 - } -} diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy deleted file mode 100644 index 4265b36..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package test.json - -import app.json.TestMapJson -import spock.lang.Specification -import spock.lang.Unroll - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PostgresqlJsonMapDomainIntegrationSpec extends Specification { - - def setup() { - TestMapJson.executeUpdate('delete from TestMapJson') - } - - @Unroll - void 'save and read a domain class with a map #map to json'() { - setup: - def testMapJson = new TestMapJson(data: map) - - when: - // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke - // nullSafeGet on the corresponding user type and will not use current session's cache. - TestMapJson.withNewTransaction { - testMapJson.save(flush: true, failOnError: true) - } - - then: - testMapJson.hasErrors() == false - - and: - def obj = testMapJson.get(testMapJson.id) - obj.data == map - - where: - map << [null, [:], [name: 'Ivan', age: 34]] - } - - void 'save and read a domain class with json'() { - setup: - def value = [name: 'Ivan', age: 34, hasChildren: true, children: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] - def testMapJson = new TestMapJson(data: value) - - when: - testMapJson.save(flush: true, failOnError: true) - - then: - testMapJson.hasErrors() == false - - and: - def obj = testMapJson.get(testMapJson.id) - obj.data.keySet().collect { it.toString() } == ['name', 'age', 'hasChildren', 'children'] - obj.data.children.size() == 2 - } -} \ No newline at end of file diff --git a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy deleted file mode 100644 index c072ee8..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package test.json - -import app.json.TestMapJsonb -import spock.lang.Specification -import spock.lang.Unroll - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PostgresqlJsonbMapDomainIntegrationSpec extends Specification { - - def setup() { - TestMapJsonb.executeUpdate('delete from TestMapJsonb') - } - - @Unroll - void 'save and read a domain class with a map #map to jsonb'() { - setup: - def testMapJsonb = new TestMapJsonb(data: map) - - when: - // Domain saving and retrieving should be in different sessions. Only in that case Hibernate will invoke - // nullSafeGet on the corresponding user type and will not use current session's cache. - TestMapJsonb.withNewTransaction { - testMapJsonb.save(flush: true, failOnError: true) - } - - then: - testMapJsonb.hasErrors() == false - - and: - def obj = testMapJsonb.get(testMapJsonb.id) - obj.data == map - - where: - map << [null, [:], [name: 'Ivan', age: 34]] - } - - void 'save and read a domain class with jsonb'() { - setup: - def value = [name: 'Ivan', age: 34, hasChilds: true, childs: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] - def testMapJsonb = new TestMapJsonb(data: value) - - when: - testMapJsonb.save(flush: true, failOnError: true) - - then: - testMapJsonb.hasErrors() == false - - and: - def obj = testMapJsonb.get(testMapJsonb.id) - obj.data.keySet().collect { it.toString() }.equals(['name', 'age', 'hasChilds', 'childs']) - obj.data.childs.size() == 2 - } -} \ No newline at end of file diff --git a/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy b/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy deleted file mode 100644 index d1d9798..0000000 --- a/test-apps/app2/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package test.order - -import app.json.TestMapJsonb -import spock.lang.Specification - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration - -@Rollback -@Integration -class PgOrderIntegrationSpec extends Specification { - - def pgOrderService - - def setup() { - TestMapJsonb.executeUpdate('delete from TestMapJsonb') - } - - void 'Order by a json property'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [name: 'Ernesto', lastName: 'Pérez']).save(flush: true, failOnError: true) - - when: - def result = pgOrderService.orderByJson() - - then: - result - result.data.name == ['Iván', 'Ernesto', 'Alonso'] - } - - void 'Order by random'() { - setup: - new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true, failOnError: true) - new TestMapJsonb(data: [name: 'Ernesto', lastName: 'Pérez']).save(flush: true, failOnError: true) - - when: - def result = pgOrderService.orderByRandom() - - then: - result - result.size() == 3 - } -} \ No newline at end of file diff --git a/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql b/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql deleted file mode 100644 index 5ed9f15..0000000 --- a/test-apps/app2/src/integration-test/resources/init-pg-extensions.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS hstore; From 9b34ee81393ee5967b42a628a16b841def36e499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 14:30:44 +0200 Subject: [PATCH 09/11] fix(hibernate7): address review of the criteria and orders - A criterion on an alias with an inner join makes GORM create the join of the alias first and uses it, so it applies to the same join as GORM criteria on the alias, in any order. For an alias with an outer join it uses a join with that join type, as GORM 8.0.0 always joins an alias with an inner join (apache/grails-core#16562). - An order by an SQL formula keeps its position among the property orders, and adds no predicate, so it does not change the result of an or {} or not {} it is called in. - Null values are bound as SQL NULL instead of the text 'null', like Hibernate 5. - Gson is an implementation dependency again: the criteria use it through JsonUtils, so the extension classes Groovy loads when compiling an application do not reference it. Add tests for criteria on an alias of a collection and of an outer join, for formula orders combined with property orders, pagination and or {}, and for null values. --- plugin-hibernate7/build.gradle | 2 +- .../hibernate/criterion/PgCriterion.groovy | 49 +++++++++-- .../hibernate/order/OrderBySqlFormula.groovy | 14 ++-- .../postgresql/criteria/ArrayCriterias.groovy | 23 ++--- .../criteria/HstoreCriterias.groovy | 11 +-- .../postgresql/criteria/JsonCriterias.groovy | 18 ++-- .../postgresql/criteria/OrderCriterias.groovy | 13 ++- .../hibernate/utils/CriteriaUtils.groovy | 47 +++++++++-- .../pgext/hibernate/utils/JsonUtils.groovy | 20 +++++ .../domain/app/criteria/array/Team.groovy | 21 +++++ ...rayCriteriaWithAliasIntegrationSpec.groovy | 83 +++++++++++++++++++ .../PgHstoreContainsKeyIntegrationSpec.groovy | 13 +++ .../json/PgJsonValuesIntegrationSpec.groovy | 14 ++++ .../test/order/PgOrderIntegrationSpec.groovy | 67 ++++++++++++++- .../app2/grails-app/conf/application.groovy | 3 +- 15 files changed, 351 insertions(+), 47 deletions(-) create mode 100644 plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/JsonUtils.groovy create mode 100644 test-apps/app1/grails-app/domain/app/criteria/array/Team.groovy create mode 100644 test-apps/app1/src/integration-test/groovy/test/array/PgArrayCriteriaWithAliasIntegrationSpec.groovy diff --git a/plugin-hibernate7/build.gradle b/plugin-hibernate7/build.gradle index 3d8fe98..fea0b33 100644 --- a/plugin-hibernate7/build.gradle +++ b/plugin-hibernate7/build.gradle @@ -29,7 +29,7 @@ dependencies { // Must be provided by the Grails application } - api 'com.google.code.gson:gson' + implementation 'com.google.code.gson:gson' } compileJava.options.release = javaVersion.toInteger() diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy index daa8baa..bc12bbc 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/criterion/PgCriterion.groovy @@ -6,9 +6,11 @@ import jakarta.persistence.criteria.AbstractQuery import jakarta.persistence.criteria.Expression import jakarta.persistence.criteria.From import jakarta.persistence.criteria.Join +import jakarta.persistence.criteria.JoinType import jakarta.persistence.criteria.Path import jakarta.persistence.criteria.Predicate import org.hibernate.query.criteria.HibernateCriteriaBuilder +import org.hibernate.query.sqm.tree.from.SqmFrom import org.grails.datastore.mapping.query.Query @@ -31,7 +33,7 @@ class PgCriterion implements Query.Criterion { * @param root The root (or association join) of the query * @param cb The criteria builder * @param property The path of the property of the criterion, or null if it has none - * @return The predicate + * @return The predicate, or null if the criterion does not restrict the query */ Predicate create(AbstractQuery query, From root, HibernateCriteriaBuilder cb, Path property) } @@ -39,15 +41,33 @@ class PgCriterion implements Query.Criterion { /** The property path, with any alias replaced by its association path */ final String propertyPath + /** Whether the criterion is an order by an SQL formula, see {@code OrderBySqlFormula} */ + final boolean order + + /** The join type of the alias the property is on, or null if it is not on an alias */ + JoinType joinType + private final PredicateFactory factory PgCriterion(String propertyPath, PredicateFactory factory) { + this(propertyPath, false, factory) + } + + private PgCriterion(String propertyPath, boolean order, PredicateFactory factory) { this.propertyPath = propertyPath + this.order = order this.factory = factory } + /** + * Creates a criterion that orders the query, and returns no predicate + */ + static PgCriterion order(PredicateFactory factory) { + new PgCriterion(null, true, factory) + } + Predicate toPredicate(AbstractQuery query, From root, HibernateCriteriaBuilder cb) { - factory.create(query, root, cb, propertyPath ? resolvePath(root, propertyPath) : null) + factory.create(query, root, cb, propertyPath ? resolvePath(root, propertyPath, joinType) : null) } /** @@ -60,19 +80,36 @@ class PgCriterion implements Query.Criterion { } /** - * Resolves a property path like {@code like.favoriteNumbers}, reusing the joins already on the query + * Binds the value as text, or as SQL NULL when it is null, like Hibernate 5 did */ - private static Path resolvePath(From root, String propertyPath) { + static Expression text(HibernateCriteriaBuilder cb, Object value) { + value == null ? cb.nullLiteral(String) : cb.value(value.toString()) + } + + /** + * Resolves a property path like {@code like.favoriteNumbers}, reusing the joins already on the query. + * GORM may join an association more than once (for {@code createAlias} and for the alias itself), so the + * latest join with the join type of the alias is used (see {@code CriteriaUtils.addToCriteria}) + */ + private static Path resolvePath(From root, String propertyPath, JoinType joinType) { String[] segments = propertyPath.split(/\./) From from = root for (int i = 0; i < segments.length - 1; i++) { String segment = segments[i] - Join join = from.joins.find { Join j -> j.attribute.name == segment } - from = join ?: from.join(segment) + Join join = joinsInCreationOrder(from).reverse().find { Join j -> + j.attribute?.name == segment && (joinType == null || j.joinType == joinType) + } + from = join ?: from.join(segment, joinType ?: JoinType.INNER) } from.get(segments[segments.length - 1]) } + private static List> joinsInCreationOrder(From from) { + // The joins of a JPA From are a Set, the joins of a Hibernate SqmFrom are kept in creation order + Collection joins = from instanceof SqmFrom ? ((SqmFrom) from).sqmJoins : from.joins + joins.findAll { it instanceof Join } as List> + } + @Override String toString() { "PgCriterion($propertyPath)" diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy index 7de1cfa..74b35c1 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy @@ -18,7 +18,9 @@ import gpc.pgext.hibernate.criterion.PgSqlFunction * without any verification. * * GORM for Hibernate 7 only orders by properties, so the order is added to the query as a {@link PgCriterion} - * that appends the formula to the orders of the query when the criteria is turned into SQL. + * that inserts the formula among the orders of the query when the criteria is turned into SQL. This relies on GORM + * adding the property orders to the query before its predicates, which the order tests of the test apps check. + * See https://github.com/apache/grails-core/issues/16563 */ @CompileStatic class OrderBySqlFormula { @@ -39,19 +41,21 @@ class OrderBySqlFormula { } /** + * @param position The position of this order among the orders of the criteria * @return The criterion that adds this order to the query */ - PgCriterion toCriterion() { + PgCriterion toCriterion(int position) { String formula = sqlFormula - new PgCriterion(null, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + PgCriterion.order({ AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> // Only queries for the entity are ordered, not count queries or other projections if (query instanceof CriteriaQuery && ((CriteriaQuery) query).resultType == root.javaType) { def criteriaQuery = (CriteriaQuery) query List orders = new ArrayList<>(criteriaQuery.orderList) - orders << cb.asc(cb.function(PgSqlFunction.EXPRESSION, Object, cb.literal(formula))) + orders.add(Math.min(position, orders.size()), cb.asc(cb.function(PgSqlFunction.EXPRESSION, Object, cb.literal(formula)))) criteriaQuery.orderBy(orders) } - cb.conjunction() + // No predicate, so the order does not change the result of an or {} or not {} it is called in + null } as PgCriterion.PredicateFactory) } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy index 9abdc2f..c9e1565 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy @@ -16,6 +16,7 @@ import gpc.pgext.hibernate.criterion.PgCriterion import gpc.pgext.hibernate.utils.PgArrayUtils import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.criterion.PgCriterion.text import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName @@ -39,7 +40,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '@>')) + addToCriteria(self, propertyName, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '@>')) } /** @@ -49,7 +50,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayIsContainedBy(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<@')) + addToCriteria(self, propertyName, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<@')) } /** @@ -59,7 +60,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayOverlaps(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '&&')) + addToCriteria(self, propertyName, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '&&')) } /** @@ -68,7 +69,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayIsEmpty(HibernateCriteriaBuilder self, String propertyName) { - addToCriteria(self, emptinessExpression(calculatePropertyName(self, propertyName), '=')) + addToCriteria(self, propertyName, emptinessExpression(calculatePropertyName(self, propertyName), '=')) } /** @@ -77,7 +78,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayIsNotEmpty(HibernateCriteriaBuilder self, String propertyName) { - addToCriteria(self, emptinessExpression(calculatePropertyName(self, propertyName), '<>')) + addToCriteria(self, propertyName, emptinessExpression(calculatePropertyName(self, propertyName), '<>')) } /** @@ -92,8 +93,8 @@ class ArrayCriterias { static Criteria pgArrayIsEmptyOrContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { def name = calculatePropertyName(self, propertyName) propertyValue ? - addToCriteria(self, arrayExpression(name, propertyValue, '@>')) : - addToCriteria(self, emptinessExpression(name, '=')) + addToCriteria(self, propertyName, arrayExpression(name, propertyValue, '@>')) : + addToCriteria(self, propertyName, emptinessExpression(name, '=')) } /** @@ -103,7 +104,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '=')) + addToCriteria(self, propertyName, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '=')) } /** @@ -113,7 +114,7 @@ class ArrayCriterias { * @return The criteria */ static Criteria pgArrayNotEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<>')) + addToCriteria(self, propertyName, arrayExpression(calculatePropertyName(self, propertyName), propertyValue, '<>')) } /** @@ -124,8 +125,8 @@ class ArrayCriterias { */ static Criteria pgArrayILike(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { String value = (String) propertyValue - addToCriteria(self, new PgCriterion(calculatePropertyName(self, propertyName), { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> - sql(cb, 'text(?) ilike ?', property, cb.value(value)) + addToCriteria(self, propertyName, new PgCriterion(calculatePropertyName(self, propertyName), { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> + sql(cb, 'text(?) ilike ?', property, text(cb, value)) } as PgCriterion.PredicateFactory)) } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy index 848adb2..b6ba3e2 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy @@ -14,6 +14,7 @@ import gpc.pgext.hibernate.criterion.PgCriterion import gpc.pgext.hibernate.usertype.HstoreHelper import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.criterion.PgCriterion.text import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName @@ -21,24 +22,24 @@ import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName class HstoreCriterias { static Criteria pgHstoreContainsKey(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, valueFunction(calculatePropertyName(self, propertyName), 'exist(?, ?)', propertyValue)) + addToCriteria(self, propertyName, valueFunction(calculatePropertyName(self, propertyName), 'exist(?, ?)', propertyValue)) } static Criteria pgHstoreContains(HibernateCriteriaBuilder self, String propertyName, Map values) { - addToCriteria(self, operatorExpression(calculatePropertyName(self, propertyName), values, '@>')) + addToCriteria(self, propertyName, operatorExpression(calculatePropertyName(self, propertyName), values, '@>')) } static Criteria pgHstoreIsContained(HibernateCriteriaBuilder self, String propertyName, Map values) { - addToCriteria(self, operatorExpression(calculatePropertyName(self, propertyName), values, '<@')) + addToCriteria(self, propertyName, operatorExpression(calculatePropertyName(self, propertyName), values, '<@')) } static Criteria pgHstoreILikeValue(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, valueFunction(calculatePropertyName(self, propertyName), 'text(avals(?)) ilike ?', propertyValue)) + addToCriteria(self, propertyName, valueFunction(calculatePropertyName(self, propertyName), 'text(avals(?)) ilike ?', propertyValue)) } private static PgCriterion valueFunction(String propertyName, String sqlFunction, Object value) { new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> - sql(cb, sqlFunction, property, cb.value(String.valueOf(value))) + sql(cb, sqlFunction, property, text(cb, value)) } as PgCriterion.PredicateFactory) } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy index 71e0a8c..5212a23 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy @@ -2,8 +2,6 @@ package gpc.pgext.hibernate.postgresql.criteria import groovy.transform.CompileStatic -import com.google.gson.Gson -import com.google.gson.GsonBuilder import jakarta.persistence.criteria.AbstractQuery import jakarta.persistence.criteria.From import jakarta.persistence.criteria.Path @@ -13,16 +11,16 @@ import grails.orm.HibernateCriteriaBuilder import org.grails.datastore.mapping.query.api.Criteria import gpc.pgext.hibernate.criterion.PgCriterion +import gpc.pgext.hibernate.utils.JsonUtils import static gpc.pgext.hibernate.criterion.PgCriterion.sql +import static gpc.pgext.hibernate.criterion.PgCriterion.text import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName @CompileStatic class JsonCriterias { - private static final Gson GSON = new GsonBuilder().serializeNulls().create() - /** * Creates a "json has field value" Criterion based on the specified property name and value * @param propertyName The property name (json field) @@ -31,7 +29,7 @@ class JsonCriterias { * @return The criteria */ static Criteria pgJsonHasFieldValue(HibernateCriteriaBuilder self, String propertyName, String jsonAttribute, Object propertyValue) { - addToCriteria(self, jsonExpression(calculatePropertyName(self, propertyName), '->>', jsonAttribute, '=', propertyValue)) + addToCriteria(self, propertyName, jsonExpression(calculatePropertyName(self, propertyName), '->>', jsonAttribute, '=', propertyValue)) } /** @@ -41,7 +39,7 @@ class JsonCriterias { * @return The criteria */ static Criteria pgJsonbContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '@>')) + addToCriteria(self, propertyName, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '@>')) } /** @@ -51,7 +49,7 @@ class JsonCriterias { * @return The criteria */ static Criteria pgJsonbIsContained(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { - addToCriteria(self, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '<@')) + addToCriteria(self, propertyName, jsonbOperator(calculatePropertyName(self, propertyName), propertyValue, '<@')) } /** @@ -64,18 +62,18 @@ class JsonCriterias { * @return The criteria */ static Criteria pgJson(HibernateCriteriaBuilder self, String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object propertyValue) { - addToCriteria(self, jsonExpression(calculatePropertyName(self, propertyName), jsonOp, jsonAttribute, sqlOp, propertyValue)) + addToCriteria(self, propertyName, jsonExpression(calculatePropertyName(self, propertyName), jsonOp, jsonAttribute, sqlOp, propertyValue)) } private static PgCriterion jsonExpression(String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object value) { String attribute = jsonAttribute.replace("'", "''") new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> - sql(cb, "?$jsonOp'$attribute' $sqlOp ?", property, cb.value(String.valueOf(value))) + sql(cb, "?$jsonOp'$attribute' $sqlOp ?", property, text(cb, value)) } as PgCriterion.PredicateFactory) } private static PgCriterion jsonbOperator(String propertyName, Object value, String op) { - String json = GSON.toJson(value) + String json = JsonUtils.toJson(value) new PgCriterion(propertyName, { AbstractQuery query, From root, JpaCriteriaBuilder cb, Path property -> sql(cb, "? $op CAST(? AS jsonb)", property, cb.value(json)) } as PgCriterion.PredicateFactory) diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy index c7e452e..400c14f 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/OrderCriterias.groovy @@ -3,8 +3,10 @@ package gpc.pgext.hibernate.postgresql.criteria import groovy.transform.CompileStatic import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.mapping.query.Query import org.grails.datastore.mapping.query.api.Criteria +import gpc.pgext.hibernate.criterion.PgCriterion import gpc.pgext.hibernate.order.OrderBySqlFormula import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria @@ -18,6 +20,15 @@ class OrderCriterias { * @return The criteria */ static Criteria order(HibernateCriteriaBuilder self, OrderBySqlFormula order) { - addToCriteria(self, order.toCriterion()) + def detachedCriteria = self.hibernateQuery.detachedCriteria + int position = detachedCriteria.orders.size() + countOrders(detachedCriteria.criteria) + addToCriteria(self, order.toCriterion(position)) + } + + private static int countOrders(List criteria) { + (int) criteria.sum(0) { Query.Criterion criterion -> + criterion instanceof PgCriterion && ((PgCriterion) criterion).order ? 1 : + criterion instanceof Query.Junction ? countOrders(((Query.Junction) criterion).criteria) : 0 + } } } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy index f9e6bb8..6882535 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy @@ -2,6 +2,8 @@ package gpc.pgext.hibernate.utils import groovy.transform.CompileStatic +import jakarta.persistence.criteria.JoinType + import grails.orm.HibernateCriteriaBuilder import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria import org.grails.datastore.mapping.query.Query @@ -20,6 +22,34 @@ class CriteriaUtils { self } + /** + * Adds the criterion on the property to the query of the builder. + * + * GORM resolves an alias created with {@code createAlias} to a join of its own, which a criterion handler + * cannot look up. When the property is on an alias with an inner join, an always true restriction on the + * alias is added before the criterion, so GORM creates that join before the criterion uses it, and the + * criteria of GORM and of this plugin on the alias apply to the same join. + * + * GORM 8.0.0 always creates the join of an alias as an inner join, so for an alias with an outer join the + * criterion uses the join of the association with the join type of the alias instead. + * + * See https://github.com/apache/grails-core/issues/16562 and https://github.com/apache/grails-core/issues/16563 + */ + static Criteria addToCriteria(HibernateCriteriaBuilder self, String propertyName, PgCriterion criterion) { + def association = findAssociation(self, propertyName) + if (!association) { + return addToCriteria(self, criterion) + } + criterion.joinType = self.hibernateQuery.detachedCriteria.joinTypes[association.associationPath] ?: JoinType.INNER + if (criterion.joinType != JoinType.INNER) { + return addToCriteria(self, criterion) + } + String identity = "${association.alias}.${association.association.associatedEntity.identity.name}" + def resolveAlias = new Query.Disjunction([new Query.IsNull(identity), new Query.IsNotNull(identity)] as List) + self.hibernateQuery.add(new Query.Conjunction([resolveAlias, criterion] as List)) + self + } + /** * Replaces an alias created with {@code createAlias} at the start of the property name by its association path, * so {@code l.favoriteNumbers} becomes {@code like.favoriteNumbers} @@ -31,21 +61,26 @@ class CriteriaUtils { } String alias = propertyName.substring(0, dot) String associationPath = self.hibernateQuery.aliases.find { it.alias() == alias }?.path() ?: - findAssociationPath(self.hibernateQuery.detachedCriteria.criteria, alias) + findAssociation(self, propertyName)?.associationPath associationPath ? associationPath + propertyName.substring(dot) : propertyName } - private static String findAssociationPath(List criteria, String alias) { + private static DetachedAssociationCriteria findAssociation(HibernateCriteriaBuilder self, String propertyName) { + int dot = propertyName.indexOf('.') + dot < 0 ? null : findAssociation(self.hibernateQuery.detachedCriteria.criteria, propertyName.substring(0, dot)) + } + + private static DetachedAssociationCriteria findAssociation(List criteria, String alias) { for (Query.Criterion criterion : criteria) { if (criterion instanceof DetachedAssociationCriteria) { def associationCriteria = (DetachedAssociationCriteria) criterion if (associationCriteria.alias == alias) { - return associationCriteria.associationPath + return associationCriteria } } else if (criterion instanceof Query.Junction) { - def associationPath = findAssociationPath(((Query.Junction) criterion).criteria, alias) - if (associationPath) { - return associationPath + def associationCriteria = findAssociation(((Query.Junction) criterion).criteria, alias) + if (associationCriteria) { + return associationCriteria } } } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/JsonUtils.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/JsonUtils.groovy new file mode 100644 index 0000000..ad18856 --- /dev/null +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/JsonUtils.groovy @@ -0,0 +1,20 @@ +package gpc.pgext.hibernate.utils + +import groovy.transform.CompileStatic + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +/** + * Converts values to json. Keeps Gson out of the signatures of the criteria extension classes, which Groovy + * loads when it compiles the application, so Gson does not have to be on its compile classpath. + */ +@CompileStatic +class JsonUtils { + + private static final Gson GSON = new GsonBuilder().serializeNulls().create() + + static String toJson(Object value) { + GSON.toJson(value) + } +} diff --git a/test-apps/app1/grails-app/domain/app/criteria/array/Team.groovy b/test-apps/app1/grails-app/domain/app/criteria/array/Team.groovy new file mode 100644 index 0000000..b5fbf5f --- /dev/null +++ b/test-apps/app1/grails-app/domain/app/criteria/array/Team.groovy @@ -0,0 +1,21 @@ +package app.criteria.array + +import groovy.transform.ToString + +import grails.compiler.GrailsCompileStatic + +/** + * Has a to-many association to {@link Like}, to test criteria on an alias of a collection + */ +@ToString +@GrailsCompileStatic +class Team { + + String name + + static hasMany = [likes: Like] + + static mapping = { + table('pg_extensions_team') + } +} diff --git a/test-apps/app1/src/integration-test/groovy/test/array/PgArrayCriteriaWithAliasIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgArrayCriteriaWithAliasIntegrationSpec.groovy new file mode 100644 index 0000000..605d8ce --- /dev/null +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgArrayCriteriaWithAliasIntegrationSpec.groovy @@ -0,0 +1,83 @@ +package test.array + +import app.criteria.array.Like +import app.criteria.array.Team +import app.criteria.array.User +import spock.lang.Specification +import spock.lang.Unroll + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Rollback +@Integration +class PgArrayCriteriaWithAliasIntegrationSpec extends Specification { + + private static final int LEFT_JOIN = 1 + + def setup() { + Team.executeUpdate('delete from Team') + User.executeUpdate('delete from User') + Like.executeUpdate('delete from Like') + } + + @Unroll + void 'a pg criterion and a GORM criterion on the same alias of a collection apply to the same element (pg criterion first: #pgFirst)'() { + setup: + def three = new Like(favoriteNumbers: [3]).save(flush: true, failOnError: true) + def five = new Like(favoriteNumbers: [5]).save(flush: true, failOnError: true) + new Team(name: 'team').addToLikes(three).addToLikes(five).save(flush: true, failOnError: true) + + when: 'no single like both contains 3 and is the like with 5' + def result = Team.withCriteria { + createAlias('likes', 'l') + if (pgFirst) { + pgArrayContains('l.favoriteNumbers', 3) + eq('l.id', five.id) + } else { + eq('l.id', five.id) + pgArrayContains('l.favoriteNumbers', 3) + } + } + + then: + result.empty + + when: 'the like with 3 both contains 3 and is the like with 3' + result = Team.withCriteria { + createAlias('likes', 'l') + if (pgFirst) { + pgArrayContains('l.favoriteNumbers', 3) + eq('l.id', three.id) + } else { + eq('l.id', three.id) + pgArrayContains('l.favoriteNumbers', 3) + } + } + + then: 'the team is found once' + result*.name == ['team'] + + where: + pgFirst << [true, false] + } + + void 'a pg criterion on an alias honours the join type of the alias'() { + setup: + new User(name: 'without like').save(flush: true, failOnError: true) + new User(name: 'with 3', like: new Like(favoriteNumbers: [3])).save(flush: true, failOnError: true) + new User(name: 'with 5', like: new Like(favoriteNumbers: [5])).save(flush: true, failOnError: true) + + when: + def result = User.withCriteria { + createAlias('like', 'l', LEFT_JOIN) + or { + isNull('like') + pgArrayContains('l.favoriteNumbers', 3) + } + } + + then: + result*.name.sort() == ['with 3', 'without like'] + } +} diff --git a/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy index 9e7043e..4c755ea 100644 --- a/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy @@ -51,4 +51,17 @@ class PgHstoreContainsKeyIntegrationSpec extends Specification { then: result.size() == 0 } + + void 'Test a null key matches no key, not even the text null'() { + setup: + new TestHstoreMap(testAttributes: ['null': 'a']).save(flush: true, failOnError: true) + + when: + def result = TestHstoreMap.withCriteria { + pgHstoreContainsKey('testAttributes', null) + } + + then: + result.empty + } } diff --git a/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy index 0162b67..19ce78d 100644 --- a/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy @@ -76,4 +76,18 @@ class PgJsonValuesIntegrationSpec extends Specification { 'Iván' || 1 'John' || 3 } + + void 'Test a null value matches no json value, not even the text null'() { + setup: + new TestMapJson(data: [name: 'null']).save(flush: true) + new TestMapJson(data: [name: 'Iván']).save(flush: true) + + when: + def result = TestMapJson.withCriteria { + pgJsonHasFieldValue('data', 'name', null) + } + + then: + result.empty + } } diff --git a/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy index d1d9798..bb6229f 100644 --- a/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy @@ -6,6 +6,8 @@ import spock.lang.Specification import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration +import static gpc.pgext.hibernate.order.OrderBySqlFormula.sqlFormula + @Rollback @Integration class PgOrderIntegrationSpec extends Specification { @@ -43,4 +45,67 @@ class PgOrderIntegrationSpec extends Specification { result result.size() == 3 } -} \ No newline at end of file + + void 'An order by a sql formula keeps its position among the property orders'() { + setup: + def first = save('first', 'A') + def second = save('second', 'B') + def third = save('third', 'A') + + when: 'ordered by group and then by id descending' + def result = TestMapJsonb.withCriteria { + order(sqlFormula("(data->>'group')")) + order('id', 'desc') + } + + then: + result*.id == [third.id, first.id, second.id] + + when: 'ordered by id descending and then by group' + result = TestMapJsonb.withCriteria { + order('id', 'desc') + order(sqlFormula("(data->>'group')")) + } + + then: + result*.id == [third.id, second.id, first.id] + } + + void 'An order by a sql formula is applied to a paginated list'() { + setup: + save('Iván', 'A') + save('Alonso', 'A') + save('Ernesto', 'A') + + when: + def result = TestMapJsonb.createCriteria().list(max: 2, offset: 1) { + order(sqlFormula("(data->>'name')")) + } + + then: + result*.data*.name == ['Ernesto', 'Iván'] + result.totalCount == 3 + } + + void 'An order by a sql formula in an or does not change the result of the or'() { + setup: + save('Iván', 'A') + save('Alonso', 'B') + save('Ernesto', 'A') + + when: + def result = TestMapJsonb.withCriteria { + or { + pgJsonHasFieldValue('data', 'group', 'A') + order(sqlFormula("(data->>'name')")) + } + } + + then: + result*.data*.name == ['Ernesto', 'Iván'] + } + + private static TestMapJsonb save(String name, String group) { + new TestMapJsonb(data: [name: name, group: group]).save(flush: true, failOnError: true) + } +} diff --git a/test-apps/app2/grails-app/conf/application.groovy b/test-apps/app2/grails-app/conf/application.groovy index f73cb96..0c69018 100644 --- a/test-apps/app2/grails-app/conf/application.groovy +++ b/test-apps/app2/grails-app/conf/application.groovy @@ -1,5 +1,6 @@ // GORM 8.0.0 for Hibernate 7 never creates the sequence of the default 'native' generator on PostgreSQL -// (GrailsNativeGenerator swallows the initialization of its SequenceStyleGenerator delegate), so use 'sequence' +// (GrailsNativeGenerator swallows the initialization of its SequenceStyleGenerator delegate), so use 'sequence'. +// See https://github.com/apache/grails-core/issues/16561 grails.gorm.default.mapping = { id generator: 'sequence' } From e09527094609f2b69080a6af2046e9dbe5c6c86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 14:30:54 +0200 Subject: [PATCH 10/11] chore: review cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Register the column types of the Hibernate 7 dialect with an own DdlType instead of Hibernate's internal DdlTypeImpl - Add Søren Berg Glasius to the developers of both plugin descriptors - Remove the Dockerfile, as the test apps run PostgreSQL with Testcontainers - Document upgrading to 9.0.0 and the Hibernate 7 limitations, and link the GORM issues --- README.md | 24 +++++++++- ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 1 + ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 1 + .../PostgresqlExtensionsDialect.groovy | 47 ++++++++++++++++++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f0cd63..8a29410 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ Both plugins provide the same user types, dialect, criteria and orders, in the s and queries of an application do not change when it moves from Hibernate 5 to Hibernate 7. > [!NOTE] -> With GORM 8.0.0 for Hibernate 7, the default `native` id generator does not create its sequences on PostgreSQL. +> With GORM 8.0.0 for Hibernate 7, the default `native` id generator does not create its sequences on PostgreSQL +> ([apache/grails-core#16561](https://github.com/apache/grails-core/issues/16561)). > Until this is fixed in GORM, use the `sequence` generator in `grails-app/conf/application.groovy`: > > ```groovy @@ -85,6 +86,25 @@ and queries of an application do not change when it moves from Hibernate 5 to Hi > } > ``` +### Upgrading to 9.0.0 + +- The Hibernate independent classes, like `HstoreHelper` and `PgArrayUtils`, moved to the + `grails-postgresql-extensions-core` module, which both plugins depend on. Their packages did not change. +- `PgArrayUtils.getValueAsArrayOfType` now throws an `IllegalArgumentException` instead of a `HibernateException` for + values of an unsupported type. The array criteria still throw a `HibernateException`. + +### Hibernate 7 limitations + +GORM 8.0.0 for Hibernate 7 has no API for custom criteria to use the joins of the query, or to order by an SQL formula +([apache/grails-core#16563](https://github.com/apache/grails-core/issues/16563)), so the plugin works around it: + +- A criterion on an alias created with `createAlias` uses the same join as the GORM criteria on the alias, when the + alias uses an inner join. GORM 8.0.0 creates the join of an alias as an inner join even when `createAlias` asks for an + outer join ([apache/grails-core#16562](https://github.com/apache/grails-core/issues/16562)), so for an outer join alias the criteria of the plugin use an + outer join, but GORM criteria on the same alias do not. +- `order(sqlFormula(...))` and `order(byRandom())` are added to the orders when the query is created, in the position + they were called in. Calling them inside `or {}` or `not {}` does not change the result of the junction. + ## Configuration After installing the plugin you have to use a new PostgreSQL Hibernate Dialect in your application. Add it to the @@ -681,6 +701,8 @@ if not you'll get a sql error during runtime. ## Release Notes +From 8.0.0 and forward release notes can be found on [GitHub](https://github.com/gpc/grails-postgresql-extensions/releases) + Version | Date | Comments ------- | ------------| --------- 7.0.0 | 29/Jul/2019 | Grails 4 (Hibernate 5.4): Add support for Grails 4 and Hibernate 5.4. Thanks to [James Hardwick](https://github.com/jamesdh) and [Zhuravskiy Vitaliy](https://github.com/zhuravskiy). diff --git a/plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy index b578b83..ea88d36 100644 --- a/plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy +++ b/plugin-hibernate5/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy @@ -46,6 +46,7 @@ class GrailsPostgresqlExtensionsGrailsPlugin extends Plugin { [name: 'albertop19'], [name: 'Andrey T'], [name: 'Mattias Reichel'], + [name: 'Søren Berg Glasius'], ] def issueManagement = [ system: 'GITHUB', diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy index 9d5e17e..a6dbb91 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy @@ -46,6 +46,7 @@ class GrailsPostgresqlExtensionsGrailsPlugin extends Plugin { [name: 'albertop19'], [name: 'Andrey T'], [name: 'Mattias Reichel'], + [name: 'Søren Berg Glasius'], ] def issueManagement = [ system: 'GITHUB', diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy index da4e304..f88f768 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy @@ -5,7 +5,9 @@ import groovy.transform.CompileStatic import org.hibernate.boot.model.TypeContributions import org.hibernate.dialect.PostgreSQLDialect import org.hibernate.service.ServiceRegistry -import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl +import org.hibernate.type.descriptor.java.JavaType +import org.hibernate.type.descriptor.jdbc.JdbcType +import org.hibernate.type.descriptor.sql.DdlType import gpc.pgext.hibernate.usertype.ArrayType import gpc.pgext.hibernate.usertype.HstoreMapType @@ -36,7 +38,48 @@ class PostgresqlExtensionsDialect extends PostgreSQLDialect { super.contributeTypes(typeContributions, serviceRegistry) def ddlTypeRegistry = typeContributions.typeConfiguration.ddlTypeRegistry COLUMN_TYPES.each { Integer sqlType, String typeName -> - ddlTypeRegistry.addDescriptor(new DdlTypeImpl(sqlType, typeName, this)) + ddlTypeRegistry.addDescriptor(new FixedDdlType(sqlType, typeName)) + } + } + + /** + * The column type of a user type, which is always the same type name + */ + private static class FixedDdlType implements DdlType { + + private static final long serialVersionUID = 1L + + private final int sqlTypeCode + private final String typeName + + FixedDdlType(int sqlTypeCode, String typeName) { + this.sqlTypeCode = sqlTypeCode + this.typeName = typeName + } + + @Override + int getSqlTypeCode() { + sqlTypeCode + } + + @Override + String getRawTypeName() { + typeName + } + + @Override + String getTypeName(Long size, Integer precision, Integer scale) { + typeName + } + + @Override + String getCastTypeName(JdbcType jdbcType, JavaType javaType) { + typeName + } + + @Override + String getCastTypeName(JdbcType jdbcType, JavaType javaType, Long length, Integer precision, Integer scale) { + typeName } } } From edcab8712f606518fe0b4ef3d3b85d763fc9e994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Fri, 9 Oct 2026 15:51:51 +0200 Subject: [PATCH 11/11] fix(hibernate7): address review nits - Resolve an inner join alias with a restriction on the property of the criterion instead of on the identity of the associated entity, which is null for a composite id - Document that an or {} with only an order by an SQL formula matches no rows --- README.md | 1 + .../gpc/pgext/hibernate/order/OrderBySqlFormula.groovy | 3 ++- .../groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a29410..48cb982 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ GORM 8.0.0 for Hibernate 7 has no API for custom criteria to use the joins of th outer join, but GORM criteria on the same alias do not. - `order(sqlFormula(...))` and `order(byRandom())` are added to the orders when the query is created, in the position they were called in. Calling them inside `or {}` or `not {}` does not change the result of the junction. + An `or {}` that only contains such an order is empty, which matches no rows. ## Configuration diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy index 74b35c1..d9a81b6 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy @@ -54,7 +54,8 @@ class OrderBySqlFormula { orders.add(Math.min(position, orders.size()), cb.asc(cb.function(PgSqlFunction.EXPRESSION, Object, cb.literal(formula)))) criteriaQuery.orderBy(orders) } - // No predicate, so the order does not change the result of an or {} or not {} it is called in + // No predicate, so the order does not change the result of an or {} or not {} it is called in. + // An or {} with only an order is empty though, which matches no rows null } as PgCriterion.PredicateFactory) } diff --git a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy index 6882535..c78dba0 100644 --- a/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy +++ b/plugin-hibernate7/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy @@ -27,8 +27,9 @@ class CriteriaUtils { * * GORM resolves an alias created with {@code createAlias} to a join of its own, which a criterion handler * cannot look up. When the property is on an alias with an inner join, an always true restriction on the - * alias is added before the criterion, so GORM creates that join before the criterion uses it, and the - * criteria of GORM and of this plugin on the alias apply to the same join. + * property ({@code l.favoriteNumbers is null or l.favoriteNumbers is not null}) is added before the criterion, + * so GORM creates that join before the criterion uses it, and the criteria of GORM and of this plugin on the + * alias apply to the same join. * * GORM 8.0.0 always creates the join of an alias as an inner join, so for an alias with an outer join the * criterion uses the join of the association with the join type of the alias instead. @@ -44,8 +45,7 @@ class CriteriaUtils { if (criterion.joinType != JoinType.INNER) { return addToCriteria(self, criterion) } - String identity = "${association.alias}.${association.association.associatedEntity.identity.name}" - def resolveAlias = new Query.Disjunction([new Query.IsNull(identity), new Query.IsNotNull(identity)] as List) + def resolveAlias = new Query.Disjunction([new Query.IsNull(propertyName), new Query.IsNotNull(propertyName)] as List) self.hibernateQuery.add(new Query.Conjunction([resolveAlias, criterion] as List)) self }