From 888a0b508726bb901882340a4fe0d76a2d0b41bf Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 10:55:19 +0200 Subject: [PATCH 1/7] feat!: Grails 7 - Updated for Grails 7 - Changed from ExpandoMetaClass to Extension Modules - New coords: `org.grails.plugins:grails-postgresql-extensions` - Re-packaged to `gpc.pgext.*` - Re-organized project --- .github/release-drafter.yml | 38 +++ .github/renovate.json | 4 + .github/workflows/gradle.yml | 70 ++++ .github/workflows/release-notes.yml | 24 ++ .github/workflows/release.yml | 135 ++++++++ .gitignore | 3 + .idea/codeStyles/Project.xml | 95 ++++++ .idea/codeStyles/codeStyleConfig.xml | 25 ++ .sdkmanrc | 2 + .travis.yml | 39 --- Makefile | 15 - README.md | 130 ++------ build.gradle | 130 +------- buildSrc/build.gradle | 24 ++ gradle.properties | 18 +- gradle/test-config.gradle | 39 +++ gradle/testVerbose.gradle | 29 -- gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 305 +++++++++++------- gradlew.bat | 80 +++-- grails-app/conf/application.yml | 28 -- grails-app/conf/logback.groovy | 30 -- .../domain/test/array/TestDouble.groovy | 19 -- grails-app/domain/test/array/TestFloat.groovy | 20 -- .../domain/test/array/TestInteger.groovy | 19 -- grails-app/domain/test/array/TestLong.groovy | 19 -- .../domain/test/array/TestString.groovy | 19 -- grails-app/domain/test/array/TestUuid.groovy | 19 -- .../domain/test/criteria/array/User.groovy | 16 - .../domain/test/hstore/TestHstoreMap.groovy | 25 -- .../domain/test/json/TestMapJson.groovy | 18 -- .../domain/test/json/TestMapJsonb.groovy | 19 -- .../array/PgArrayTestSearchService.groovy | 58 ---- .../hstore/PgHstoreTestSearchService.groovy | 13 - .../json/PgJsonTestSearchService.groovy | 19 -- .../json/PgJsonbTestSearchService.groovy | 25 -- .../services/test/order/PgOrderService.groovy | 21 -- plugin/build.gradle | 76 +++++ ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 57 ++++ .../PostgresqlExtensionsDialect.groovy | 14 +- .../criterion/array/PgArrayExpression.groovy | 52 ++- .../array/PgArrayILikeFunction.groovy | 14 +- .../array/PgEmptinessExpression.groovy | 13 +- .../hstore/PgHstoreILikeValueFunction.groovy | 22 ++ .../hstore/PgHstoreOperatorExpression.groovy | 22 +- .../hstore/PgHstoreValueFunction.groovy | 15 +- .../criterion/json/PgJsonExpression.groovy | 13 +- .../criterion/json/PgJsonbOperator.groovy | 13 +- .../hibernate/order/OrderByRandom.groovy | 3 +- .../hibernate/order/OrderBySqlFormula.groovy | 7 +- .../postgresql/criteria/ArrayCriterias.groovy | 150 +++++++++ .../criteria/HstoreCriterias.groovy | 33 ++ .../postgresql/criteria/JsonCriterias.groovy | 94 ++++++ .../pgext}/hibernate/usertype/ArrayType.java | 24 +- .../hibernate/usertype/BidiEnumMap.groovy | 61 ++++ .../hibernate/usertype/HstoreHelper.groovy | 39 +++ .../hibernate/usertype/HstoreMapType.groovy | 37 ++- .../usertype/HstoreParseException.groovy | 4 +- .../hibernate/usertype/HstoreParser.java | 18 +- .../hibernate/usertype/JsonMapType.groovy | 25 +- .../hibernate/usertype/JsonbMapType.groovy | 2 +- .../hibernate/utils/CriteriaUtils.groovy | 77 +++++ .../hibernate/utils/PgArrayUtils.groovy | 58 ++-- ...rg.codehaus.groovy.runtime.ExtensionModule | 6 + .../usertype/HstoreHelperSpec.groovy | 32 +- .../usertype/HstoreParserSpec.groovy | 15 +- settings.gradle | 41 ++- ...eCriteriaTestServiceIntegrationSpec.groovy | 45 --- .../PgHstoreContainsIntegrationSpec.groovy | 112 ------- .../PgHstoreContainsKeyIntegrationSpec.groovy | 51 --- ...reILikeValueFunctionIntegrationSpec.groovy | 51 --- .../PgHstoreIsContainedIntegrationSpec.groovy | 78 ----- ...ilsPostgresqlExtensionsGrailsPlugin.groovy | 39 --- .../hstore/PgHstoreILikeValueFunction.groovy | 24 -- .../postgresql/criteria/ArrayCriterias.groovy | 204 ------------ .../criteria/HstoreCriterias.groovy | 63 ---- .../postgresql/criteria/JsonCriterias.groovy | 99 ------ .../hibernate/usertype/BidiEnumMap.groovy | 61 ---- .../hibernate/usertype/HstoreHelper.groovy | 73 ----- test-apps/app1/build.gradle | 21 ++ .../app1/grails-app/conf/application.yml | 15 + .../app1/grails-app/conf/logback-spring.xml | 25 ++ .../grails-app}/conf/spring/resources.groovy | 0 .../domain/app/array/TestDouble.groovy | 22 ++ .../domain/app}/array/TestEnum.groovy | 13 +- .../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 | 25 +- .../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 | 7 +- .../array/PgArrayTestSearchService.groovy | 52 +++ .../hstore/PgHstoreTestSearchService.groovy | 12 + .../json/PgJsonTestSearchService.groovy | 18 ++ .../json/PgJsonbTestSearchService.groovy | 24 ++ .../services/app/order/PgOrderService.groovy | 21 ++ ...sCriteriaTestServiceIntegrationSpec.groovy | 129 ++++---- ...sCriteriaTestServiceIntegrationSpec.groovy | 109 ++++--- ...eCriteriaTestServiceIntegrationSpec.groovy | 47 +++ ...yCriteriaTestServiceIntegrationSpec.groovy | 110 +++---- ...yCriteriaTestServiceIntegrationSpec.groovy | 29 +- ...sCriteriaTestServiceIntegrationSpec.groovy | 68 ++-- ...yCriteriaTestServiceIntegrationSpec.groovy | 27 +- ...sCriteriaTestServiceIntegrationSpec.groovy | 107 +++--- ...sCriteriaTestServiceIntegrationSpec.groovy | 93 +++--- ...stgresqlArraysDomainIntegrationSpec.groovy | 25 +- .../groovy/test}/array/UuidBuilder.groovy | 6 +- .../PgHstoreContainsIntegrationSpec.groovy | 115 +++++++ .../PgHstoreContainsKeyIntegrationSpec.groovy | 54 ++++ ...reILikeValueFunctionIntegrationSpec.groovy | 54 ++++ .../PgHstoreIsContainedIntegrationSpec.groovy | 81 +++++ ...resqlHstoreMapDomainIntegrationSpec.groovy | 30 +- .../json/PgJsonEqualsIntegrationSpec.groovy | 19 +- .../json/PgJsonPathsIntegrationSpec.groovy | 20 +- .../json/PgJsonValuesIntegrationSpec.groovy | 20 +- .../PgJsonbContainsIntegrationSpec.groovy | 18 +- .../json/PgJsonbEqualsIntegrationSpec.groovy | 19 +- .../PgJsonbIsContainedIntegrationSpec.groovy | 18 +- .../json/PgJsonbPathsIntegrationSpec.groovy | 20 +- .../json/PgJsonbValuesIntegrationSpec.groovy | 20 +- ...tgresqlJsonMapDomainIntegrationSpec.groovy | 17 +- ...gresqlJsonbMapDomainIntegrationSpec.groovy | 11 +- .../test}/order/PgOrderIntegrationSpec.groovy | 13 +- travis-build.sh | 15 - 130 files changed, 2857 insertions(+), 2423 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/renovate.json create mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/release-notes.yml create mode 100644 .github/workflows/release.yml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .sdkmanrc delete mode 100644 .travis.yml delete mode 100644 Makefile create mode 100644 buildSrc/build.gradle create mode 100644 gradle/test-config.gradle delete mode 100644 gradle/testVerbose.gradle delete mode 100644 grails-app/conf/application.yml delete mode 100644 grails-app/conf/logback.groovy delete mode 100644 grails-app/domain/test/array/TestDouble.groovy delete mode 100644 grails-app/domain/test/array/TestFloat.groovy delete mode 100644 grails-app/domain/test/array/TestInteger.groovy delete mode 100644 grails-app/domain/test/array/TestLong.groovy delete mode 100644 grails-app/domain/test/array/TestString.groovy delete mode 100644 grails-app/domain/test/array/TestUuid.groovy delete mode 100644 grails-app/domain/test/criteria/array/User.groovy delete mode 100644 grails-app/domain/test/hstore/TestHstoreMap.groovy delete mode 100644 grails-app/domain/test/json/TestMapJson.groovy delete mode 100644 grails-app/domain/test/json/TestMapJsonb.groovy delete mode 100644 grails-app/services/test/criteria/array/PgArrayTestSearchService.groovy delete mode 100644 grails-app/services/test/criteria/hstore/PgHstoreTestSearchService.groovy delete mode 100644 grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy delete mode 100644 grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy delete mode 100644 grails-app/services/test/order/PgOrderService.groovy create mode 100644 plugin/build.gradle create mode 100644 plugin/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/PostgresqlExtensionsDialect.groovy (81%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/array/PgArrayExpression.groovy (50%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/array/PgArrayILikeFunction.groovy (66%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/array/PgEmptinessExpression.groovy (71%) create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy (62%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/hstore/PgHstoreValueFunction.groovy (66%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/json/PgJsonExpression.groovy (73%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/criterion/json/PgJsonbOperator.groovy (72%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/order/OrderByRandom.groovy (94%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/order/OrderBySqlFormula.groovy (88%) create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/ArrayType.java (93%) create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/HstoreMapType.groovy (77%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/HstoreParseException.groovy (73%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/HstoreParser.java (97%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/JsonMapType.groovy (93%) rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/usertype/JsonbMapType.groovy (83%) create mode 100644 plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy rename {src/main/groovy/net/kaleidos => plugin/src/main/groovy/gpc/pgext}/hibernate/utils/PgArrayUtils.groovy (50%) create mode 100644 plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule rename {src/test/groovy/net/kaleidos => plugin/src/test/groovy/gpc/pgext}/hibernate/usertype/HstoreHelperSpec.groovy (69%) rename {src/test/groovy/net/kaleidos => plugin/src/test/groovy/gpc/pgext}/hibernate/usertype/HstoreParserSpec.groovy (86%) delete mode 100644 src/integration-test/groovy/net/kaleidos/hibernate/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy delete mode 100644 src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsIntegrationSpec.groovy delete mode 100644 src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsKeyIntegrationSpec.groovy delete mode 100644 src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy delete mode 100644 src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreIsContainedIntegrationSpec.groovy delete mode 100644 src/main/groovy/grails/postgresql/extensions/GrailsPostgresqlExtensionsGrailsPlugin.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/ArrayCriterias.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/HstoreCriterias.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/usertype/BidiEnumMap.groovy delete mode 100644 src/main/groovy/net/kaleidos/hibernate/usertype/HstoreHelper.groovy create mode 100644 test-apps/app1/build.gradle create mode 100644 test-apps/app1/grails-app/conf/application.yml create mode 100644 test-apps/app1/grails-app/conf/logback-spring.xml rename {grails-app => test-apps/app1/grails-app}/conf/spring/resources.groovy (100%) create mode 100644 test-apps/app1/grails-app/domain/app/array/TestDouble.groovy rename {grails-app/domain/test => test-apps/app1/grails-app/domain/app}/array/TestEnum.groovy (64%) create mode 100644 test-apps/app1/grails-app/domain/app/array/TestFloat.groovy create mode 100644 test-apps/app1/grails-app/domain/app/array/TestInteger.groovy create mode 100644 test-apps/app1/grails-app/domain/app/array/TestLong.groovy create mode 100644 test-apps/app1/grails-app/domain/app/array/TestString.groovy create mode 100644 test-apps/app1/grails-app/domain/app/array/TestUuid.groovy rename {grails-app/domain/test => test-apps/app1/grails-app/domain/app}/criteria/array/Like.groovy (50%) create mode 100644 test-apps/app1/grails-app/domain/app/criteria/array/User.groovy create mode 100644 test-apps/app1/grails-app/domain/app/hstore/TestHstoreMap.groovy create mode 100644 test-apps/app1/grails-app/domain/app/json/TestMapJson.groovy create mode 100644 test-apps/app1/grails-app/domain/app/json/TestMapJsonb.groovy rename {grails-app/init/grails/postgresql/extensions => test-apps/app1/grails-app/init/app}/Application.groovy (74%) create mode 100644 test-apps/app1/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy create mode 100644 test-apps/app1/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy create mode 100644 test-apps/app1/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy create mode 100644 test-apps/app1/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy create mode 100644 test-apps/app1/grails-app/services/app/order/PgOrderService.groovy rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy (71%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy (72%) create mode 100644 test-apps/app1/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy (80%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy (88%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy (77%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy (88%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy (73%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy (76%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/PostgresqlArraysDomainIntegrationSpec.groovy (92%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/array/UuidBuilder.groovy (75%) create mode 100644 test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy create mode 100644 test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy create mode 100644 test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy create mode 100644 test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy (87%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonEqualsIntegrationSpec.groovy (86%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonPathsIntegrationSpec.groovy (91%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonValuesIntegrationSpec.groovy (93%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonbContainsIntegrationSpec.groovy (94%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonbEqualsIntegrationSpec.groovy (84%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonbIsContainedIntegrationSpec.groovy (93%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonbPathsIntegrationSpec.groovy (91%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PgJsonbValuesIntegrationSpec.groovy (93%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PostgresqlJsonMapDomainIntegrationSpec.groovy (80%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy (96%) rename {src/integration-test/groovy/net/kaleidos/hibernate => test-apps/app1/src/integration-test/groovy/test}/order/PgOrderIntegrationSpec.groovy (91%) delete mode 100755 travis-build.sh diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..a5e97e9 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,38 @@ +name-template: $RESOLVED_VERSION +tag-template: v$RESOLVED_VERSION +categories: + - title: 🚀 Features + labels: + - "type: enhancement" + - "type: new feature" + - "type: major" + - title: 🚀 Bug Fixes/Improvements + labels: + - "type: improvement" + - "type: bug" + - "type: minor" + - title: 🛠 Dependency upgrades + labels: + - "type: dependency upgrade" + - "dependencies" + - title: ⚙️ Build/CI + labels: + - "type: ci" + - "type: build" +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +version-resolver: + major: + labels: + - 'type: major' + minor: + labels: + - 'type: minor' + patch: + labels: + - 'type: patch' + default: patch +template: | + ## What's Changed + $CHANGES + ## Contributors + $CONTRIBUTORS \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..9995802 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"] +} \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..7932c22 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,70 @@ +name: "Java CI" +on: + push: + branches: + - master + - '[0-9]+.x' + pull_request: + branches: + - master + - '[0-9]+.x' + workflow_dispatch: +permissions: + packages: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +jobs: + tests: + if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} + name: "Test" + runs-on: ubuntu-24.04 + container: ubuntu + services: + postgres: + image: postgres + ports: + - 5432:5432 + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v4 + - name: "☕️ Setup JDK" + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: liberica + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + 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 + if: ${{ always() && github.repository_owner == 'gpc' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (needs.tests.result == 'success' || needs.tests.result == 'skipped') }} + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v4 + - name: "☕️ Setup JDK" + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: liberica + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "📤 Publish Snapshot Artifacts" + env: + GRAILS_PUBLISH_RELEASE: 'false' + MAVEN_PUBLISH_URL: ${{ secrets.MAVEN_PUBLISH_SNAPSHOT_URL }} + MAVEN_PUBLISH_USERNAME: ${{ secrets.MAVEN_PUBLISH_USERNAME }} + MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_PUBLISH_PASSWORD }} + run: ./gradlew --no-build-cache publish \ No newline at end of file diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000..2ae8559 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,24 @@ +name: "Release Drafter" +on: + issues: + types: [closed, reopened] + push: + branches: + - 'master' + - '[0-9]+.x' + pull_request: + types: [opened, reopened, synchronize] + pull_request_target: + types: [opened, reopened, synchronize] + workflow_dispatch: +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-24.04 + steps: + - name: "📝 Update Release Draft" + uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9cc7c12 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release +on: + release: + types: [ published ] +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JAVA_VERSION: '17.0.15' # this must be a specific version for reproducible builds + RELEASE_TAG_PREFIX: 'v' +jobs: + publish: + permissions: + packages: read # pre-release workflow + contents: write # to create release + issues: write # to modify milestones + runs-on: ubuntu-24.04 + outputs: + release_version: ${{ steps.release_version.outputs.value }} + extract_repository_name: ${{ steps.extract_repository_name.outputs.repository_name }} + steps: + - name: "📝 Store the current release version" + id: release_version + run: | + export RELEASE_VERSION="${{ github.ref_name }}" + export RELEASE_VERSION=${RELEASE_VERSION:${#RELEASE_TAG_PREFIX}} + echo "Found Release Version: ${RELEASE_VERSION}" + echo "value=${RELEASE_VERSION}" >> $GITHUB_OUTPUT + - name: "Extract repository name" + id: extract_repository_name + run: | + echo "repository_name=${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT + - name: "📥 Checkout the repository" + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: v${{ steps.release_version.outputs.value }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Ensure source files use common date" + run: | + find . -depth \( -type f -o -type d \) -exec touch -d "@${SOURCE_DATE_EPOCH}" {} + + - name: "☕️ Setup JDK" + uses: actions/setup-java@v4 + with: + distribution: liberica + java-version: ${{ env.JAVA_VERSION }} + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "⚙️ Run pre-release" + uses: apache/grails-github-actions/pre-release@asf + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.value }} + - name: "🔐 Generate key file for artifact signing" + env: + SECRING_FILE: ${{ secrets.SECRING_FILE }} + run: | + printf "%s" "$SECRING_FILE" | base64 -d > "${{ github.workspace }}/secring.gpg" + - name: "🧩 Run Assemble" + id: assemble + run: | + ./gradlew -U assemble -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg -Psigning.keyId=${{ secrets.SIGNING_KEY }} + env: + GRAILS_PUBLISH_RELEASE: 'true' + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + - name: "📤 Publish to Maven Central" + env: + GRAILS_PUBLISH_RELEASE: 'true' + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: ${{ secrets.NEXUS_PUBLISH_RELEASE_URL }} + NEXUS_PUBLISH_DESCRIPTION: '${{ steps.extract_repository_name.outputs.repository_name }}:${{ steps.release_version.outputs.value }}' + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + run: > + ./gradlew + -Psigning.keyId=${{ secrets.SIGNING_KEY }} + -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg + publishMavenPublicationToSonatypeRepository + closeSonatypeStagingRepository + - name: "Generate Build Date file" + run: echo "$SOURCE_DATE_EPOCH" >> build/BUILD_DATE.txt + - name: "Upload Build Date file" + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 + with: + tag_name: ${{ github.event.release.tag_name }} + files: build/BUILD_DATE.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: + needs: publish + runs-on: ubuntu-24.04 + environment: release + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: v${{ needs.publish.outputs.release_version }} + - name: "☕️ Setup JDK" + uses: actions/setup-java@v4 + with: + distribution: liberica + java-version: ${{ env.JAVA_VERSION }} + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + - name: "📤 Release staging repository" + env: + GRAILS_PUBLISH_RELEASE: 'true' + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: ${{ secrets.NEXUS_PUBLISH_RELEASE_URL }} + NEXUS_PUBLISH_DESCRIPTION: '${{ needs.publish.outputs.extract_repository_name }}:${{ needs.publish.outputs.release_version }}' + run: > + ./gradlew + findSonatypeStagingRepository + releaseSonatypeStagingRepository + - name: "📖 Generate Documentation" + if: ${{ hashFiles('src/main/asciidoc/**') != '' }} + run: ./gradlew docs + - name: "📤 Publish Documentation to Github Pages" + uses: apache/grails-github-actions/deploy-github-pages@asf + if: ${{ hashFiles('src/main/asciidoc/**') != '' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRADLE_PUBLISH_RELEASE: 'true' + SOURCE_FOLDER: build/docs + VERSION: ${{ needs.publish.outputs.release_version }} + - name: "⚙️ Run post-release" + uses: apache/grails-github-actions/post-release@asf diff --git a/.gitignore b/.gitignore index db7d75a..bae7029 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ plugin.xml grails-postgresql-extensions-*.zip grails-postgresql-extensions-*.zip.sha1 /.idea +!/.idea/codeStyles +!.idea/codeStyles/Project.xml +!.idea/codeStyles/codeStyleConfig.xml *.iml *.swp *.ids diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3f19036 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..52768fe --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..48cc71b --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,2 @@ +# Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env +java=17.0.15-librca diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4ceeff3..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -dist: precise -sudo: required -language: groovy -jdk: - - oraclejdk8 - -env: - global: - - GIT_NAME="Iván López" - - GIT_EMAIL="lopez.ivan@gmail.com" - - secure: qdvGtJFbtvTJPZ9HbBC4DtPGLAeWQq5A1gX7u4xxfPBNw/Mkd7Z0/OrWiMuNMccDvqhiPAbauZRb3ZSE/KOWhqSKg6EAMeQpBgGUOrjXNBH6x0m2BthFkQhEVWrDVqGx+AuEbm7kRsNw6ec+kJ6oaBPjnHOfbPnGVro+4sGWSck= - - secure: ZYo6fjlQNV1Oyc+fcINeISvR3pUAQG/V9I28v0xs2hjDiAug4gPnaQgxhmpa1SfRo5vwk+Oi+eNG22K2ePmUqARz2K5REahEPvocRZwrEow7pvJGvsLwiDcJk/o20fLPcUJlxgAyJeOQYR+exg35TlfHQ0ZWPVj07HO/A+1KTeM= - -addons: - postgresql: "9.4" - -sudo: false - -before_script: - - psql -d template1 -c 'create extension hstore;' -U postgres - - psql -c 'create database pg_extensions_test;' -U postgres - - psql -c "create user postgres_extensions with password 'postgres_extensions';" -U postgres - - psql -c "grant all privileges on database pg_extensions_test to postgres_extensions;" -U postgres - - rm -rf target - -script: - - ./travis-build.sh - -notifications: - email: - recipients: - - lopez.ivan@gmail.com - - alonso.javier.torres@gmail.com - - on_success: change - on_failure: change - -#after_success: -# - ./gradlew cobertura coveralls diff --git a/Makefile b/Makefile deleted file mode 100644 index eee89a1..0000000 --- a/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -.PHONY: test - -image : - docker build -f Dockerfile -t grails/postgres-extensions . - -clean : - docker rmi -f grails/postgres-extensions - -test : - ./gradlew clean - docker run --rm --name pg_extensions_test -p 5432:5432 -d grails/postgres-extensions - ./gradlew check - docker container kill pg_extensions_test - -all : image test \ No newline at end of file diff --git a/README.md b/README.md index dd931b4..4d977d2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ -# ⚠️ This plugin is not maintained anymore ⚠️ +# Grails PostgreSQL Extensions -## Grails Postgresql Extensions +This is a Grails plugin that provides Hibernate user types to use PostgreSQL native types such as Array, Hstore, Json, +Jsonb, ..., from a Grails application. It also provides new criteria to query these new native types. -#### THIS BRANCH (master) IS FOR GRAILS 4 AND HIBERNATE 5.4 #### - -This is a grails plugin that provides hibernate user types to use Postgresql native types such as Array, Hstore, Json, -Jsonb... from a Grails application. It also provides new criterias to query this new native types. - -Currently the plugin supports array, hstore, json and jsonb fields as well as some query methods. +Currently, the plugin supports array, hstore, json and jsonb fields as well as some query methods. More native types and query methods will be added in the future. * [Installation](#installation) - * [Postgresql driver](#postgresql-driver) + * [PostgreSQL driver](#postgresql-driver) * [Hibernate plugin](#hibernate-plugin) * [Configuration](#configuration) * [Native Types](#native-types) @@ -46,90 +42,26 @@ More native types and query methods will be added in the future. * [Order](#order) * [Random order](#random-order) * [Sql formula](#sql-formula) -* [Authors](#authors) * [Release Notes](#release-notes) ## Installation -The Grails 3 version supports both Hibernate 4.X (versions 4.x.x of the plugin) and Hibernate 5.X (versions 5.x.x of the -plugin). In `build.gradle` add the `jcenter` repository and the following dependency to install the plugin: - +The Grails 7 version of this plugin supports Hibernate 5. +In `build.gradle` add the following dependencies to install the plugin: ```groovy -repositories { - ... - jcenter() - ... -} - dependencies { - ... - compile 'org.grails.plugins:postgresql-extensions:' - ... -} -``` - -### Postgresql driver - -You also need to install the Postgresql jdbc driver. You can see all available Postgresql jdbc libraries versions at -[MVN Repository](http://mvnrepository.com/artifact/org.postgresql/postgresql). - -```groovy -dependencies { - ... - provided 'org.postgresql:postgresql:9.4.1211.jre7' - ... -} -``` - -### Hibernate plugin - -It's also necessary to install the Grails-Hibernate plugin. Depending if you use Hibernate 4 or Hibernate 5 you'll need -different dependencies. Please make sure you use the latest versions of the plugin and the hibernate dependencies - - -```groovy -// Hibernate 4 -buildscript { - ... - dependencies { - ... - classpath "org.grails.plugins:hibernate4:6.0.3" - } -} - -dependencies { - ... - compile "org.grails.plugins:hibernate4" - compile "org.hibernate:hibernate-core:4.3.11.Final" - compile "org.hibernate:hibernate-ehcache:4.3.11.Final" - ... -} -``` - -```groovy -// Hibernate 5 -buildscript { - ... - dependencies { - ... - classpath "org.grails.plugins:hibernate5:6.0.3" - } -} - -dependencies { - ... - compile "org.grails.plugins:hibernate5" - compile "org.hibernate:hibernate-core:5.1.1.Final" - compile "org.hibernate:hibernate-ehcache:5.1.1.Final" - ... + //... + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.grails.plugins:grails-postgresql-extensions:' + //... } ``` ## Configuration -After install the plugin you have to use a new Postgresql Hibernate Dialect in your application. Add it to the +After installing the plugin you have to use a new PostgreSQL Hibernate Dialect in your application. Add it to the `grails-app/conf/application.yml` file: ```yaml @@ -144,24 +76,16 @@ dataSource: dbCreate: update hibernate: - dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect + dialect: gpc.pgext.hibernate.PostgresqlExtensionsDialect ``` -If you just only add the dialect, hibernate will create a new sequence for every table to generate the sequential ids +If you just add the dialect, hibernate will create a new sequence for every table to generate the sequential ids used for the primary keys instead of a global sequence for all your tables. -If you're using Hibernate 4 you can also deactivate this behaviour and create only one unique sequence for all the tables with the following +You can also deactivate this behavior and create only one unique sequence for all the tables with the following property in your datasource definition: -```yaml -dataSource: - postgresql: - extensions: - sequence_per_table: false -} -``` - -For Hibernate 5 add the following to `grails-app/conf/application.groovy`: +Add the following to `grails-app/conf/application.groovy`: ```groovy grails.gorm.default.mapping = { @@ -182,14 +106,14 @@ The plugin supports the definition of `Integer`, `Long`, `Float`, `Double`, `Str classes. The `Enum` arrays behaves almost identical to `Integer` arrays in that they store and retrieve an array of ints. The -difference, however, is that this is used with an Array of Enums, rather than Ints. The Enums are serialized to their +difference, however, is that this is used with an Array of Enums, rather than ints. The Enums are serialized to their ordinal value before persisted to the database. On retrieval, they are then converted back into their original `Enum` type. #### Example ```groovy -import net.kaleidos.hibernate.usertype.ArrayType +import gpc.pgext.hibernate.usertype.ArrayType class Like { Integer[] favoriteNumbers = [] @@ -244,11 +168,11 @@ And now, with `psql`: 1 | {123,239,3498239,2344235} | {0.3,0.1} | {100.33,44.11} | {Spiderman,"Blade Runner",Starwars} | {5,17,9,6} | {0,2} ``` -#### Criterias +#### Criteria -The plugin also includes some hibernate criterias to use in your queries. Please check the -[services](https://github.com/kaleidos/grails-postgresql-extensions/tree/master/grails-app/services/test/criteria/array) -and the [tests](https://github.com/kaleidos/grails-postgresql-extensions/tree/master/src/integration-test/groovy/net/kaleidos/hibernate/array) +The plugin also includes some Hibernate criteria to use in your queries. Please check the +[services](https://github.com/gpc/grails-postgresql-extensions/tree/master/test-apps/app1/grails-app/services/app/criteria/array) +and the [tests](https://github.com/gpc/grails-postgresql-extensions/tree/master/test-apps/app1/src/integration-test/groovy/array) created to see all usage examples. You can also check the official [Postgresql Array operators](http://www.postgresql.org/docs/9.4/static/functions-array.html#ARRAY-OPERATORS-TABLE). @@ -729,16 +653,6 @@ It's important to note that the "raw" sql is appended to the criteria, so you ne if not you'll get a sql error during runtime. -## Authors - -You can send any questions to: - -- Iván López: lopez.ivan@gmail.com ([@ilopmar](https://twitter.com/ilopmar)) -- Alonso Torres: alonso.javier.torres@gmail.com ([@alotor](https://twitter.com/alotor)) - -Collaborations are appreciated :-) - - ## Release Notes Version | Date | Comments diff --git a/build.gradle b/build.gradle index 19ec900..6106700 100644 --- a/build.gradle +++ b/build.gradle @@ -1,119 +1,23 @@ -buildscript { - ext { - grailsVersion = project.grailsVersion - gormVersion = project.gormVersion - } - repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { url 'https://repo.grails.org/grails/core' } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate5:7.0.0" - classpath 'net.saliman:gradle-cobertura-plugin:2.5.4' - classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2' - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" - } -} - -version '7.0.0' -group 'org.grails.plugins' - -apply plugin: 'eclipse' -apply plugin: 'idea' -apply plugin: 'org.grails.grails-plugin' -apply plugin: 'org.grails.grails-plugin-publish' -apply plugin: 'cobertura' -apply plugin: 'com.github.kt3k.coveralls' -apply plugin: 'com.jfrog.bintray' - -ext { - gradleWrapperVersion = project.gradleWrapperVersion - grailsVersion = project.grailsVersion - gormVersion = project.gormVersion - hibernateVersion = project.hibernateVersion -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -cobertura { - coverageExcludes = ['.*BootStrap.*', '.*test.*'] - coverageFormats = ['html', 'xml'] -} - -jar { - exclude 'test/**' - exclude 'UrlMappings.groovy' - exclude 'error.gsp' - exclude 'messages.properties' - exclude 'hibernate/**' - exclude 'layouts/**' - exclude 'spring/**' -} - -repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { url 'https://repo.grails.org/grails/core' } -} - -dependencies { - // Grails deps - compile 'org.springframework.boot:spring-boot-starter-logging' - compile 'org.springframework.boot:spring-boot-autoconfigure' - compile 'org.grails:grails-core' - compile 'org.springframework.boot:spring-boot-starter-actuator' - compile 'org.springframework.boot:spring-boot-starter-tomcat' - compile 'org.grails:grails-dependencies' - compile 'org.grails:grails-web-boot' - console 'org.grails:grails-console' +// Workaround needed for nexus publishing bug +// version and group must be specified in the root project +// https://github.com/gradle-nexus/publish-plugin/issues/310 +version = projectVersion +group = 'this.will.be.overridden' - profile 'org.grails.profiles:web-plugin' - provided 'org.grails:grails-plugin-services' - provided 'org.grails:grails-plugin-domain-class' - testCompile 'org.grails:grails-gorm-testing-support' +subprojects { - provided 'org.postgresql:postgresql:42.2.6' - - // plugins - provided 'org.grails.plugins:hibernate5' - - // libraries - provided "org.hibernate:hibernate-core:$hibernateVersion" - provided("org.hibernate:hibernate-ehcache:$hibernateVersion") { - exclude group: 'net.sf.ehcache', module: 'ehcache' - } - compile 'com.google.code.gson:gson:2.8.4' -} - -bintray { - pkg { - userOrg = 'kaleidos' - name = 'postgresql-extensions' - issueTrackerUrl = 'https://github.com/kaleidosnet/grails-grails-postgresql-extensions/issues' - vcsUrl = 'https://github.com/kaleidosnet/grails-grails-postgresql-extensions/issues' + repositories { + maven { url = 'https://repo.grails.org/grails/restricted' } + maven { + url = 'https://repository.apache.org/content/groups/snapshots' + content { + includeVersionByRegex('org[.]apache[.](grails|groovy).*', '.*', '.*-SNAPSHOT') + } + } } -} - -grailsPublish { - user = System.getenv("BINTRAY_USER") ?: '' - key = System.getenv("BINTRAY_KEY") ?: '' - userOrg = 'kaleidos' - repo = 'plugins' - websiteUrl = 'https://github.com/kaleidos/grails-postgresql-extensions' - license { - name = 'Apache-2.0' + if (name == 'grails-postgresql-extensions') { + // This has to be applied here in the root project due to the nexus plugin requirements + apply plugin: 'org.apache.grails.gradle.grails-publish' } - issueTrackerUrl = 'https://github.com/kaleidosnet/grails-grails-postgresql-extensions/issues' - vcsUrl = 'https://github.com/kaleidos/grails-postgresql-extensions.git' - title = 'PostgreSQL Extensions' - desc = 'This is a grails plugin that provides hibernate user types to use Postgresql native types. It also provides new criterias to query this new native types.' - developers = [ivan: 'Iván López'] } - -apply from: "${rootProject.projectDir}/gradle/testVerbose.gradle" diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..56437fb --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'groovy-gradle-plugin' +} + +def versions = new Properties() +file('../gradle.properties').withInputStream { + versions.load(it) +} + +repositories { + maven { url = 'https://repo.grails.org/grails/restricted' } + maven { + url = 'https://repository.apache.org/content/groups/snapshots' + content { + includeVersionByRegex('org[.]apache[.](grails|groovy).*', '.*', '.*-SNAPSHOT') + } + mavenContent { snapshotsOnly() } + } +} + +dependencies { + implementation platform("org.apache.grails:grails-bom:${versions['grailsVersion']}") + implementation 'org.apache.grails:grails-gradle-plugins' +} diff --git a/gradle.properties b/gradle.properties index bbc3fbc..5293811 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,14 @@ -gradleWrapperVersion=5.1.1 -grailsVersion=4.0.0 -hibernateVersion=5.4.0.Final -gormVersion=7.0.2.RELEASE +projectVersion=8.0.0-SNAPSHOT + +grailsVersion=7.0.0-RC2 +javaVersion=17 + +hibernate5Version=5.6.15.Final + +# This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs +# https://github.com/grails/grails-gradle-plugin/issues/222 +slf4jPreventExclusion=true + +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.parallel=true diff --git a/gradle/test-config.gradle b/gradle/test-config.gradle new file mode 100644 index 0000000..f31b098 --- /dev/null +++ b/gradle/test-config.gradle @@ -0,0 +1,39 @@ +dependencies { + add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher') +} + +tasks.withType(Test).configureEach { + + useJUnitPlatform() + + afterTest { desc, result -> + logger.quiet( + ' -- Executed test {} [{}] with result: {}', + desc.name, desc.className, result.resultType + ) + } + testLogging { + events('passed', 'skipped', 'failed') // 'standardOut' + showExceptions = true + exceptionFormat = 'full' + showCauses = true + showStackTraces = true + + // set options for log level DEBUG and INFO + debug { + events('passed', 'skipped', 'failed')//, 'started', 'standardOut', 'standardError' + exceptionFormat = 'full' + } + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + } + + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + logger.quiet "\n${'-' * repeatLength}\n${startItem}${output}${endItem}\n${'-' * repeatLength}" + } + } +} \ No newline at end of file diff --git a/gradle/testVerbose.gradle b/gradle/testVerbose.gradle deleted file mode 100644 index a1149ee..0000000 --- a/gradle/testVerbose.gradle +++ /dev/null @@ -1,29 +0,0 @@ -tasks.withType(Test) { - afterTest { desc, result -> - //logger.quiet " -- Executed test ${desc.name} [${desc.className}] with result: ${result.resultType}" - } - testLogging { - events "passed", "skipped", "failed"//, "standardOut" - showExceptions true - exceptionFormat "full" - showCauses true - showStackTraces true - - // set options for log level DEBUG and INFO - debug { - events "passed", "skipped", "failed"//, "started", "standardOut", "standardError" - exceptionFormat "full" - } - info.events = debug.events - info.exceptionFormat = debug.exceptionFormat - - afterSuite { desc, result -> - if (!desc.parent) { // will match the outermost suite - def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" - def startItem = '| ', endItem = ' |' - def repeatLength = startItem.length() + output.length() + endItem.length() - logger.quiet "\n${'-' * repeatLength}\n${startItem}${output}${endItem}\n${'-' * repeatLength}" - } - } - } -} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 94336fcae912db8a11d55634156fa011f4686124..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml deleted file mode 100644 index 9be0559..0000000 --- a/grails-app/conf/application.yml +++ /dev/null @@ -1,28 +0,0 @@ -grails: - profile: web-plugin - codegen: - defaultPackage: grails.postgresql.extensions -info: - app: - name: '@info.app.name@' - version: '@info.app.version@' - grailsVersion: '@info.app.grailsVersion@' -spring: - groovy: - template: - check-template-location: false - - ---- -dataSource: - pooled: true - jmxExport: true - driverClassName: org.postgresql.Driver - dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect - username: postgres_extensions - password: postgres_extensions - url: jdbc:postgresql://localhost/pg_extensions_test - dbCreate: create-drop - -hibernate: - dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect diff --git a/grails-app/conf/logback.groovy b/grails-app/conf/logback.groovy deleted file mode 100644 index 647d141..0000000 --- a/grails-app/conf/logback.groovy +++ /dev/null @@ -1,30 +0,0 @@ -import grails.util.BuildSettings -import grails.util.Environment - -// See http://logback.qos.ch/manual/groovy.html for details on configuration -appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } -} - -def targetDir = BuildSettings.TARGET_DIR -if (Environment.isDevelopmentMode() && targetDir != null) { - appender("FULL_STACKTRACE", FileAppender) { - file = "${targetDir}/stacktrace.log" - append = true - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } - } - logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) - root(ERROR, ['STDOUT', 'FULL_STACKTRACE']) -} else { - root(ERROR, ['STDOUT']) -} - -logger('org.hibernate.SQL', DEBUG) - -logger('org.hibernate.type', TRACE) - -logger('org.hibernate.tool.hbm2ddl', DEBUG) diff --git a/grails-app/domain/test/array/TestDouble.groovy b/grails-app/domain/test/array/TestDouble.groovy deleted file mode 100644 index 841b25a..0000000 --- a/grails-app/domain/test/array/TestDouble.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - -@ToString -class TestDouble { - - Double[] doubleNumbers - - static mapping = { - doubleNumbers type: ArrayType, params: [type: Double] - } - - static constraints = { - doubleNumbers nullable: true - } - -} diff --git a/grails-app/domain/test/array/TestFloat.groovy b/grails-app/domain/test/array/TestFloat.groovy deleted file mode 100644 index 3f5ef76..0000000 --- a/grails-app/domain/test/array/TestFloat.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - - -@ToString -class TestFloat { - - Float[] floatNumbers - - static mapping = { - floatNumbers type: ArrayType, params: [type: Float] - } - - static constraints = { - floatNumbers nullable: true - } - -} diff --git a/grails-app/domain/test/array/TestInteger.groovy b/grails-app/domain/test/array/TestInteger.groovy deleted file mode 100644 index 8237f24..0000000 --- a/grails-app/domain/test/array/TestInteger.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - -@ToString -class TestInteger { - - Integer[] integerNumbers - - static mapping = { - integerNumbers type: ArrayType, params: ["type": Integer] - } - - static constraints = { - integerNumbers nullable: true - } - -} diff --git a/grails-app/domain/test/array/TestLong.groovy b/grails-app/domain/test/array/TestLong.groovy deleted file mode 100644 index a123e04..0000000 --- a/grails-app/domain/test/array/TestLong.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - -@ToString -class TestLong { - - Long[] longNumbers - - static mapping = { - longNumbers type: ArrayType, params: [type: Long] - } - - static constraints = { - longNumbers nullable: true - } - -} diff --git a/grails-app/domain/test/array/TestString.groovy b/grails-app/domain/test/array/TestString.groovy deleted file mode 100644 index 0d84b7b..0000000 --- a/grails-app/domain/test/array/TestString.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - -@ToString -class TestString { - - String[] stringArray - - static mapping = { - stringArray type: ArrayType, params: [type: String] - } - - static constraints = { - stringArray nullable: true - } - -} diff --git a/grails-app/domain/test/array/TestUuid.groovy b/grails-app/domain/test/array/TestUuid.groovy deleted file mode 100644 index bc02d1a..0000000 --- a/grails-app/domain/test/array/TestUuid.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.array - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType - -@ToString -class TestUuid { - - UUID[] uuidArray - - static mapping = { - uuidArray type: ArrayType, params: [type: UUID] - } - - static constraints = { - uuidArray nullable: true - } - -} diff --git a/grails-app/domain/test/criteria/array/User.groovy b/grails-app/domain/test/criteria/array/User.groovy deleted file mode 100644 index 3144234..0000000 --- a/grails-app/domain/test/criteria/array/User.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package test.criteria.array - -class User { - - String name - Like like - - static mapping = { - table "pg_extensions_user" - } - - String toString() { - name - } - -} diff --git a/grails-app/domain/test/hstore/TestHstoreMap.groovy b/grails-app/domain/test/hstore/TestHstoreMap.groovy deleted file mode 100644 index 66890c3..0000000 --- a/grails-app/domain/test/hstore/TestHstoreMap.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package test.hstore - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.HstoreMapType - -@ToString -class TestHstoreMap { - - String name - Integer luckyNumber - - Map testAttributes - - static constraints = { - name nullable: true - luckyNumber nullable: true - } - - static mapping = { - testAttributes type: HstoreMapType - } - -} - - diff --git a/grails-app/domain/test/json/TestMapJson.groovy b/grails-app/domain/test/json/TestMapJson.groovy deleted file mode 100644 index 2028fb8..0000000 --- a/grails-app/domain/test/json/TestMapJson.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package test.json - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.JsonMapType - -@ToString -class TestMapJson { - - Map data - - static constraints = { - data nullable: true - } - - static mapping = { - data type: JsonMapType - } -} \ No newline at end of file diff --git a/grails-app/domain/test/json/TestMapJsonb.groovy b/grails-app/domain/test/json/TestMapJsonb.groovy deleted file mode 100644 index aadcef3..0000000 --- a/grails-app/domain/test/json/TestMapJsonb.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.json - -import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.JsonbMapType - -@ToString -class TestMapJsonb { - - Map data - - static constraints = { - data nullable: true - } - - static mapping = { - data type: JsonbMapType - } - -} \ No newline at end of file diff --git a/grails-app/services/test/criteria/array/PgArrayTestSearchService.groovy b/grails-app/services/test/criteria/array/PgArrayTestSearchService.groovy deleted file mode 100644 index e850fe5..0000000 --- a/grails-app/services/test/criteria/array/PgArrayTestSearchService.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package test.criteria.array - -class PgArrayTestSearchService { - - static transactional = false - - List search(String field, String criteriaName, value) { - Like.withCriteria { - "${criteriaName}" field, value - } - } - - List search(String field, String criteriaName) { - Like.withCriteria { - "${criteriaName}" field - } - } - - List searchWithJoin(String field, String criteriaName, value) { - User.withCriteria { - like { - "${criteriaName}" field, value - } - } - } - - List searchWithJoin(String field, String criteriaName) { - User.withCriteria { - like { - "${criteriaName}" field - } - } - } - - List searchWithJoinByStringOrInteger(Map params, String criteriaName) { - User.withCriteria { - like { - or { - params.each { entry-> - "${criteriaName}" (entry.key, entry.value) - } - } - } - } - } - - List searchWithJoinAnd(Map params, String criteriaName) { - User.withCriteria { - like { - and { - params.each { entry-> - "${criteriaName}" (entry.key, entry.value) - } - } - } - } - } -} diff --git a/grails-app/services/test/criteria/hstore/PgHstoreTestSearchService.groovy b/grails-app/services/test/criteria/hstore/PgHstoreTestSearchService.groovy deleted file mode 100644 index 8c7676d..0000000 --- a/grails-app/services/test/criteria/hstore/PgHstoreTestSearchService.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package test.criteria.hstore - -import test.hstore.TestHstoreMap - -class PgHstoreTestSearchService { - static transactional = false - - List search(String field, String criteriaName, value) { - TestHstoreMap.withCriteria { - "${criteriaName}" field, value - } - } -} diff --git a/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy b/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy deleted file mode 100644 index f2f5b2a..0000000 --- a/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package test.criteria.json - -import test.json.TestMapJson - -class PgJsonTestSearchService { - static transactional = false - - List search(String criteriaName, String field, String jsonAttribute, value) { - TestMapJson.withCriteria { - "${criteriaName}" field, jsonAttribute, value.toString() - } - } - - List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) { - TestMapJson.withCriteria { - "${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString() - } - } -} diff --git a/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy b/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy deleted file mode 100644 index 34e8f66..0000000 --- a/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package test.criteria.json - -import test.json.TestMapJsonb - -class PgJsonbTestSearchService { - static transactional = false - - List search(String criteriaName, String field, value) { - TestMapJsonb.withCriteria { - "${criteriaName}" field, value - } - } - - List search(String criteriaName, String field, String jsonAttribute, value) { - TestMapJsonb.withCriteria { - "${criteriaName}" field, jsonAttribute, value.toString() - } - } - - List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) { - TestMapJsonb.withCriteria { - "${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString() - } - } -} diff --git a/grails-app/services/test/order/PgOrderService.groovy b/grails-app/services/test/order/PgOrderService.groovy deleted file mode 100644 index 3108958..0000000 --- a/grails-app/services/test/order/PgOrderService.groovy +++ /dev/null @@ -1,21 +0,0 @@ -package test.order - -import static net.kaleidos.hibernate.order.OrderByRandom.byRandom -import static net.kaleidos.hibernate.order.OrderBySqlFormula.sqlFormula - -import test.json.TestMapJsonb - -class PgOrderService { - - List orderByJson() { - return TestMapJsonb.withCriteria { - order sqlFormula("(data->'name') desc") - } - } - - List orderByRandom() { - return TestMapJsonb.withCriteria { - order byRandom() - } - } -} diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..732ace6 --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,76 @@ +plugins { + id 'java-library' + id 'org.apache.grails.gradle.grails-plugin' +} + +version = projectVersion +group = 'org.grails.plugins' + +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', + ] +} + +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/plugin/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/plugin/src/main/groovy/gpc/pgext/GrailsPostgresqlExtensionsGrailsPlugin.groovy new file mode 100644 index 0000000..b578b83 --- /dev/null +++ b/plugin/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 = '7.0.0-SNAPSHOT > *' + def title = 'Grails PostgreSQL Extensions Plugin' + 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/src/main/groovy/net/kaleidos/hibernate/PostgresqlExtensionsDialect.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy similarity index 81% rename from src/main/groovy/net/kaleidos/hibernate/PostgresqlExtensionsDialect.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy index 0edb9d3..f5d8a50 100644 --- a/src/main/groovy/net/kaleidos/hibernate/PostgresqlExtensionsDialect.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/PostgresqlExtensionsDialect.groovy @@ -1,13 +1,15 @@ -package net.kaleidos.hibernate +package gpc.pgext.hibernate + +import java.sql.Types import groovy.transform.CompileStatic -import net.kaleidos.hibernate.usertype.ArrayType -import net.kaleidos.hibernate.usertype.HstoreMapType -import net.kaleidos.hibernate.usertype.JsonMapType -import net.kaleidos.hibernate.usertype.JsonbMapType + import org.hibernate.dialect.PostgreSQL95Dialect -import java.sql.Types +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 PostgreSQL95Dialect { diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayExpression.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy similarity index 50% rename from src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayExpression.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy index 2bf1046..f2a1832 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayExpression.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayExpression.groovy @@ -1,16 +1,16 @@ -package net.kaleidos.hibernate.criterion.array +package gpc.pgext.hibernate.criterion.array import groovy.transform.CompileStatic -import net.kaleidos.hibernate.usertype.ArrayType -import net.kaleidos.hibernate.utils.PgArrayUtils + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue import org.hibernate.type.CustomType -import org.hibernate.type.Type + +import gpc.pgext.hibernate.usertype.ArrayType +import gpc.pgext.hibernate.utils.PgArrayUtils @CompileStatic class PgArrayExpression implements Criterion { @@ -21,7 +21,7 @@ class PgArrayExpression implements Criterion { private final Object value private final String op - protected PgArrayExpression(String propertyName, Object value, String op) { + PgArrayExpression(String propertyName, Object value, String op) { this.propertyName = propertyName this.value = value this.op = op @@ -29,47 +29,37 @@ class PgArrayExpression implements Criterion { @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - ArrayType arrayType = checkAndGetArrayType(criteria, criteriaQuery) - - String postgresArrayType = PgArrayUtils.getNativeSqlType(arrayType.getTypeClass()) + "[]" - - return StringHelper.join( - " and ", - StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), " ${op} CAST(? as ${postgresArrayType})") - ) + def arrayType = checkAndGetArrayType(criteria, criteriaQuery) + def postgresArrayType = PgArrayUtils.getNativeSqlType(arrayType.getTypeClass()) + '[]' + criteriaQuery.findColumns(propertyName, criteria) + .collect {"$it $op CAST(? as $postgresArrayType)" } + .join(' and ') } @Override TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - ArrayType arrayType = checkAndGetArrayType(criteria, criteriaQuery) - - Object[] arrValue - if (arrayType.typeClass.isEnum()) { - arrValue = PgArrayUtils.getValueAsArrayOfType(value, Integer, mapValueToEnumOrdinal()) - } else { - arrValue = PgArrayUtils.getValueAsArrayOfType(value, arrayType.typeClass) - } - - return criteriaQuery.getTypedValue(criteria, propertyName, arrValue) as TypedValue[] + def arrayType = checkAndGetArrayType(criteria, criteriaQuery) + def arrValue = arrayType.typeClass.isEnum() ? + PgArrayUtils.getValueAsArrayOfType(value, Integer, mapValueToEnumOrdinal()) : + PgArrayUtils.getValueAsArrayOfType(value, arrayType.typeClass) + criteriaQuery.getTypedValue(criteria, propertyName, arrValue) as TypedValue[] } private PgArrayUtils.MapFunction mapValueToEnumOrdinal() { - { Object o -> + return { Object o -> try { return (o as Enum).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", e) } } as PgArrayUtils.MapFunction } private ArrayType checkAndGetArrayType(Criteria criteria, CriteriaQuery criteriaQuery) { - Type propertyType = criteriaQuery.getType(criteria, propertyName) - + def propertyType = criteriaQuery.getType(criteria, propertyName) if (!(propertyType instanceof CustomType) || !((propertyType as CustomType).userType instanceof ArrayType)) { - throw new HibernateException("Property is not an instance of the postgres type ArrayType. Type is: ${propertyType.class}") + throw new HibernateException("Property is not an instance of the postgres type ArrayType. Type is: $propertyType.class") } - - return (propertyType as CustomType).getUserType() as ArrayType + (propertyType as CustomType).userType as ArrayType } } diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayILikeFunction.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy similarity index 66% rename from src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayILikeFunction.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy index dbc4dc8..b680031 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgArrayILikeFunction.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgArrayILikeFunction.groovy @@ -1,9 +1,9 @@ -package net.kaleidos.hibernate.criterion.array +package gpc.pgext.hibernate.criterion.array import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue @@ -17,18 +17,16 @@ class PgArrayILikeFunction implements Criterion { private final String propertyName private final String value - protected PgArrayILikeFunction(String propertyName, String value) { + PgArrayILikeFunction(String propertyName, String value) { this.propertyName = propertyName this.value = value } @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - String[] columns = StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), "") - for (int i = 0; i < columns.length; i++) { - columns[i] = "text(${columns[i]}) ilike ?" - } - return StringHelper.join(" and ", columns) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "text($it) ilike ?" } + .join(' and ') } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgEmptinessExpression.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy similarity index 71% rename from src/main/groovy/net/kaleidos/hibernate/criterion/array/PgEmptinessExpression.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy index b62c481..62a7de3 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/array/PgEmptinessExpression.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/array/PgEmptinessExpression.groovy @@ -1,9 +1,9 @@ -package net.kaleidos.hibernate.criterion.array +package gpc.pgext.hibernate.criterion.array import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue @@ -18,17 +18,16 @@ class PgEmptinessExpression implements Criterion { private static final TypedValue[] NO_VALUES = new TypedValue[0] - protected PgEmptinessExpression(String propertyName, String op) { + PgEmptinessExpression(String propertyName, String op) { this.propertyName = propertyName this.op = op } @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - StringHelper.join( - " and ", - StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), " ${op} '{}'") - ) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "$it $op '{}'" } + .join(' and ') } @Override diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy new file mode 100644 index 0000000..24e9ea0 --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy @@ -0,0 +1,22 @@ +package gpc.pgext.hibernate.criterion.hstore + +import groovy.transform.CompileStatic + +import org.hibernate.Criteria +import org.hibernate.HibernateException +import org.hibernate.criterion.CriteriaQuery + +@CompileStatic +class PgHstoreILikeValueFunction extends PgHstoreValueFunction { + + PgHstoreILikeValueFunction(String propertyName, Object value) { + super(propertyName, value, '') + } + + @Override + String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { + criteriaQuery.findColumns(propertyName, criteria) + .collect { "text(avals($it)) ilike ?" } + .join(' and ') + } +} diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy similarity index 62% rename from src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy index 9be43e9..19e4fef 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreOperatorExpression.groovy @@ -1,16 +1,18 @@ -package net.kaleidos.hibernate.criterion.hstore +package gpc.pgext.hibernate.criterion.hstore import groovy.transform.CompileStatic -import net.kaleidos.hibernate.usertype.HstoreHelper + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue +import gpc.pgext.hibernate.usertype.HstoreHelper + @CompileStatic class PgHstoreOperatorExpression implements Criterion { + private static final long serialVersionUID = 2872183637309166619L private final String propertyName @@ -18,7 +20,11 @@ class PgHstoreOperatorExpression implements Criterion { private final String operator private static final TypedValue[] NO_VALUES = new TypedValue[0] - protected PgHstoreOperatorExpression(String propertyName, Map value, String operator) { + PgHstoreOperatorExpression(String propertyName, Object value, String operator) { + this(propertyName, value as Map, operator) + } + + PgHstoreOperatorExpression(String propertyName, Map value, String operator) { this.propertyName = propertyName this.value = value this.operator = operator @@ -26,11 +32,9 @@ class PgHstoreOperatorExpression implements Criterion { @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - String[] columns = StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), "") - for (int i = 0; i < columns.length; i++) { - columns[i] = "${columns[i]} ${operator} '${HstoreHelper.toString(value)}'" - } - return StringHelper.join(" and ", columns) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "$it $operator '${HstoreHelper.toString(value)}'" } + .join(' and ') } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreValueFunction.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy similarity index 66% rename from src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreValueFunction.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy index d384d0c..8174d37 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreValueFunction.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/hstore/PgHstoreValueFunction.groovy @@ -1,9 +1,9 @@ -package net.kaleidos.hibernate.criterion.hstore +package gpc.pgext.hibernate.criterion.hstore import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue @@ -11,13 +11,14 @@ import org.hibernate.type.StringType @CompileStatic class PgHstoreValueFunction implements Criterion { + private static final long serialVersionUID = 2872183637309166619L protected final String propertyName protected final Object value protected final String function - protected PgHstoreValueFunction(String propertyName, Object value, String function) { + PgHstoreValueFunction(String propertyName, Object value, String function) { this.propertyName = propertyName this.value = value this.function = function @@ -25,11 +26,9 @@ class PgHstoreValueFunction implements Criterion { @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - String[] columns = StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), "") - for (int i = 0; i < columns.length; i++) { - columns[i] = "${function}(${columns[i]}, ?)" - } - return StringHelper.join(" and ", columns) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "$function($it, ?)" } + .join(' and ') } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonExpression.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy similarity index 73% rename from src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonExpression.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy index 0ef62f3..e050557 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonExpression.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonExpression.groovy @@ -1,9 +1,9 @@ -package net.kaleidos.hibernate.criterion.json +package gpc.pgext.hibernate.criterion.json import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue @@ -20,7 +20,7 @@ class PgJsonExpression implements Criterion { private final String sqlOp private final Object value - protected PgJsonExpression(String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object value) { + PgJsonExpression(String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object value) { this.propertyName = propertyName this.jsonOp = jsonOp this.jsonAttribute = jsonAttribute @@ -30,10 +30,9 @@ class PgJsonExpression implements Criterion { @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - return StringHelper.join( - " and ", - StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), jsonOp + "'" + jsonAttribute + "' " + sqlOp + " ?") - ) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "$it$jsonOp'$jsonAttribute' $sqlOp ?" } + .join(' and ') } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonbOperator.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy similarity index 72% rename from src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonbOperator.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy index a30b5ff..425e980 100644 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/json/PgJsonbOperator.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/criterion/json/PgJsonbOperator.groovy @@ -1,9 +1,9 @@ -package net.kaleidos.hibernate.criterion.json +package gpc.pgext.hibernate.criterion.json import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper import org.hibernate.criterion.CriteriaQuery import org.hibernate.criterion.Criterion import org.hibernate.engine.spi.TypedValue @@ -17,7 +17,7 @@ class PgJsonbOperator implements Criterion { private final Object value private final String op - protected PgJsonbOperator(String propertyName, Object value, String op) { + PgJsonbOperator(String propertyName, Object value, String op) { this.propertyName = propertyName this.value = value this.op = op @@ -25,10 +25,9 @@ class PgJsonbOperator implements Criterion { @Override String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - return StringHelper.join( - " and ", - StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), " ${op} ?") - ) + criteriaQuery.findColumns(propertyName, criteria) + .collect { "$it $op ?" } + .join(' and ') } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/order/OrderByRandom.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy similarity index 94% rename from src/main/groovy/net/kaleidos/hibernate/order/OrderByRandom.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy index 1d3edb2..02ed0f4 100644 --- a/src/main/groovy/net/kaleidos/hibernate/order/OrderByRandom.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderByRandom.groovy @@ -1,6 +1,7 @@ -package net.kaleidos.hibernate.order +package gpc.pgext.hibernate.order import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException import org.hibernate.criterion.CriteriaQuery diff --git a/src/main/groovy/net/kaleidos/hibernate/order/OrderBySqlFormula.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy similarity index 88% rename from src/main/groovy/net/kaleidos/hibernate/order/OrderBySqlFormula.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy index b40fc35..ba80391 100644 --- a/src/main/groovy/net/kaleidos/hibernate/order/OrderBySqlFormula.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/order/OrderBySqlFormula.groovy @@ -1,6 +1,7 @@ -package net.kaleidos.hibernate.order +package gpc.pgext.hibernate.order import groovy.transform.CompileStatic + import org.hibernate.Criteria import org.hibernate.HibernateException import org.hibernate.criterion.CriteriaQuery @@ -8,13 +9,15 @@ import org.hibernate.criterion.Order /** * Extends {@link org.hibernate.criterion.Order} to allow ordering by an SQL formula passed by the user. - * Is simply appends the sqlFormula passed by the user to the resulting SQL query, without any verification. + * It simply appends the sqlFormula passed by the user to the resulting SQL query, + * without any verification. * * From: http://blog.tremend.ro/2008/06/10/how-to-order-by-a-custom-sql-formulaexpression-when-using-hibernate-criteria-api/ * */ @CompileStatic class OrderBySqlFormula extends Order { + private String sqlFormula /** diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy new file mode 100644 index 0000000..f3fa5b9 --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/ArrayCriterias.groovy @@ -0,0 +1,150 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import org.hibernate.criterion.Criterion + +import grails.orm.HibernateCriteriaBuilder + +import gpc.pgext.hibernate.criterion.array.PgArrayExpression +import gpc.pgext.hibernate.criterion.array.PgArrayILikeFunction +import gpc.pgext.hibernate.criterion.array.PgEmptinessExpression + +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyValue +import static gpc.pgext.hibernate.utils.CriteriaUtils.throwRuntimeException +import static gpc.pgext.hibernate.utils.CriteriaUtils.validateExpression +import static gpc.pgext.hibernate.utils.CriteriaUtils.validateSimpleExpression + +@CompileStatic +class ArrayCriterias { + + /** + * 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 A Criterion instance + */ + static Criterion pgArrayContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgArrayContains', PgArrayExpression, 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 A Criterion instance + */ + static Criterion pgArrayIsContainedBy(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgArrayIsContainedBy', PgArrayExpression, 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 A Criterion instance + */ + static Criterion pgArrayOverlaps(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgArrayOverlaps', PgArrayExpression, propertyName, propertyValue, '&&') + } + + /** + * Creates an "is empty array" Criterion based on the specified property name + * @param propertyName The property name + * @return A Criterion instance + */ + static Criterion pgArrayIsEmpty(HibernateCriteriaBuilder self, String propertyName) { + if (!validateSimpleExpression(self)) { + throwRuntimeException( + self, + "Call to [pgArrayIsEmpty] with propertyName [$propertyName] not allowed here." + ) + } + addToCriteria( + self, + new PgEmptinessExpression( + calculatePropertyName(self, propertyName), + '=' + ) + ) + } + + /** + * Creates an "is not empty array" Criterion based on the specified property name + * @param propertyName The property name + * @return A Criterion instance + */ + static Criterion pgArrayIsNotEmpty(HibernateCriteriaBuilder self, String propertyName) { + if (!validateSimpleExpression(self)) { + throwRuntimeException( + self, + "Call to [pgArrayIsNotEmpty] with propertyName [$propertyName] not allowed here." + ) + } + addToCriteria( + self, + new PgEmptinessExpression( + 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 A Criterion instance + */ + static Criterion pgArrayIsEmptyOrContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + validateExpression(self, 'pgArrayIsEmptyOrContains', propertyName, propertyValue) + def name = calculatePropertyName(self, propertyName) + def value = calculatePropertyValue(self, propertyValue) + value ? + addToCriteria(self, new PgArrayExpression(name, value, '@>')) : + addToCriteria(self, new PgEmptinessExpression(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 A Criterion instance + */ + static Criterion pgArrayEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgArrayEquals', PgArrayExpression, 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 A Criterion instance + */ + static Criterion pgArrayNotEquals(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgArrayNotEquals', PgArrayExpression, 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 A Criterion instance + */ + static Criterion pgArrayILike(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + validateExpression(self, 'pgArrayLike', propertyName, propertyValue) + addToCriteria( + self, + new PgArrayILikeFunction( + calculatePropertyName(self, propertyName), + calculatePropertyValue(self, propertyValue) as String + ) + ) + } +} diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy new file mode 100644 index 0000000..f6b7304 --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/HstoreCriterias.groovy @@ -0,0 +1,33 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import org.hibernate.criterion.Criterion + +import grails.orm.HibernateCriteriaBuilder + +import gpc.pgext.hibernate.criterion.hstore.PgHstoreILikeValueFunction +import gpc.pgext.hibernate.criterion.hstore.PgHstoreOperatorExpression +import gpc.pgext.hibernate.criterion.hstore.PgHstoreValueFunction + +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria + +@CompileStatic +class HstoreCriterias { + + static Criterion pgHstoreContainsKey(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgHstoreContainsKey', PgHstoreValueFunction, propertyName, propertyValue, 'exist') + } + + static Criterion pgHstoreContains(HibernateCriteriaBuilder self, String propertyName, Map values) { + addToCriteria(self, 'pgHstoreContains', PgHstoreOperatorExpression, propertyName, values, '@>') + } + + static Criterion pgHstoreIsContained(HibernateCriteriaBuilder self, String propertyName, Map values) { + addToCriteria(self, 'pgHstoreIsContained', PgHstoreOperatorExpression, propertyName, values, '<@') + } + + static Criterion pgHstoreILikeValue(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgHstoreILikeValue', PgHstoreILikeValueFunction, propertyName, propertyValue) + } +} diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy new file mode 100644 index 0000000..ef79003 --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/postgresql/criteria/JsonCriterias.groovy @@ -0,0 +1,94 @@ +package gpc.pgext.hibernate.postgresql.criteria + +import groovy.transform.CompileStatic + +import org.hibernate.criterion.Criterion + +import grails.orm.HibernateCriteriaBuilder + +import gpc.pgext.hibernate.criterion.json.PgJsonExpression +import gpc.pgext.hibernate.criterion.json.PgJsonbOperator + +import static gpc.pgext.hibernate.utils.CriteriaUtils.addToCriteria +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyName +import static gpc.pgext.hibernate.utils.CriteriaUtils.calculatePropertyValue +import static gpc.pgext.hibernate.utils.CriteriaUtils.throwRuntimeException +import static gpc.pgext.hibernate.utils.CriteriaUtils.validateSimpleExpression + +@CompileStatic +class JsonCriterias { + + /** + * 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 A Criterion instance + */ + static Criterion pgJsonHasFieldValue(HibernateCriteriaBuilder self, String propertyName, String jsonAttribute, Object propertyValue) { + if (!validateSimpleExpression(self)) { + throwRuntimeException( + self, + "Call to [pgJsonHasFieldValue] with propertyName [$propertyName], jsonAttribute [$jsonAttribute] and value [$propertyValue] is not allowed here." + ) + } + addToCriteria( + self, + new PgJsonExpression( + calculatePropertyName(self, propertyName), + '->>', + jsonAttribute, + '=', + calculatePropertyValue(self, propertyValue) as String + ) + ) + } + + /** + * 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 A Criterion instance + */ + static Criterion pgJsonbContains(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgJsonbContains', PgJsonbOperator, 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 A Criterion instance + */ + static Criterion pgJsonbIsContained(HibernateCriteriaBuilder self, String propertyName, Object propertyValue) { + addToCriteria(self, 'pgJsonbIsContained', PgJsonbOperator, 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 A Criterion instance + */ + static Criterion pgJson(HibernateCriteriaBuilder self, String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object propertyValue) { + if (!validateSimpleExpression(self)) { + throwRuntimeException( + self, + "Call to [pgJson] with propertyName [$propertyName], json operator [$jsonOp], jsonAttribute [$jsonAttribute], sql operator [$sqlOp] and value [$propertyValue] is not allowed here." + ) + } + addToCriteria( + self, + new PgJsonExpression( + calculatePropertyName(self, propertyName), + jsonOp, + jsonAttribute, + sqlOp, + calculatePropertyValue(self, propertyValue) as String + ) + ) + } +} diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/ArrayType.java b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java similarity index 93% rename from src/main/groovy/net/kaleidos/hibernate/usertype/ArrayType.java rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java index 67f2a2f..7d0c7e8 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/ArrayType.java +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/ArrayType.java @@ -1,10 +1,4 @@ -package net.kaleidos.hibernate.usertype; - -import net.kaleidos.hibernate.utils.PgArrayUtils; -import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.usertype.ParameterizedType; -import org.hibernate.usertype.UserType; +package gpc.pgext.hibernate.usertype; import java.io.Serializable; import java.sql.Array; @@ -18,7 +12,15 @@ import java.util.Properties; import java.util.UUID; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +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; @@ -27,7 +29,7 @@ public class ArrayType implements UserType, ParameterizedType { public static final int DOUBLE_ARRAY = 90006; public static final int UUID_ARRAY = 90007; - private static final Map CLASS_TO_SQL_CODE = new HashMap(); + private static final Map, Integer> CLASS_TO_SQL_CODE = new HashMap<>(); static { CLASS_TO_SQL_CODE.put(Integer.class, INTEGER_ARRAY); @@ -151,7 +153,7 @@ public int[] sqlTypes() { @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { Object result = null; - Class typeArrayClass = java.lang.reflect.Array.newInstance(typeClass, 0).getClass(); + Class typeArrayClass = java.lang.reflect.Array.newInstance(typeClass, 0).getClass(); Array sqlArray = rs.getArray(names[0]); if (!rs.wasNull()) { Object array = sqlArray.getArray(); @@ -176,7 +178,7 @@ public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSes } Object[] valueToSet = (Object[]) value; - Class typeArrayClass = java.lang.reflect.Array.newInstance(typeClass, 0).getClass(); + Class typeArrayClass = java.lang.reflect.Array.newInstance(typeClass, 0).getClass(); if (typeClass.isEnum()) { typeArrayClass = Integer[].class; @@ -203,7 +205,7 @@ public Class getTypeClass() { private void ensureBidiMapInitialized() throws HibernateException { try { if (bidiMap == null) { - bidiMap = new BidiEnumMap(typeClass); + bidiMap = new BidiEnumMap((Class) typeClass); } } catch (Exception e) { throw new HibernateException("Unable to create bidirectional enum map for " + typeClass, e); diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy new file mode 100644 index 0000000..6a7986f --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/BidiEnumMap.groovy @@ -0,0 +1,61 @@ +package gpc.pgext.hibernate.usertype + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +@Slf4j +@CompileStatic +class BidiEnumMap implements Serializable { + + static final String ENUM_ID_ACCESSOR = 'getId' + + private static final long serialVersionUID = 3325751131102095834L + + private final Map enumToKey + private final Map keytoEnum + + BidiEnumMap(Class enumClass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + log.debug('Building Bidirectional Enum Map...') + + def enumToKey = new EnumMap(enumClass) + def keytoEnum = new HashMap() + + def idAccessor = getIdAccessor(enumClass) + def values = getEnumValues(enumClass) + + for (Enum value : values) { + def id = idAccessor.invoke(value) + enumToKey.put(value, id) + if (keytoEnum.containsKey(id)) { + log.warn('Duplicate Enum ID [{}] detected for Enum [{}]!', id, enumClass.name) + } + keytoEnum.put(id, value) + } + + this.enumToKey = Collections.unmodifiableMap(enumToKey) + this.keytoEnum = Collections.unmodifiableMap(keytoEnum) + } + + private static E[] getEnumValues(Class enumClass) { + enumClass.enumConstants + } + + private static Method getIdAccessor(Class enumClass) { + def idMethod = enumClass.methods.find { it.name == ENUM_ID_ACCESSOR } + if (!idMethod) { + idMethod = enumClass.getMethod('ordinal') + } + return idMethod + } + + Object getEnumValue(int id) { + keytoEnum.get(id) + } + + int getKey(Object enumValue) { + enumToKey.get(enumValue) as Integer + } +} diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy new file mode 100644 index 0000000..1dd55ae --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreHelper.groovy @@ -0,0 +1,39 @@ +package gpc.pgext.hibernate.usertype + +import groovy.transform.CompileStatic + +/** + * Converts Maps to String and vice versa according to hstore syntax. + */ +@CompileStatic +class HstoreHelper { + + private static final String K_V_SEPARATOR = '=>' + + private static String escapeQuotes(Object text) { + text.toString().replaceAll('"', '\'') + } + + static String toString(Map m) { + if (!m) return '' + m.collect { k, v -> + "\"${escapeQuotes(k)}\"$K_V_SEPARATOR\"${escapeQuotes(v)}\"" + }.join(', ') + } + + static String asStatement(Map m) { + if (!m) return '' + def token = "\"?\"$K_V_SEPARATOR\"?\"" + Collections.nCopies(m.size(), token).join(', ') + } + + static List asListKeyValue(Map m) { + m ? (m.collectMany { k, v -> [k, v] } as List) : [] as List + } + + static Map toMap(String s) { + !s ? + new HashMap() : + new HstoreParser(s).asMap() + } +} diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreMapType.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy similarity index 77% rename from src/main/groovy/net/kaleidos/hibernate/usertype/HstoreMapType.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy index e590786..7466fc6 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreMapType.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreMapType.groovy @@ -1,22 +1,23 @@ -package net.kaleidos.hibernate.usertype - -import groovy.transform.CompileStatic -import org.hibernate.HibernateException -import org.hibernate.engine.spi.SharedSessionContractImplementor -import org.hibernate.usertype.UserType +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.engine.spi.SharedSessionContractImplementor +import org.hibernate.usertype.UserType + @CompileStatic class HstoreMapType implements UserType { static int SQLTYPE = 90011 int[] sqlTypes() { - return SQLTYPE as int[] + SQLTYPE as int[] } Class returnedClass() { @@ -27,11 +28,7 @@ class HstoreMapType implements UserType { if (x == null) { return y == null } - - Map m1 = x as Map - Map m2 = y as Map - - return m1.equals(m2) + (x as Map) == (y as Map) } int hashCode(Object x) throws HibernateException { @@ -39,15 +36,17 @@ class HstoreMapType implements UserType { } Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { - String col = names[0] - String val = rs.getString(col) - - return HstoreHelper.toMap(val) + def col = names[0] + def val = rs.getString(col) + HstoreHelper.toMap(val) } - void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { - String s = HstoreHelper.toString(value as Map) - st.setObject(index, s, Types.OTHER) + void nullSafeSet(PreparedStatement ps, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { + ps.setObject( + index, + HstoreHelper.toString(value as Map), + Types.OTHER + ) } Object deepCopy(Object value) throws HibernateException { diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParseException.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy similarity index 73% rename from src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParseException.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy index a5f537c..35d79de 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParseException.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParseException.groovy @@ -1,4 +1,4 @@ -package net.kaleidos.hibernate.usertype +package gpc.pgext.hibernate.usertype import groovy.transform.CompileStatic @@ -8,6 +8,6 @@ class HstoreParseException extends RuntimeException { private static final long serialVersionUID = 3418828452515857160L HstoreParseException(String error, int position) { - super("Error @${position} : ${error}") + super("Error @$position : $error") } } diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParser.java b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java similarity index 97% rename from src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParser.java rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java index 8d62d63..0eb1ad7 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreParser.java +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/HstoreParser.java @@ -1,19 +1,22 @@ -package net.kaleidos.hibernate.usertype; - -import org.postgresql.util.PGobject; -import org.springframework.util.Assert; +package gpc.pgext.hibernate.usertype; +import java.io.Serial; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import org.postgresql.util.PGobject; + +import org.springframework.util.Assert; + /** * This class is an extended version of HStore.java from https://code.google.com/p/pg-spring-type-mapper/ */ public class HstoreParser extends PGobject implements Iterable> { + @Serial private static final long serialVersionUID = -2491617655490561600L; private int length; @@ -44,7 +47,7 @@ public void setValue(String rawValue) { } public Map asMap() { - HashMap r = new HashMap(); + HashMap r = new HashMap<>(); try { for (final HStoreIterator iterator = new HStoreIterator(); iterator.hasNext(); ) { final HStoreEntry entry = iterator.rawNext(); @@ -63,7 +66,8 @@ private String replaceEscapePlaceholders(String text) { } private static class HStoreEntry implements Entry { - private String key; + + private final String key; private String value; HStoreEntry(String key, String value) { @@ -136,7 +140,7 @@ public Entry next() throws NoSuchElementException, IllegalStateE /** * Advance in parsing the rawValue string and assign the nextValue - * It creates a new nextElement or assigns null to it, if there are no more elements + * It creates a new nextElement or assigns null to it if there are no more elements * * @throws HstoreParseException */ diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/JsonMapType.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy similarity index 93% rename from src/main/groovy/net/kaleidos/hibernate/usertype/JsonMapType.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy index 9be558d..1dea5b5 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/JsonMapType.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonMapType.groovy @@ -1,20 +1,20 @@ -package net.kaleidos.hibernate.usertype +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 groovy.transform.CompileStatic -import org.apache.commons.lang.ObjectUtils import org.hibernate.HibernateException import org.hibernate.engine.spi.SharedSessionContractImplementor import org.hibernate.usertype.UserType import org.postgresql.util.PGobject -import java.lang.reflect.Type -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.SQLException -import java.sql.Types - @CompileStatic class JsonMapType implements UserType { @@ -35,7 +35,7 @@ class JsonMapType implements UserType { @Override boolean equals(Object x, Object y) throws HibernateException { - ObjectUtils.equals(x, y) + Objects.equals(x, y) } @Override @@ -47,7 +47,6 @@ class JsonMapType implements UserType { Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { PGobject o = rs.getObject(names[0]) as PGobject String jsonString = o?.value - gson.fromJson(jsonString, userType) } @@ -65,12 +64,10 @@ class JsonMapType implements UserType { if (!value && value != null && value instanceof Map) { return new HashMap() } - if (!value) { return null } - - return new HashMap(value as Map) + new HashMap(value as Map) } @Override diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/JsonbMapType.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy similarity index 83% rename from src/main/groovy/net/kaleidos/hibernate/usertype/JsonbMapType.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy index c10a3f6..6fce0f2 100644 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/JsonbMapType.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/usertype/JsonbMapType.groovy @@ -1,4 +1,4 @@ -package net.kaleidos.hibernate.usertype +package gpc.pgext.hibernate.usertype import groovy.transform.CompileStatic diff --git a/plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy new file mode 100644 index 0000000..5afd6a7 --- /dev/null +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/utils/CriteriaUtils.groovy @@ -0,0 +1,77 @@ +package gpc.pgext.hibernate.utils + +import java.lang.reflect.Method + +import groovy.transform.CompileStatic + +import org.hibernate.criterion.Criterion + +import grails.orm.HibernateCriteriaBuilder +import org.grails.orm.hibernate.query.AbstractHibernateCriteriaBuilder + +@CompileStatic +class CriteriaUtils { + + static void validateExpression(HibernateCriteriaBuilder self, String methodName, String propertyName, Object propertyValue) { + if (!validateSimpleExpression(self)) { + throwRuntimeException( + self, + "Call to [$methodName] with propertyName [$propertyName] and value [$propertyValue] is not allowed here." + ) + } + } + + static boolean validateSimpleExpression(HibernateCriteriaBuilder target) { + makeMethodAccessible(AbstractHibernateCriteriaBuilder, 'validateSimpleExpression').invoke(target, new Class[]{}) as boolean + } + + static void throwRuntimeException(HibernateCriteriaBuilder target, String message) { + makeMethodAccessible(AbstractHibernateCriteriaBuilder, 'throwRuntimeException', RuntimeException).invoke( + target, + new IllegalArgumentException(message) + ) + } + + static Method makeMethodAccessible(Class clazz, String methodName, Class... parameterTypes) { + clazz.getDeclaredMethod(methodName, parameterTypes).tap { + accessible = true + } + } + + static Criterion addToCriteria(HibernateCriteriaBuilder self, String methodName, Class criterionClass, String propertyName, Object propertyValue, String operator) { + validateExpression(self, methodName, propertyName, propertyValue) + addToCriteria( + self, + (criterionClass as Class).getDeclaredConstructor(new Class[] {String, Object, String}).newInstance(propertyName, propertyValue, operator) + ) + } + + static Criterion addToCriteria(HibernateCriteriaBuilder self, String methodName, Class criterionClass, String propertyName, Object propertyValue) { + validateExpression(self, methodName, propertyName, propertyValue) + addToCriteria( + self, + (criterionClass as Class).getDeclaredConstructor(new Class[] {String, Object}).newInstance(propertyName, propertyValue) + ) + } + + static Criterion addToCriteria(HibernateCriteriaBuilder target, Criterion criterion) { + makeMethodAccessible(AbstractHibernateCriteriaBuilder, 'addToCriteria', Criterion).invoke( + target, + criterion + ) as Criterion + } + + static String calculatePropertyName(HibernateCriteriaBuilder target, String propertyName) { + makeMethodAccessible(AbstractHibernateCriteriaBuilder, 'calculatePropertyName', String).invoke( + target, + propertyName + ) as String + } + + static Object calculatePropertyValue(HibernateCriteriaBuilder target, Object propertyValue) { + makeMethodAccessible(AbstractHibernateCriteriaBuilder, 'calculatePropertyValue', Object).invoke( + target, + propertyValue + ) + } +} diff --git a/src/main/groovy/net/kaleidos/hibernate/utils/PgArrayUtils.groovy b/plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy similarity index 50% rename from src/main/groovy/net/kaleidos/hibernate/utils/PgArrayUtils.groovy rename to plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy index c965dc6..ccd9f5c 100644 --- a/src/main/groovy/net/kaleidos/hibernate/utils/PgArrayUtils.groovy +++ b/plugin/src/main/groovy/gpc/pgext/hibernate/utils/PgArrayUtils.groovy @@ -1,9 +1,10 @@ -package net.kaleidos.hibernate.utils +package gpc.pgext.hibernate.utils + +import java.lang.reflect.Array import groovy.transform.CompileStatic -import org.hibernate.HibernateException -import java.lang.reflect.Array +import org.hibernate.HibernateException /** * Utils for the different criteria queries. @@ -18,7 +19,7 @@ class PgArrayUtils { (Float) : 'float4', (Double) : 'float8', (UUID) : 'uuid', - ] + ] as Map, String> /** * Returns a new array wrapping the parameter value. The type of the array @@ -29,47 +30,26 @@ class PgArrayUtils { * @param mapFunction If non-null, it will transform each object in the array to a given object. * @return an array wrapping the parameter value */ - static Object[] getValueAsArrayOfType(Object targetValue, Class expectedType, MapFunction mapFunction) { - Object[] arrValue + static Object[] getValueAsArrayOfType(Object targetValue, Class expectedType, MapFunction mapFunction) { + if (targetValue instanceof Object[]) return (Object[]) targetValue - if (targetValue instanceof List) { - List valueAsList = targetValue as List - arrValue = Array.newInstance(expectedType, valueAsList.size()) as Object[] - - int count = valueAsList.size() - for (int i = 0; i < count; i++) { - Object object = valueAsList.get(i) - if (expectedType.isInstance(object)) { - arrValue[i] = expectedType.cast(object) - } else if (mapFunction) { - arrValue[i] = expectedType.cast(mapFunction.map(object)) - } else { - throw new HibernateException("criteria doesn't support values of type: " + - targetValue.getClass().getName() + ". Try: " + expectedType + " or List<" + expectedType + "> instead") - } - } - } else if (expectedType.isInstance(targetValue) || mapFunction) { - arrValue = Array.newInstance(expectedType, 1) as Object[] - - if (mapFunction) { - arrValue[0] = expectedType.cast(mapFunction.map(targetValue)) - } else { - arrValue[0] = expectedType.cast(targetValue) - } - } else if (targetValue instanceof Object[]) { - arrValue = targetValue as Object[] - } else { - throw new HibernateException("criteria doesn't support values of type: ${targetValue.class.name}." + - "Try: ${expectedType} or List<${expectedType}> instead") + def items = (targetValue instanceof Collection) ? (targetValue as List) : [targetValue] + def converted = items.collect { o -> + if (expectedType.isInstance(o)) return o + 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") } - return arrValue + + def arr = Array.newInstance(expectedType, converted.size()) + converted.eachWithIndex { v, i -> Array.set(arr, i, expectedType.cast(v)) } + (Object[]) arr } /** * Overloaded version of getValueAsArrayOfType that doesn't use a mapFunction */ static Object[] getValueAsArrayOfType(Object targetValue, Class expectedType) { - return getValueAsArrayOfType(targetValue, expectedType, null) + getValueAsArrayOfType(targetValue, expectedType, null) } /** @@ -90,11 +70,9 @@ class PgArrayUtils { if (typeName != null) { return typeName } - if (clazz.isEnum()) { return 'int' } - - throw new RuntimeException("Type class not valid: ${clazz}") + throw new RuntimeException("Type class not valid: $clazz") } } diff --git a/plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule b/plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 0000000..8e42a21 --- /dev/null +++ b/plugin/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,6 @@ +moduleName=grails-postgresql-extensions +moduleVersion=1.0 +extensionClasses=\ + gpc.pgext.hibernate.postgresql.criteria.ArrayCriterias,\ + gpc.pgext.hibernate.postgresql.criteria.HstoreCriterias,\ + gpc.pgext.hibernate.postgresql.criteria.JsonCriterias \ No newline at end of file diff --git a/src/test/groovy/net/kaleidos/hibernate/usertype/HstoreHelperSpec.groovy b/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy similarity index 69% rename from src/test/groovy/net/kaleidos/hibernate/usertype/HstoreHelperSpec.groovy rename to plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy index ce54b4a..1750664 100644 --- a/src/test/groovy/net/kaleidos/hibernate/usertype/HstoreHelperSpec.groovy +++ b/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreHelperSpec.groovy @@ -1,4 +1,4 @@ -package net.kaleidos.hibernate.usertype +package gpc.pgext.hibernate.usertype import spock.lang.Issue import spock.lang.Specification @@ -12,7 +12,7 @@ class HstoreHelperSpec extends Specification { def m = value expect: - HstoreHelper.toString(m) == "" + HstoreHelper.toString(m) == '' where: value << [[:], null] @@ -21,7 +21,7 @@ class HstoreHelperSpec extends Specification { void 'non empty map to string'() { setup: def m = [:] - m.foo = "bar" + m.foo = 'bar' expect: HstoreHelper.toString(m) == '"foo"=>"bar"' @@ -30,7 +30,7 @@ class HstoreHelperSpec extends Specification { void 'transform map with double quotes'() { setup: def m = [:] - m['Test "thing"'] = "bar" + m['Test "thing"'] = 'bar' expect: HstoreHelper.toString(m) == '"Test \'thing\'"=>"bar"' @@ -39,8 +39,8 @@ class HstoreHelperSpec extends Specification { void 'map with two values to string'() { setup: def m = [:] - m.foo = "bar" - m.xxx = "Groovy Rocks!" + m.foo = 'bar' + m.xxx = 'Groovy Rocks!' expect: HstoreHelper.toString(m) == '"foo"=>"bar", "xxx"=>"Groovy Rocks!"' @@ -53,7 +53,7 @@ class HstoreHelperSpec extends Specification { m.prop = value expect: - HstoreHelper.toString(m) == "\"prop\"=>\"${value}\"" + HstoreHelper.toString(m) == "\"prop\"=>\"$value\"" where: value << [123, true, null, 999L, new Date(), 87987.8976] @@ -62,7 +62,7 @@ class HstoreHelperSpec extends Specification { void 'map with key and value that contains a comma'() { setup: def m = [:] - m["foo,bar"] = "baz,qux" + m['foo,bar'] = 'baz,qux' expect: HstoreHelper.toString(m) == '"foo,bar"=>"baz,qux"' @@ -71,7 +71,7 @@ class HstoreHelperSpec extends Specification { void 'map with key and value that contains a comma and space'() { setup: def m = [:] - m["foo, bar"] = "baz, qux" + m['foo, bar'] = 'baz, qux' expect: HstoreHelper.toString(m) == '"foo, bar"=>"baz, qux"' @@ -86,8 +86,8 @@ class HstoreHelperSpec extends Specification { result == expected where: - map << [null, [:], ["a": "b"], ["a": "b", "c": "d"], ["a": "b", "c": "d", "e": "f"], ["foo,bar": "baz,qux"]] - expected << ["", "", '"?"=>"?"', '"?"=>"?", "?"=>"?"', '"?"=>"?", "?"=>"?", "?"=>"?"', '"?"=>"?"'] + map << [null, [:], [a: 'b'], [a: 'b', c: 'd'], [a: 'b', c: 'd', e: 'f'], ['foo,bar': 'baz,qux']] + expected << ['', '', '"?"=>"?"', '"?"=>"?", "?"=>"?"', '"?"=>"?", "?"=>"?", "?"=>"?"', '"?"=>"?"'] } @Unroll @@ -99,19 +99,19 @@ class HstoreHelperSpec extends Specification { result == expected where: - map << [null, [:], ["a": "b"], ["a": "b", "c": "d"], ["a": "b", "c": "d", "e": "f"], ["foo,bar": "baz,qux"]] - expected << [[], [], ["a", "b"], ["a", "b", "c", "d"], ["a", "b", "c", "d", "e", "f"], ["foo,bar", "baz,qux"]] + map << [null, [:], [a: 'b'], [a: 'b', c: 'd'], [a: 'b', c: 'd', e: 'f'], ['foo,bar': 'baz,qux']] + expected << [[], [], ['a', 'b'], ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd', 'e', 'f'], ['foo,bar', 'baz,qux']] } - @Issue("https://github.com/kaleidos/grails-postgresql-extensions/issues/25") @Unroll + @Issue('https://github.com/gpc/grails-postgresql-extensions/issues/25') void 'map with key of type different to String. key: #key'() { setup: def m = [:] - m[key] = "value" + m[key] = 'value' expect: - HstoreHelper.toString(m) == "\"${key}\"=>\"value\"" + HstoreHelper.toString(m) == "\"$key\"=>\"value\"" where: key << [123, true, 999L, new Date(), 87987.8976] diff --git a/src/test/groovy/net/kaleidos/hibernate/usertype/HstoreParserSpec.groovy b/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy similarity index 86% rename from src/test/groovy/net/kaleidos/hibernate/usertype/HstoreParserSpec.groovy rename to plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy index 6705cf8..f2fd6f9 100644 --- a/src/test/groovy/net/kaleidos/hibernate/usertype/HstoreParserSpec.groovy +++ b/plugin/src/test/groovy/gpc/pgext/hibernate/usertype/HstoreParserSpec.groovy @@ -1,13 +1,14 @@ -package net.kaleidos.hibernate.usertype +package gpc.pgext.hibernate.usertype import spock.lang.Specification import spock.lang.Unroll class HstoreParserSpec extends Specification { + @Unroll - void "AsMap with value populated by constructor"() { + void 'AsMap with value populated by constructor'() { expect: - HstoreParser parser = new HstoreParser(input) + def parser = new HstoreParser(input) def map = parser.asMap() map[expected_key] == expected_value @@ -19,10 +20,12 @@ class HstoreParserSpec extends Specification { } @Unroll - void "AsMap with value populated by setValue"() { + void 'AsMap with value populated by setValue'() { + given: + def parser = new HstoreParser('') + parser.value = input + expect: - HstoreParser parser = new HstoreParser('') - parser.setValue(input) def map = parser.asMap() map[expected_key] == expected_value diff --git a/settings.gradle b/settings.gradle index 6129273..8e1c6b3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,40 @@ -rootProject.name = 'postgresql-extensions' +plugins { + id 'com.gradle.develocity' version '4.1.1' + id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.3' +} + +def isCI = System.getenv().containsKey('CI') +def isLocal = !isCI +def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null +if(isReproducibleBuild) { + gradle.settingsEvaluated { + logger.warn('*************** Remote Build Cache Disabled due to Reproducible Build ********************') + logger.warn("Build date will be set to (SOURCE_DATE_EPOCH=${System.getenv("SOURCE_DATE_EPOCH")})") + } +} + +develocity { + server = 'https://ge.grails.org' + buildScan { + tag('gpc') + tag('grails-postgresql-extensions') + publishing.onlyIf { it.authenticated } + uploadInBackground = isLocal + } +} + +buildCache { + local { enabled = (isLocal && !isReproducibleBuild) || (isCI && isReproducibleBuild) } + remote(develocity.buildCache) { + push = isCI + enabled = !isReproducibleBuild + } +} + +rootProject.name = 'postgresql-extensions.ROOT' + +include('plugin') +findProject(':plugin').name = 'grails-postgresql-extensions' + +include('test-app1') +project(':test-app1').projectDir = new File(settingsDir, 'test-apps/app1') diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy b/src/integration-test/groovy/net/kaleidos/hibernate/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy deleted file mode 100644 index d0f6e4d..0000000 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package net.kaleidos.hibernate.array - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService - -@Integration -@Rollback -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", "Starwars"]).save(flush: true, failOnError: true) - new Like(favoriteMovies: ["Starwars"]).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 - "%tarwar%" | 2 - "%ider%" | 1 - "%Suspects%" | 0 - "" | 0 - "%" | 5 - } - -} diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsIntegrationSpec.groovy b/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsIntegrationSpec.groovy deleted file mode 100644 index a2da128..0000000 --- a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsIntegrationSpec.groovy +++ /dev/null @@ -1,112 +0,0 @@ -package net.kaleidos.hibernate.hstore - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.hstore.PgHstoreTestSearchService -import test.hstore.TestHstoreMap - -@Integration -@Rollback -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" } != null - result.find { it.name == "test2" } == null - result.find { it.name == "test3" } == null - result.find { it.name == "test4" } != null - - 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" } == null - result.find { it.name == "test2" } == null - result.find { it.name == "test3" } == null - result.find { it.name == "test4" } != null - - 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/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsKeyIntegrationSpec.groovy b/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsKeyIntegrationSpec.groovy deleted file mode 100644 index a5610ee..0000000 --- a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreContainsKeyIntegrationSpec.groovy +++ /dev/null @@ -1,51 +0,0 @@ -package net.kaleidos.hibernate.hstore - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.hstore.PgHstoreTestSearchService -import test.hstore.TestHstoreMap - -@Integration -@Rollback -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" } != null - result.find { it.name == "test2" } != null - result.find { it.name == "test3" } == null - result.find { it.name == "test4" } != null - } - - 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/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy b/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy deleted file mode 100644 index 7ce4f8d..0000000 --- a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy +++ /dev/null @@ -1,51 +0,0 @@ -package net.kaleidos.hibernate.hstore - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.hstore.PgHstoreTestSearchService -import test.hstore.TestHstoreMap - -@Integration -@Rollback -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" } != null - result.find { it.name == "test2" } == null - result.find { it.name == "test3" } != null - result.find { it.name == "test4" } != null - } - - 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/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreIsContainedIntegrationSpec.groovy b/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreIsContainedIntegrationSpec.groovy deleted file mode 100644 index c793b8d..0000000 --- a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PgHstoreIsContainedIntegrationSpec.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package net.kaleidos.hibernate.hstore - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.hstore.PgHstoreTestSearchService -import test.hstore.TestHstoreMap - -@Integration -@Rollback -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" } != null - result.find { it.name == "test2" } != null - result.find { it.name == "test3" } != null - result.find { it.name == "test4" } != null - - 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" } != null - result.find { it.name == "test2" } == null - result.find { it.name == "test3" } != null - result.find { it.name == "test4" } != null - - where: - map = ["a": "test", "b": "1", "c": "test"] - } -} diff --git a/src/main/groovy/grails/postgresql/extensions/GrailsPostgresqlExtensionsGrailsPlugin.groovy b/src/main/groovy/grails/postgresql/extensions/GrailsPostgresqlExtensionsGrailsPlugin.groovy deleted file mode 100644 index d5aa3f5..0000000 --- a/src/main/groovy/grails/postgresql/extensions/GrailsPostgresqlExtensionsGrailsPlugin.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package grails.postgresql.extensions - -import grails.compiler.GrailsCompileStatic -import grails.plugins.Plugin -import net.kaleidos.hibernate.postgresql.criteria.ArrayCriterias -import net.kaleidos.hibernate.postgresql.criteria.HstoreCriterias -import net.kaleidos.hibernate.postgresql.criteria.JsonCriterias - -@GrailsCompileStatic -class GrailsPostgresqlExtensionsGrailsPlugin extends Plugin { - - // the version or versions of Grails the plugin is designed for - def grailsVersion = "3.2.0 > *" - // resources that are excluded from plugin packaging - def pluginExcludes = [ - 'test/**' - ] - - def title = "Grails Postgresql Extensions Plugin" - def author = "Iván López" - def authorEmail = "lopez.ivan@gmail.com" - def description = '''\ -Provides Hibernate user types to support for Postgresql Native Types like Array, HStore, JSON, JSONB,... as well as new criterias to query this native types -''' - - def documentation = "https://github.com/kaleidos/grails-postgresql-extensions/blob/master/README.md" - def license = "APACHE" - def organization = [name: "Kaleidos", url: "http://kaleidos.net"] - def developers = [[name: "Alonso Torres", email: "alonso.javier.torres@gmail.com"]] - def issueManagement = [system: "GITHUB", url: "https://github.com/kaleidos/grails-postgresql-extensions/issues"] - def scm = [url: "https://github.com/kaleidos/grails-postgresql-extensions"] - - void doWithDynamicMethods() { - new ArrayCriterias() - new HstoreCriterias() - new JsonCriterias() - } - -} diff --git a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy b/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy deleted file mode 100644 index 80411bd..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/criterion/hstore/PgHstoreILikeValueFunction.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package net.kaleidos.hibernate.criterion.hstore - -import groovy.transform.CompileStatic -import org.hibernate.Criteria -import org.hibernate.HibernateException -import org.hibernate.annotations.common.util.StringHelper -import org.hibernate.criterion.CriteriaQuery - -@CompileStatic -class PgHstoreILikeValueFunction extends PgHstoreValueFunction { - - protected PgHstoreILikeValueFunction(String propertyName, Object value) { - super(propertyName, value, "") - } - - @Override - String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { - String[] columns = StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), "") - for (int i = 0; i < columns.length; i++) { - columns[i] = "text(avals(${columns[i]})) ilike ?" - } - return StringHelper.join(" and ", columns) - } -} diff --git a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/ArrayCriterias.groovy b/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/ArrayCriterias.groovy deleted file mode 100644 index ec15e9e..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/ArrayCriterias.groovy +++ /dev/null @@ -1,204 +0,0 @@ -package net.kaleidos.hibernate.postgresql.criteria - -import grails.orm.HibernateCriteriaBuilder -import net.kaleidos.hibernate.criterion.array.PgArrayExpression -import net.kaleidos.hibernate.criterion.array.PgArrayILikeFunction -import net.kaleidos.hibernate.criterion.array.PgEmptinessExpression - -class ArrayCriterias { - - ArrayCriterias() { - addContainsOperator() - addIsContainedByOperator() - addOverlapsOperator() - addIsEmptyOperator() - addIsNotEmptyOperator() - addIsEmptyOrContainsOperator() - addEqualsOperator() - addNotEqualsOperator() - addILikeOperator() - } - - private void addContainsOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayContains = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayContains] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "@>")) - } - } - - private void addIsContainedByOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayIsContainedBy = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayIsContainedBy] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "<@")) - } - } - - private void addOverlapsOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayOverlaps = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgOverlap] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "&&")) - } - } - - private void addIsEmptyOperator() { - /** - * Creates an "is empty array" Criterion based on the specified property name - * @param propertyName The property name - * @return A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayIsEmpty = { String propertyName -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayIsEmpty] with propertyName [" + - propertyName + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - - return addToCriteria(new PgEmptinessExpression(propertyName, "=")) - } - } - - private void addIsNotEmptyOperator() { - /** - * Creates an "is empty array" Criterion based on the specified property name - * @param propertyName The property name - * @return A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayIsNotEmpty = { String propertyName -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayIsNotEmpty] with propertyName [" + - propertyName + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - - return addToCriteria(new PgEmptinessExpression(propertyName, "<>")) - } - } - - private void addIsEmptyOrContainsOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayIsEmptyOrContains = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayIsEmptyOrContains] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - if (propertyValue) { - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "@>")) - } else { - return addToCriteria(new PgEmptinessExpression(propertyName, "=")) - } - } - } - - private void addEqualsOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayEquals = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayEquals] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "=")) - } - } - - private void addNotEqualsOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayNotEquals = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayNotEquals] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayExpression(propertyName, propertyValue, "<>")) - } - } - - private void addILikeOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgArrayILike = { String propertyName, String propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgArrayILike] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgArrayILikeFunction(propertyName, propertyValue)) - } - } -} diff --git a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/HstoreCriterias.groovy b/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/HstoreCriterias.groovy deleted file mode 100644 index 951015a..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/HstoreCriterias.groovy +++ /dev/null @@ -1,63 +0,0 @@ -package net.kaleidos.hibernate.postgresql.criteria - -import grails.orm.HibernateCriteriaBuilder -import net.kaleidos.hibernate.criterion.hstore.PgHstoreILikeValueFunction -import net.kaleidos.hibernate.criterion.hstore.PgHstoreOperatorExpression -import net.kaleidos.hibernate.criterion.hstore.PgHstoreValueFunction - -class HstoreCriterias { - HstoreCriterias() { - addPgHstoreContainsKey() - addPgHstoreContains() - addPgHstoreIsContained() - addPgHstoreILikeValueFunction() - } - - void addPgHstoreContainsKey() { - HibernateCriteriaBuilder.metaClass.pgHstoreContainsKey = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgHstoreContains] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgHstoreValueFunction(propertyName, propertyValue, "exist")) - } - } - - void addPgHstoreContains() { - HibernateCriteriaBuilder.metaClass.pgHstoreContains = { String propertyName, Map values -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgHstoreContains] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - propertyName = calculatePropertyName(propertyName) - return addToCriteria(new PgHstoreOperatorExpression(propertyName, values, "@>")) - } - } - - void addPgHstoreIsContained() { - HibernateCriteriaBuilder.metaClass.pgHstoreIsContained = { String propertyName, Map values -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgHstoreIsContained] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - propertyName = calculatePropertyName(propertyName) - return addToCriteria(new PgHstoreOperatorExpression(propertyName, values, "<@")) - } - } - - void addPgHstoreILikeValueFunction() { - HibernateCriteriaBuilder.metaClass.pgHstoreILikeValue = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgHstoreILikeValue] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgHstoreILikeValueFunction(propertyName, propertyValue)) - } - } -} diff --git a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy b/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy deleted file mode 100644 index 896d780..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy +++ /dev/null @@ -1,99 +0,0 @@ -package net.kaleidos.hibernate.postgresql.criteria - -import grails.orm.HibernateCriteriaBuilder -import net.kaleidos.hibernate.criterion.json.PgJsonExpression -import net.kaleidos.hibernate.criterion.json.PgJsonbOperator - -class JsonCriterias { - - JsonCriterias() { - addHasFieldValueOperator() - addPgJsonbContainsOperator() - addPgJsonbIsContainedOperator() - addGenericFieldValueOperator() - } - - private void addHasFieldValueOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgJsonHasFieldValue = { String propertyName, String jsonAttribute, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgJsonHasFieldValue] with propertyName [" + - propertyName + "], jsonAttribute [" + jsonAttribute + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgJsonExpression(propertyName, '->>', jsonAttribute, "=", propertyValue as String)) - } - } - - private void addPgJsonbContainsOperator() { - /** - * Creates a "json constains another json" Criterion based on the specified property name and value - * @param propertyName The property name (jsonb field) - * @param propertyValue The property value - * @return A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgJsonbContains = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgJsonContains] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgJsonbOperator(propertyName, propertyValue, "@>")) - } - } - - private void addPgJsonbIsContainedOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgJsonbIsContained = { String propertyName, propertyValue -> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgJsonIsContained] with propertyName [" + - propertyName + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgJsonbOperator(propertyName, propertyValue, "<@")) - } - } - - private void addGenericFieldValueOperator() { - /** - * 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 A Criterion instance - */ - HibernateCriteriaBuilder.metaClass.pgJson = { String propertyName, String jsonOp, String jsonAttribute, String sqlOp, propertyValue-> - if (!validateSimpleExpression()) { - throwRuntimeException(new IllegalArgumentException("Call to [pgJson] with propertyName [" + - propertyName + "], json operator [" + jsonOp + "], jsonAttribute [" + jsonAttribute + "], sql operator [" + sqlOp + "] and value [" + propertyValue + "] not allowed here.")) - } - - propertyName = calculatePropertyName(propertyName) - propertyValue = calculatePropertyValue(propertyValue) - - return addToCriteria(new PgJsonExpression(propertyName, jsonOp, jsonAttribute, sqlOp, propertyValue as String)) - } - } -} diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/BidiEnumMap.groovy b/src/main/groovy/net/kaleidos/hibernate/usertype/BidiEnumMap.groovy deleted file mode 100644 index 4f25090..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/BidiEnumMap.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package net.kaleidos.hibernate.usertype - -import groovy.transform.CompileDynamic -import groovy.transform.CompileStatic -import groovy.util.logging.Slf4j - -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method - -@Slf4j -@CompileStatic -public class BidiEnumMap implements Serializable { - static final String ENUM_ID_ACCESSOR = 'getId' - - private static final long serialVersionUID = 3325751131102095834L - private final Map enumToKey - private final Map keytoEnum - - BidiEnumMap(Class enumClass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - log.debug 'Building Bidirectional Enum Map...' - - EnumMap enumToKey = new EnumMap(enumClass) - HashMap keytoEnum = new HashMap() - - Method idAccessor = getIdAccessor(enumClass) - Object[] values = getEnumValues(enumClass) - - for (Object value : values) { - Object id = idAccessor.invoke(value) - enumToKey.put(value as Enum, id) - if (keytoEnum.containsKey(id)) { - log.warn(String.format("Duplicate Enum ID '%s' detected for Enum %s!", id, enumClass.name)) - } - keytoEnum.put(id, value) - } - - this.enumToKey = Collections.unmodifiableMap(enumToKey) - this.keytoEnum = Collections.unmodifiableMap(keytoEnum) - } - - @CompileDynamic - private Object[] getEnumValues(Class enumClass) { - enumClass.values() - } - - private Method getIdAccessor(Class enumClass) { - def idMethod = enumClass.methods.find { it.name == 'getId' } - if (!idMethod) { - idMethod = enumClass.getMethod('ordinal') - } - return idMethod - } - - Object getEnumValue(int id) { - keytoEnum.get(id) - } - - int getKey(Object enumValue) { - enumToKey.get(enumValue) as Integer - } -} diff --git a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreHelper.groovy b/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreHelper.groovy deleted file mode 100644 index a80b5a0..0000000 --- a/src/main/groovy/net/kaleidos/hibernate/usertype/HstoreHelper.groovy +++ /dev/null @@ -1,73 +0,0 @@ -package net.kaleidos.hibernate.usertype - -import groovy.transform.CompileStatic - -/** - * Converts Maps to String and vice versa according to hstore syntax. - */ -@CompileStatic -class HstoreHelper { - - private static final String K_V_SEPARATOR = "=>" - - private static String escapeQuotes(String text) { - return text.replaceAll("\"", "'") - } - - static String toString(Map m) { - if (!m) { - return "" - } - - StringBuilder sb = new StringBuilder() - int n = m.size() - for (Object key : m.keySet()) { - String stringKey = String.valueOf(key) - - sb.append('"').append(escapeQuotes(stringKey)).append('"') - sb.append(K_V_SEPARATOR) - sb.append('"').append(escapeQuotes(String.valueOf(m.get(key)))).append('"') - - if (n > 1) { - sb.append(", ") - n-- - } - } - return sb.toString() - } - - static String asStatement(Map m) { - if (!m) { - return "" - } - StringBuilder sb = new StringBuilder() - int n = m.size() - for (String key : m.keySet()) { - sb.append('"?"' + K_V_SEPARATOR + '"?"') - if (n > 1) { - sb.append(", ") - n-- - } - } - return sb.toString() - } - - static List asListKeyValue(Map m) { - List result = new LinkedList() - if (m) { - for (Map.Entry entry : m.entrySet()) { - result << entry.key - result << entry.value - } - } - return result - } - - static Map toMap(String s) { - if (!s) { - return new HashMap() - } - HstoreParser parser = new HstoreParser(s) - return parser.asMap() - } -} diff --git a/test-apps/app1/build.gradle b/test-apps/app1/build.gradle new file mode 100644 index 0000000..02cc950 --- /dev/null +++ b/test-apps/app1/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') + implementation 'org.apache.grails:grails-data-hibernate5' + 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/app1/grails-app/conf/application.yml b/test-apps/app1/grails-app/conf/application.yml new file mode 100644 index 0000000..466169f --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/conf/logback-spring.xml b/test-apps/app1/grails-app/conf/logback-spring.xml new file mode 100644 index 0000000..e3b8d89 --- /dev/null +++ b/test-apps/app1/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/grails-app/conf/spring/resources.groovy b/test-apps/app1/grails-app/conf/spring/resources.groovy similarity index 100% rename from grails-app/conf/spring/resources.groovy rename to test-apps/app1/grails-app/conf/spring/resources.groovy diff --git a/test-apps/app1/grails-app/domain/app/array/TestDouble.groovy b/test-apps/app1/grails-app/domain/app/array/TestDouble.groovy new file mode 100644 index 0000000..2f06ed8 --- /dev/null +++ b/test-apps/app1/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/grails-app/domain/test/array/TestEnum.groovy b/test-apps/app1/grails-app/domain/app/array/TestEnum.groovy similarity index 64% rename from grails-app/domain/test/array/TestEnum.groovy rename to test-apps/app1/grails-app/domain/app/array/TestEnum.groovy index f67a120..8c9c259 100644 --- a/grails-app/domain/test/array/TestEnum.groovy +++ b/test-apps/app1/grails-app/domain/app/array/TestEnum.groovy @@ -1,9 +1,13 @@ -package test.array +package app.array import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType @ToString +@GrailsCompileStatic class TestEnum { static enum Day { @@ -23,11 +27,10 @@ class TestEnum { Day[] days static mapping = { - days type: ArrayType, params: [type: Day] + days(type: ArrayType, params: [type: Day]) } static constraints = { - days nullable: true + days(nullable: true) } - } diff --git a/test-apps/app1/grails-app/domain/app/array/TestFloat.groovy b/test-apps/app1/grails-app/domain/app/array/TestFloat.groovy new file mode 100644 index 0000000..2a6f488 --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/array/TestInteger.groovy b/test-apps/app1/grails-app/domain/app/array/TestInteger.groovy new file mode 100644 index 0000000..8b27811 --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/array/TestLong.groovy b/test-apps/app1/grails-app/domain/app/array/TestLong.groovy new file mode 100644 index 0000000..e8af71f --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/array/TestString.groovy b/test-apps/app1/grails-app/domain/app/array/TestString.groovy new file mode 100644 index 0000000..f4100bb --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/array/TestUuid.groovy b/test-apps/app1/grails-app/domain/app/array/TestUuid.groovy new file mode 100644 index 0000000..1d3974c --- /dev/null +++ b/test-apps/app1/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/grails-app/domain/test/criteria/array/Like.groovy b/test-apps/app1/grails-app/domain/app/criteria/array/Like.groovy similarity index 50% rename from grails-app/domain/test/criteria/array/Like.groovy rename to test-apps/app1/grails-app/domain/app/criteria/array/Like.groovy index 3d1b9c9..0dc7d9d 100644 --- a/grails-app/domain/test/criteria/array/Like.groovy +++ b/test-apps/app1/grails-app/domain/app/criteria/array/Like.groovy @@ -1,9 +1,13 @@ -package test.criteria.array +package app.criteria.array import groovy.transform.ToString -import net.kaleidos.hibernate.usertype.ArrayType + +import grails.compiler.GrailsCompileStatic + +import gpc.pgext.hibernate.usertype.ArrayType @ToString +@GrailsCompileStatic class Like { static belongsTo = User @@ -33,15 +37,14 @@ class Like { } 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] + 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/app1/grails-app/domain/app/criteria/array/User.groovy b/test-apps/app1/grails-app/domain/app/criteria/array/User.groovy new file mode 100644 index 0000000..f9fba91 --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/hstore/TestHstoreMap.groovy b/test-apps/app1/grails-app/domain/app/hstore/TestHstoreMap.groovy new file mode 100644 index 0000000..fd24582 --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/json/TestMapJson.groovy b/test-apps/app1/grails-app/domain/app/json/TestMapJson.groovy new file mode 100644 index 0000000..828cb7b --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/domain/app/json/TestMapJsonb.groovy b/test-apps/app1/grails-app/domain/app/json/TestMapJsonb.groovy new file mode 100644 index 0000000..5f40e4c --- /dev/null +++ b/test-apps/app1/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/grails-app/init/grails/postgresql/extensions/Application.groovy b/test-apps/app1/grails-app/init/app/Application.groovy similarity index 74% rename from grails-app/init/grails/postgresql/extensions/Application.groovy rename to test-apps/app1/grails-app/init/app/Application.groovy index a084cd7..0931f30 100644 --- a/grails-app/init/grails/postgresql/extensions/Application.groovy +++ b/test-apps/app1/grails-app/init/app/Application.groovy @@ -1,11 +1,10 @@ -package grails.postgresql.extensions +package app + +import groovy.transform.CompileStatic import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration -import grails.plugins.metadata.PluginSource -import groovy.transform.CompileStatic -@PluginSource @CompileStatic class Application extends GrailsAutoConfiguration { static void main(String[] args) { diff --git a/test-apps/app1/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy b/test-apps/app1/grails-app/services/app/criteria/array/PgArrayTestSearchService.groovy new file mode 100644 index 0000000..c0357ea --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy b/test-apps/app1/grails-app/services/app/criteria/hstore/PgHstoreTestSearchService.groovy new file mode 100644 index 0000000..b4961cb --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy b/test-apps/app1/grails-app/services/app/criteria/json/PgJsonTestSearchService.groovy new file mode 100644 index 0000000..e6ee0bd --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy b/test-apps/app1/grails-app/services/app/criteria/json/PgJsonbTestSearchService.groovy new file mode 100644 index 0000000..c323b57 --- /dev/null +++ b/test-apps/app1/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/app1/grails-app/services/app/order/PgOrderService.groovy b/test-apps/app1/grails-app/services/app/order/PgOrderService.groovy new file mode 100644 index 0000000..8b28213 --- /dev/null +++ b/test-apps/app1/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/src/integration-test/groovy/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy similarity index 71% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy index 65b1dd8..d6f5881 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgContainsCriteriaTestServiceIntegrationSpec.groovy @@ -1,20 +1,23 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import org.hibernate.HibernateException -import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -139,10 +142,10 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { @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", "Starwars"]).save(flush: true, failOnError: true) - new Like(favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"]).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: ['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) @@ -151,27 +154,27 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { result.size() == resultSize where: - movie | resultSize - "The Matrix" | 1 - "The Lord of the Rings" | 2 - "Blade Runner" | 2 - "Starwars" | 2 - "The Usual Suspects" | 0 - ["Starwars", "Romeo & Juliet"] | 1 - ["The Lord of the Rings"] | 2 - [] | 4 - ["Starwars", "Romeo & Juliet"] as String[] | 1 - ["The Lord of the Rings"] as String[] | 2 - [] as String[] | 4 + 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", "Starwars"])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(["Romeo & Juliet", "Casablanca", "Starwars"])).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: 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) @@ -180,18 +183,18 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { 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("Starwars") | 2 - UuidBuilder.createUUID("The Usual Suspects") | 0 - UuidBuilder.createUUIDs(["Starwars", "Romeo & Juliet"]) | 1 - UuidBuilder.createUUIDs(["The Lord of the Rings"]) | 2 - [] | 4 - UuidBuilder.createUUIDs(["Starwars", "Romeo & Juliet"]) as UUID[] | 1 - UuidBuilder.createUUIDs(["The Lord of the Rings"]) as UUID[] | 2 - [] as UUID[] | 4 + 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 @@ -228,41 +231,41 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"])).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) + 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) == true - result.contains(user3) == true + result.contains(user2) + result.contains(user3) where: - movie = "Starwars" + 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"])).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) + 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) == true - result.contains(user3) == true - result.contains(user4) == true + result.contains(user2) + result.contains(user3) + result.contains(user4) where: - movie = "Starwars" + movie = "Star Wars" number = 4 } @@ -274,7 +277,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1, "Test"], [1L], [1, 1L]] + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] } void 'search an invalid list inside the array of long'() { @@ -285,7 +288,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1L, "Test"], [1], [1L, 1]] + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] } void 'search an invalid list inside the array of float'() { @@ -296,7 +299,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1f, "Test"], [1], [1f, 1]] + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] } void 'search an invalid list inside the array of double'() { @@ -307,7 +310,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1d, "Test"], [1], [1d, 1]] + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] } void 'search an invalid list inside the array of string'() { @@ -318,7 +321,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", 1], [1L], ["Test", 1L]] + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] } void 'search an invalid list inside the array of UUID'() { @@ -329,7 +332,7 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] } void 'search an invalid list inside the array of enum'() { @@ -340,6 +343,6 @@ class PgContainsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] } } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy similarity index 72% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy index d95cbb0..b71b070 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgEqualsCriteriaTestServiceIntegrationSpec.groovy @@ -1,20 +1,23 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import org.hibernate.HibernateException -import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -130,10 +133,10 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { @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", "Starwars"]).save(flush: true, failOnError: true) - new Like(favoriteMovies: ["Starwars"]).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: ['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: @@ -144,24 +147,24 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { where: movie | resultSize - "Starwars" | 1 - ["Starwars"] | 1 - ["Starwars"] as String[] | 1 - "The Usual Suspects" | 0 - ["Spiderman", "Blade Runner", "Starwars"] | 1 + '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 + ['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", "Starwars"])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(["Starwars"])).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: 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: @@ -172,14 +175,14 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { where: movie | resultSize - UuidBuilder.createUUID("Starwars") | 1 - UuidBuilder.createUUIDs(["Starwars"]) | 1 - UuidBuilder.createUUIDs(["Starwars"]) as UUID[] | 1 - UuidBuilder.createUUID("The Usual Suspects") | 0 - UuidBuilder.createUUIDs(["Spiderman", "Blade Runner", "Starwars"]) | 1 + 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 + 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 } @@ -208,41 +211,41 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ["Spiderman", "Blade Runner", "Starwars"])).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) + 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) == true - result.contains(user3) == true + result.contains(user2) + result.contains(user3) where: - movie = ["Spiderman", "Blade Runner", "Starwars"] + 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ["Spiderman", "Blade Runner", "Starwars"])).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) + 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) == true - result.contains(user3) == true - result.contains(user4) == true + result.contains(user2) + result.contains(user3) + result.contains(user4) where: - movie = ["Spiderman", "Blade Runner", "Starwars"] + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] number = [9, 4] } @@ -254,7 +257,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1, "Test"], [1L], [1, 1L]] + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] } void 'search an invalid list inside the array of long'() { @@ -265,7 +268,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1L, "Test"], [1], [1L, 1]] + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] } void 'search an invalid list inside the array of float'() { @@ -276,7 +279,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1f, "Test"], [1], [1f, 1]] + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] } void 'search an invalid list inside the array of double'() { @@ -287,7 +290,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1d, "Test"], [1], [1d, 1]] + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] } void 'search an invalid list inside the array of string'() { @@ -298,7 +301,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", 1], [1L], ["Test", 1L]] + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] } void 'search an invalid list inside the array of UUID'() { @@ -309,7 +312,7 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] } void 'search an invalid list inside the array of enum'() { @@ -320,6 +323,6 @@ class PgEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] } } diff --git a/test-apps/app1/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgILikeCriteriaTestServiceIntegrationSpec.groovy new file mode 100644 index 0000000..1b19c85 --- /dev/null +++ b/test-apps/app1/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/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy similarity index 80% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy index 6564fad..8ce46c3 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgIsContainedByCriteriaTestServiceIntegrationSpec.groovy @@ -1,20 +1,23 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import org.hibernate.HibernateException -import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -138,10 +141,10 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { @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) + 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) @@ -151,24 +154,24 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { 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 + '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 + ['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) + 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) @@ -178,14 +181,14 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { 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 + 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 + UuidBuilder.createUUIDs(['A', 'Z']) as UUID[] | 1 + UuidBuilder.createUUIDs(['B', 'D', 'E', 'F', 'G']) as UUID[] | 2 [] as UUID[] | 0 } @@ -224,41 +227,41 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { 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) + 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) == true - result.contains(user3) == true + result.contains(user2) + result.contains(user3) where: - movies = ["A", "B", "C"] + 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) + 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) == true - result.contains(user3) == true - result.contains(user4) == true + result.contains(user2) + result.contains(user3) + result.contains(user4) where: - movies = ["A", "B", "C"] + movies = ['A', 'B', 'C'] numbers = [1, 2] } @@ -270,7 +273,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1, "Test"], [1L], [1, 1L]] + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] } void 'search a invalid list inside the array of long'() { @@ -281,7 +284,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1L, "Test"], [1], [1L, 1]] + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] } void 'search a invalid list inside the array of float'() { @@ -292,7 +295,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1f, "Test"], [1], [1f, 1]] + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] } void 'search a invalid list inside the array of double'() { @@ -303,7 +306,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1d, "Test"], [1], [1d, 1]] + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] } void 'search a invalid list inside the array of string'() { @@ -314,7 +317,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", 1], [1L], ["Test", 1L]] + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] } void 'search an invalid list inside the array of UUID'() { @@ -325,7 +328,7 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] } void 'search an invalid list juice inside the array of enum'() { @@ -336,7 +339,6 @@ class PgIsContainedByCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - juice << [["Test"], ["Test", Like.Juice.ORANGE], [1L], ["Test", 1L]] + juice << [['Test'], ['Test', Like.Juice.ORANGE], [1L], ['Test', 1L]] } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy similarity index 88% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy index cd4c80f..1a692d0 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.array +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 -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration @Rollback +@Integration class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { - @Autowired PgArrayTestSearchService pgArrayTestSearchService + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -73,7 +76,7 @@ class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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: ['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) @@ -86,7 +89,7 @@ class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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: 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) @@ -112,7 +115,7 @@ class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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 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) @@ -121,7 +124,7 @@ class PgIsEmptyCriteriaTestServiceIntegrationSpec extends Specification { then: result.size() == 2 - result.contains(user2) == true - result.contains(user3) == true + result.contains(user2) + result.contains(user3) } } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy similarity index 77% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy index 54df9f1..5b0ce99 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec.groovy @@ -1,16 +1,18 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 @@ -119,10 +121,10 @@ class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specificatio @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: ['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", "Starwars"]).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: ['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) @@ -131,23 +133,23 @@ class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specificatio result.size() == resultSize where: - movie | resultSize - ["The Matrix"] | 1 - ["Starwars", "Romeo & Juliet"] | 1 - ["The Lord of the Rings"] | 2 - [] | 1 - ["Starwars", "Romeo & Juliet"] as String[] | 1 - ["The Lord of the Rings"] as String[] | 2 - [] as String[] | 1 + 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: 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", "Starwars"])).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: 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) @@ -156,14 +158,14 @@ class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specificatio result.size() == resultSize where: - movie | resultSize - UuidBuilder.createUUIDs(["The Matrix"]) | 1 - UuidBuilder.createUUIDs(["Starwars", "Romeo & Juliet"]) | 1 - UuidBuilder.createUUIDs(["The Lord of the Rings"]) | 2 - [] | 1 - UuidBuilder.createUUIDs(["Starwars", "Romeo & Juliet"]) as UUID[] | 1 - UuidBuilder.createUUIDs(["The Lord of the Rings"]) as UUID[] | 2 - [] as UUID[] | 1 + 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 @@ -190,10 +192,10 @@ class PgIsEmptyOrContainsCriteriaTestServiceIntegrationSpec extends Specificatio @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", "Starwars"])).save(flush: true, failOnError: true) + 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) + 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) diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy similarity index 88% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy index b8030ba..697837b 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgIsNotEmptyCriteriaTestServiceIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.array +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 -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration @Rollback +@Integration class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { - @Autowired PgArrayTestSearchService pgArrayTestSearchService + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -73,7 +76,7 @@ class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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: ['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) @@ -86,7 +89,7 @@ class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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: 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) @@ -112,7 +115,7 @@ class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { 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 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) @@ -121,6 +124,6 @@ class PgIsNotEmptyCriteriaTestServiceIntegrationSpec extends Specification { then: result.size() == 1 - result.contains(user1) == true + result.contains(user1) } } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy similarity index 73% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy index dd50025..e79b5f3 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgNotEqualsCriteriaTestServiceIntegrationSpec.groovy @@ -1,20 +1,23 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import org.hibernate.HibernateException -import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -120,9 +123,9 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { @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", "Starwars"]).save(flush: true, failOnError: true) - new Like(favoriteMovies: ["Starwars"]).save(flush: true, failOnError: true) + 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: @@ -132,22 +135,22 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { result.size() == resultSize where: - movie | resultSize - "Starwars" | 3 - ["Starwars"] | 3 - ["Starwars"] as String[] | 3 - "The Usual Suspects" | 4 - ["Spiderman", "Blade Runner", "Starwars"] | 3 - ["Spiderman", "Starwars", "Blade Runner"] | 4 - [] | 3 + 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", "Starwars"])).save(flush: true, failOnError: true) - new Like(favoriteMovieUUIDs: UuidBuilder.createUUIDs(["Starwars"])).save(flush: true, failOnError: true) + 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: @@ -157,14 +160,14 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { result.size() == resultSize where: - movie | resultSize - UuidBuilder.createUUID("Starwars") | 3 - UuidBuilder.createUUIDs(["Starwars"]) | 3 - UuidBuilder.createUUIDs(["Starwars"]) as UUID[] | 3 - UuidBuilder.createUUID("The Usual Suspects") | 4 - UuidBuilder.createUUIDs(["Spiderman", "Blade Runner", "Starwars"]) | 3 - UuidBuilder.createUUIDs(["Spiderman", "Starwars", "Blade Runner"]) | 4 - [] | 3 + 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 @@ -191,40 +194,40 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ["Spiderman", "Blade Runner", "Starwars"])).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) + 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) == true - result.contains(user4) == true + result.contains(user1) + result.contains(user4) where: - movie = ["Spiderman", "Blade Runner", "Starwars"] + 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [5, 17, 9, 6], favoriteMovies: ["Spiderman", "Blade Runner", "Starwars"])).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) + 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) == true - result.contains(user4) == true + result.contains(user1) + result.contains(user4) where: - movie = ["Spiderman", "Blade Runner", "Starwars"] + movie = ['Spiderman', 'Blade Runner', 'Star Wars'] number = [5, 17, 9, 6] } @@ -236,7 +239,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1, "Test"], [1L], [1, 1L]] + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] } void 'search an invalid list inside the array of long'() { @@ -247,7 +250,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1L, "Test"], [1], [1L, 1]] + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] } void 'search an invalid list inside the array of float'() { @@ -258,7 +261,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1f, "Test"], [1], [1f, 1]] + number << [['Test'], [1f, 'Test'], [1], [1f, 1]] } void 'search an invalid list inside the array of double'() { @@ -269,7 +272,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1d, "Test"], [1], [1d, 1]] + number << [['Test'], [1d, 'Test'], [1], [1d, 1]] } void 'search an invalid list inside the array of string'() { @@ -280,7 +283,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", 1], [1L], ["Test", 1L], [UUID.randomUUID()]] + movie << [[1], ['Test', 1], [1L], ['Test', 1L], [UUID.randomUUID()]] } void 'search an invalid list inside the array of UUID'() { @@ -291,7 +294,7 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] } void 'search an invalid list inside the array of enum'() { @@ -302,6 +305,6 @@ class PgNotEqualsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] } } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy similarity index 76% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy index d184a6e..ba8ff80 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PgOverlapsCriteriaTestServiceIntegrationSpec.groovy @@ -1,20 +1,23 @@ -package net.kaleidos.hibernate.array +package test.array -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.criteria.array.Like +import app.criteria.array.PgArrayTestSearchService +import app.criteria.array.User import org.hibernate.HibernateException -import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification import spock.lang.Unroll -import test.criteria.array.Like -import test.criteria.array.PgArrayTestSearchService -import test.criteria.array.User -@Integration +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 + @Autowired + PgArrayTestSearchService pgArrayTestSearchService def setup() { User.executeUpdate('delete from User') @@ -140,10 +143,10 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { @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", "Starwars"]).save(flush: true, failOnError: true) - new Like(favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"]).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: ['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) @@ -152,18 +155,18 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { result.size() == resultSize where: - movie | resultSize - "The Matrix" | 1 - "The Lord of the Rings" | 2 - "Blade Runner" | 2 - "Starwars" | 2 - "The Usual Suspects" | 0 - ["Starwars", "Romeo & Juliet"] | 3 - ["The Lord of the Rings"] | 2 - [] | 0 - ["Starwars", "Romeo & Juliet"] as String[] | 3 - ["The Lord of the Rings"] as String[] | 2 - [] as String[] | 0 + 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 @@ -200,41 +203,41 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"])).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) + 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) == true - result.contains(user3) == true - result.contains(user4) == true + result.contains(user2) + result.contains(user3) + result.contains(user4) where: - movie = ["Starwars", "Romeo & Juliet"] + 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", "Starwars"])).save(flush: true, failOnError: true) - def user3 = new User(name: 'Mary', like: new Like(favoriteNumbers: [3, 4], favoriteMovies: ["Romeo & Juliet", "Casablanca", "Starwars"])).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) + 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) == true - result.contains(user4) == true + result.contains(user2) + result.contains(user4) where: - movie = ["Starwars", "Romeo & Juliet"] + movie = ['Star Wars', 'Romeo & Juliet'] number = [9] } @@ -247,7 +250,7 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1, "Test"], [1L], [1, 1L]] + number << [['Test'], [1, 'Test'], [1L], [1, 1L]] } @Unroll @@ -259,7 +262,7 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - number << [["Test"], [1L, "Test"], [1], [1L, 1]] + number << [['Test'], [1L, 'Test'], [1], [1L, 1]] } @Unroll @@ -271,7 +274,7 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", 1], [1L], ["Test", 1L]] + movie << [[1], ['Test', 1], [1L], ['Test', 1L]] } @Unroll @@ -283,7 +286,7 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - movie << [[1], ["Test", UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] + movie << [[1], ['Test', UUID.randomUUID()], [1L], [UUID.randomUUID(), 1L]] } @Unroll @@ -295,6 +298,6 @@ class PgOverlapsCriteriaTestServiceIntegrationSpec extends Specification { thrown HibernateException where: - juice << [["Test"], [Like.Juice.ORANGE, "Test"], [1L], [Like.Juice.APPLE, 1L]] + juice << [['Test'], [Like.Juice.ORANGE, 'Test'], [1L], [Like.Juice.APPLE, 1L]] } } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy similarity index 92% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy index 678750c..3233536 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/PostgresqlArraysDomainIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/PostgresqlArraysDomainIntegrationSpec.groovy @@ -1,19 +1,20 @@ -package net.kaleidos.hibernate.array +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 -import spock.lang.Specification -import spock.lang.Unroll -import test.array.TestDouble -import test.array.TestEnum -import test.array.TestFloat -import test.array.TestInteger -import test.array.TestLong -import test.array.TestString -import test.array.TestUuid -@Integration @Rollback +@Integration class PostgresqlArraysDomainIntegrationSpec extends Specification { def setup() { @@ -103,7 +104,7 @@ class PostgresqlArraysDomainIntegrationSpec extends Specification { testString.stringArray?.length == strings?.size() where: - strings << [null, [], ["string 1"], ["string 1", "string 2"], ["string 1", "string 2", "string 3"]] + strings << [null, [], ['string 1'], ['string 1', 'string 2'], ['string 1', 'string 2', 'string 3']] } @Unroll diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/array/UuidBuilder.groovy b/test-apps/app1/src/integration-test/groovy/test/array/UuidBuilder.groovy similarity index 75% rename from src/integration-test/groovy/net/kaleidos/hibernate/array/UuidBuilder.groovy rename to test-apps/app1/src/integration-test/groovy/test/array/UuidBuilder.groovy index b713454..f9e66ae 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/array/UuidBuilder.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/array/UuidBuilder.groovy @@ -1,5 +1,8 @@ -package net.kaleidos.hibernate.array +package test.array +import groovy.transform.CompileStatic + +@CompileStatic class UuidBuilder { static UUID createUUID(String name) { @@ -9,5 +12,4 @@ class UuidBuilder { static List createUUIDs(List names) { names.collect { createUUID(it) } } - } diff --git a/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsIntegrationSpec.groovy new file mode 100644 index 0000000..2a0cfcc --- /dev/null +++ b/test-apps/app1/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/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreContainsKeyIntegrationSpec.groovy new file mode 100644 index 0000000..9e7043e --- /dev/null +++ b/test-apps/app1/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/app1/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreILikeValueFunctionIntegrationSpec.groovy new file mode 100644 index 0000000..a2d4a04 --- /dev/null +++ b/test-apps/app1/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/app1/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PgHstoreIsContainedIntegrationSpec.groovy new file mode 100644 index 0000000..f9c53fa --- /dev/null +++ b/test-apps/app1/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/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy similarity index 87% rename from src/integration-test/groovy/net/kaleidos/hibernate/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy index 104fa90..7702efb 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/hstore/PostgresqlHstoreMapDomainIntegrationSpec.groovy @@ -1,14 +1,15 @@ -package net.kaleidos.hibernate.hstore +package test.hstore -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.hstore.TestHstoreMap import spock.lang.Ignore import spock.lang.Specification import spock.lang.Unroll -import test.hstore.TestHstoreMap -@Integration +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + @Rollback +@Integration class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { def setup() { @@ -31,8 +32,8 @@ class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { where: data | attribute | value - [foo: "bar"] | "foo" | "bar" - ["foo,bar": "baz,qux"] | "foo,bar" | "baz,qux" + [foo: 'bar'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' } @Unroll @@ -51,8 +52,8 @@ class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { where: data | key | value - [foo: "bar", xxx: "abc"] | "foo" | "bar" - ["foo,bar": "baz,qux"] | "foo,bar" | "baz,qux" + [foo: 'bar', xxx: 'abc'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' } @Unroll @@ -71,9 +72,9 @@ class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { where: data | valueToRemove | size - [foo: "bar", xxx: "abc"] | 'xxx' | 1 - ["foo,bar": "baz,qux"] | 'foo,bar' | 0 - [foo: "bar"] | 'xxx' | 1 + [foo: 'bar', xxx: 'abc'] | 'xxx' | 1 + ['foo,bar': 'baz,qux'] | 'foo,bar' | 0 + [foo: 'bar'] | 'xxx' | 1 } @Unroll @@ -95,8 +96,8 @@ class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { where: data | attribute | value - [foo: "bar"] | "foo" | "bar" - ["foo,bar": "baz,qux"] | "foo,bar" | "baz,qux" + [foo: 'bar'] | 'foo' | 'bar' + ['foo,bar': 'baz,qux'] | 'foo,bar' | 'baz,qux' } @Unroll @@ -137,5 +138,4 @@ class PostgresqlHstoreMapDomainIntegrationSpec extends Specification { then: 'It should be dirty' retrievedTestHstoreMap.isDirty() } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy similarity index 86% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy index 6999824..8b2cf62 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonEqualsIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonTestSearchService -import test.json.TestMapJson -@Integration +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 + @Autowired + PgJsonTestSearchService pgJsonTestSearchService def setup() { TestMapJson.executeUpdate('delete from TestMapJson') diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy similarity index 91% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy index 62a58ab..3ba9b3d 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonPathsIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonTestSearchService -import test.json.TestMapJson -@Integration +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 + @Autowired + PgJsonTestSearchService pgJsonTestSearchService @Unroll void 'Test equals finding nested values (json). sqlOp: equals'() { @@ -55,5 +58,4 @@ class PgJsonPathsIntegrationSpec extends Specification { 3 || 1 6 || 0 } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy similarity index 93% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy index 84253c0..0162b67 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonValuesIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonTestSearchService +import app.json.TestMapJson import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonTestSearchService -import test.json.TestMapJson -@Integration +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 + @Autowired + PgJsonTestSearchService pgJsonTestSearchService @Unroll void 'Test equals finding value: #value with condition is ilike (json)'() { @@ -73,5 +76,4 @@ class PgJsonValuesIntegrationSpec extends Specification { 'Iván' || 1 'John' || 3 } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbContainsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy similarity index 94% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbContainsIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy index 9bde21c..0fae20d 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbContainsIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbContainsIntegrationSpec.groovy @@ -1,17 +1,20 @@ -package net.kaleidos.hibernate.json +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 -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.json.PgJsonbTestSearchService -import test.json.TestMapJsonb -@Integration @Rollback +@Integration class PgJsonbContainsIntegrationSpec extends Specification { - @Autowired PgJsonbTestSearchService pgJsonbTestSearchService + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService def setup() { TestMapJsonb.executeUpdate('delete from TestMapJsonb') @@ -101,5 +104,4 @@ class PgJsonbContainsIntegrationSpec extends Specification { where: map = null } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy similarity index 84% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy index 17c80c1..0437c71 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbEqualsIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonbTestSearchService -import test.json.TestMapJsonb -@Integration +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 + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService @Unroll void 'Test equals finding value: #value (jsonb)'() { diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbIsContainedIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy similarity index 93% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbIsContainedIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy index a06043c..b8fd025 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbIsContainedIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbIsContainedIntegrationSpec.groovy @@ -1,17 +1,20 @@ -package net.kaleidos.hibernate.json +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 -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification -import test.criteria.json.PgJsonbTestSearchService -import test.json.TestMapJsonb -@Integration @Rollback +@Integration class PgJsonbIsContainedIntegrationSpec extends Specification { - @Autowired PgJsonbTestSearchService pgJsonbTestSearchService + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService def setup() { TestMapJsonb.executeUpdate('delete from TestMapJsonb') @@ -84,5 +87,4 @@ class PgJsonbIsContainedIntegrationSpec extends Specification { where: map = null } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy similarity index 91% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy index 5549cbb..09223aa 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbPathsIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonbTestSearchService -import test.json.TestMapJsonb -@Integration +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 + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService @Unroll void 'Test equals finding nested values (jsonb). sqlOp: equals'() { @@ -55,5 +58,4 @@ class PgJsonbPathsIntegrationSpec extends Specification { 3 || 1 6 || 0 } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy similarity index 93% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy index dad7b46..0623645 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PgJsonbValuesIntegrationSpec.groovy @@ -1,18 +1,21 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import org.springframework.beans.factory.annotation.Autowired +import app.criteria.json.PgJsonbTestSearchService +import app.json.TestMapJsonb import spock.lang.Specification import spock.lang.Unroll -import test.criteria.json.PgJsonbTestSearchService -import test.json.TestMapJsonb -@Integration +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 + @Autowired + PgJsonbTestSearchService pgJsonbTestSearchService @Unroll void 'Test equals finding value: #value with condition is ilike (jsonb)'() { @@ -73,5 +76,4 @@ class PgJsonbValuesIntegrationSpec extends Specification { 'Iván' || 1 'John' || 3 } - } diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy similarity index 80% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy index 764f9df..4265b36 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonMapDomainIntegrationSpec.groovy @@ -1,13 +1,14 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.json.TestMapJson import spock.lang.Specification import spock.lang.Unroll -import test.json.TestMapJson -@Integration +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + @Rollback +@Integration class PostgresqlJsonMapDomainIntegrationSpec extends Specification { def setup() { @@ -39,7 +40,7 @@ class PostgresqlJsonMapDomainIntegrationSpec extends Specification { void 'save and read a domain class with json'() { setup: - def value = [name: 'Ivan', age: 34, hasChilds: true, childs: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] + def value = [name: 'Ivan', age: 34, hasChildren: true, children: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] def testMapJson = new TestMapJson(data: value) when: @@ -50,7 +51,7 @@ class PostgresqlJsonMapDomainIntegrationSpec extends Specification { and: def obj = testMapJson.get(testMapJson.id) - obj.data.keySet().collect { it.toString() }.equals(['name', 'age', 'hasChilds', 'childs']) - obj.data.childs.size() == 2 + obj.data.keySet().collect { it.toString() } == ['name', 'age', 'hasChildren', 'children'] + obj.data.children.size() == 2 } } \ No newline at end of file diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy similarity index 96% rename from src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy index ceafe9e..c072ee8 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/json/PostgresqlJsonbMapDomainIntegrationSpec.groovy @@ -1,13 +1,14 @@ -package net.kaleidos.hibernate.json +package test.json -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration +import app.json.TestMapJsonb import spock.lang.Specification import spock.lang.Unroll -import test.json.TestMapJsonb -@Integration +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + @Rollback +@Integration class PostgresqlJsonbMapDomainIntegrationSpec extends Specification { def setup() { diff --git a/src/integration-test/groovy/net/kaleidos/hibernate/order/PgOrderIntegrationSpec.groovy b/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy similarity index 91% rename from src/integration-test/groovy/net/kaleidos/hibernate/order/PgOrderIntegrationSpec.groovy rename to test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy index bcaa9f2..d1d9798 100644 --- a/src/integration-test/groovy/net/kaleidos/hibernate/order/PgOrderIntegrationSpec.groovy +++ b/test-apps/app1/src/integration-test/groovy/test/order/PgOrderIntegrationSpec.groovy @@ -1,12 +1,13 @@ -package net.kaleidos.hibernate.order +package test.order + +import app.json.TestMapJsonb +import spock.lang.Specification import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration -import spock.lang.Specification -import test.json.TestMapJsonb -@Integration @Rollback +@Integration class PgOrderIntegrationSpec extends Specification { def pgOrderService @@ -25,7 +26,7 @@ class PgOrderIntegrationSpec extends Specification { def result = pgOrderService.orderByJson() then: - result != null + result result.data.name == ['Iván', 'Ernesto', 'Alonso'] } @@ -39,7 +40,7 @@ class PgOrderIntegrationSpec extends Specification { def result = pgOrderService.orderByRandom() then: - result != null + result result.size() == 3 } } \ No newline at end of file diff --git a/travis-build.sh b/travis-build.sh deleted file mode 100755 index 7facd50..0000000 --- a/travis-build.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -./gradlew clean check assemble --stacktrace - -EXIT_STATUS=0 -echo "Publishing archives for branch $TRAVIS_BRANCH" -if [[ -n $TRAVIS_TAG ]] || [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then - if [[ -n $TRAVIS_TAG ]]; then - echo "Pushing build to Bintray" - ./gradlew bintrayUpload || EXIT_STATUS=$? - fi -fi -exit $EXIT_STATUS - From 0a57f5256043ef34a460d92175fc79a6bf975681 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 13:38:25 +0200 Subject: [PATCH 2/7] build: Add `mavenCentral()` --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 6106700..299bdf4 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ group = 'this.will.be.overridden' subprojects { repositories { + mavenCentral() maven { url = 'https://repo.grails.org/grails/restricted' } maven { url = 'https://repository.apache.org/content/groups/snapshots' From f8234e56cd02cc8a810ad49139159a481a909f51 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 13:53:23 +0200 Subject: [PATCH 3/7] ci: Add postgres env vars --- .github/workflows/gradle.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7932c22..2bffe4f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -23,13 +23,21 @@ jobs: services: postgres: image: postgres + env: + POSTGRES_USER: postgres_extensions + POSTGRES_PASSWORD: postgres_extensions ports: - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: liberica From daed851cc645a20ea0fc4c5eeb29457fa5173d02 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 13:59:48 +0200 Subject: [PATCH 4/7] ci: Add expected postgres db name --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2bffe4f..4858cdc 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,6 +24,7 @@ jobs: postgres: image: postgres env: + POSTGRES_DB: pg_extensions_test POSTGRES_USER: postgres_extensions POSTGRES_PASSWORD: postgres_extensions ports: From 7e67393361ef52ade0606df30421642caf66032b Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 14:16:54 +0200 Subject: [PATCH 5/7] ci: Create an extension in PG --- .github/workflows/gradle.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4858cdc..e17e48b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -30,11 +30,19 @@ jobs: ports: - 5432:5432 options: >- - --health-cmd pg_isready + --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 + run: sudo apt-get update && sudo apt-get install -y 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" From f5fb29bc9a899c0cd13778df05a543af69ce2324 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sun, 12 Oct 2025 14:22:58 +0200 Subject: [PATCH 6/7] ci: fix pg client install --- .github/workflows/gradle.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e17e48b..3a64c11 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -36,7 +36,11 @@ jobs: --health-retries 5 steps: - name: Install psql client - run: sudo apt-get update && sudo apt-get install -y postgresql-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 From e31f3263c66f7a0d581eb84dc22f7075b6b5c341 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 14 Oct 2025 19:34:38 +0200 Subject: [PATCH 7/7] fix: change maven coords GPC projects should publish to `io.github.gpc`. --- README.md | 2 +- plugin/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d977d2..17ffe9a 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ In `build.gradle` add the following dependencies to install the plugin: ```groovy dependencies { //... + implementation 'io.github.gpc:grails-postgresql-extensions:' implementation 'org.apache.grails:grails-data-hibernate5' - implementation 'org.grails.plugins:grails-postgresql-extensions:' //... } ``` diff --git a/plugin/build.gradle b/plugin/build.gradle index 732ace6..de5f5d3 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -4,7 +4,7 @@ plugins { } version = projectVersion -group = 'org.grails.plugins' +group = 'io.github.gpc' grailsPublish { artifactId = 'grails-postgresql-extensions'