diff --git a/docker_db.sh b/docker_db.sh index 5255d9f75d24..30ec1eda4341 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -151,7 +151,7 @@ mariadb_verylatest() { } postgresql() { - postgresql_16 + postgresql_17 } postgresql_12() { @@ -186,8 +186,15 @@ postgresql_16() { $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-16-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } +postgresql_17() { + $CONTAINER_CLI rm -f postgres || true + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d ${DB_IMAGE_POSTGRESQL_17:-docker.io/postgis/postgis:17-3.5} \ + -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c shared_buffers=256MB -c maintenance_work_mem=256MB -c max_wal_size=1GB -c checkpoint_timeout=1d + $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-17-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' +} + edb() { - edb_16 + edb_17 } edb_12() { @@ -218,6 +225,13 @@ edb_16() { $CONTAINER_CLI run --name edb -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p 5444:5444 -d edb-test:16 } +edb_17() { + $CONTAINER_CLI rm -f edb || true + # We need to build a derived image because the existing image is mainly made for use by a kubernetes operator + (cd edb; $CONTAINER_CLI build -t edb-test:17 -f edb17.Dockerfile .) + $CONTAINER_CLI run --name edb -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p 5444:5444 -d edb-test:17 +} + db2() { db2_11_5 } diff --git a/edb/edb17.Dockerfile b/edb/edb17.Dockerfile new file mode 100644 index 000000000000..a2be36fcef54 --- /dev/null +++ b/edb/edb17.Dockerfile @@ -0,0 +1,48 @@ +FROM quay.io/enterprisedb/edb-postgres-advanced:17.4-3.5-postgis +USER root +# this 777 will be replaced by 700 at runtime (allows semi-arbitrary "--user" values) +RUN chown -R postgres:postgres /var/lib/edb && chmod 777 /var/lib/edb && rm /docker-entrypoint-initdb.d/10_postgis.sh + +USER postgres +ENV LANG en_US.utf8 +ENV PG_MAJOR 17 +ENV PG_VERSION 17 +ENV PGPORT 5444 +ENV PGDATA /var/lib/edb/as$PG_MAJOR/data/ +VOLUME /var/lib/edb/as$PG_MAJOR/data/ + +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] + +# We set the default STOPSIGNAL to SIGINT, which corresponds to what PostgreSQL +# calls "Fast Shutdown mode" wherein new connections are disallowed and any +# in-progress transactions are aborted, allowing PostgreSQL to stop cleanly and +# flush tables to disk, which is the best compromise available to avoid data +# corruption. +# +# Users who know their applications do not keep open long-lived idle connections +# may way to use a value of SIGTERM instead, which corresponds to "Smart +# Shutdown mode" in which any existing sessions are allowed to finish and the +# server stops when all sessions are terminated. +# +# See https://www.postgresql.org/docs/12/server-shutdown.html for more details +# about available PostgreSQL server shutdown signals. +# +# See also https://www.postgresql.org/docs/12/server-start.html for further +# justification of this as the default value, namely that the example (and +# shipped) systemd service files use the "Fast Shutdown mode" for service +# termination. +# +STOPSIGNAL SIGINT +# +# An additional setting that is recommended for all users regardless of this +# value is the runtime "--stop-timeout" (or your orchestrator/runtime's +# equivalent) for controlling how long to wait between sending the defined +# STOPSIGNAL and sending SIGKILL (which is likely to cause data corruption). +# +# The default in most runtimes (such as Docker) is 10 seconds, and the +# documentation at https://www.postgresql.org/docs/12/server-start.html notes +# that even 90 seconds may not be long enough in many instances. + +EXPOSE 5444 +CMD ["postgres"] \ No newline at end of file diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 27aec0cc6ab8..f59c66fca7a5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -519,7 +519,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_postgresql( false ); functionFactory.jsonArrayInsert_postgresql(); - functionFactory.unnest_postgresql(); + functionFactory.unnest_postgresql( false ); functionFactory.generateSeries( null, "ordinality", true ); functionFactory.jsonTable_cockroachdb(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 87a6be4b79d5..5047e2430b94 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -650,7 +650,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); if ( getVersion().isSameOrAfter( 17 ) ) { - functionFactory.jsonValue(); + functionFactory.jsonValue_postgresql( true ); functionFactory.jsonQuery(); functionFactory.jsonExists(); functionFactory.jsonObject(); @@ -660,7 +660,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonTable(); } else { - functionFactory.jsonValue_postgresql(); + functionFactory.jsonValue_postgresql( false ); functionFactory.jsonQuery_postgresql(); functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { @@ -726,12 +726,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); functionFactory.dateTrunc(); - if ( getVersion().isSameOrAfter( 17 ) ) { - functionFactory.unnest( null, "ordinality" ); - } - else { - functionFactory.unnest_postgresql(); - } + functionFactory.unnest_postgresql( getVersion().isSameOrAfter( 17 ) ); functionFactory.generateSeries( null, "ordinality", false ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index be551da6554f..953ebdd7f028 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -487,7 +487,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_postgresql( false ); functionFactory.jsonArrayInsert_postgresql(); - functionFactory.unnest_postgresql(); + functionFactory.unnest_postgresql( false ); functionFactory.generateSeries( null, "ordinality", true ); functionFactory.jsonTable_cockroachdb(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 09964836b84b..103f726ce9a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -614,7 +614,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); if ( getVersion().isSameOrAfter( 17 ) ) { - functionFactory.jsonValue(); + functionFactory.jsonValue_postgresql( true ); functionFactory.jsonQuery(); functionFactory.jsonExists(); functionFactory.jsonObject(); @@ -624,7 +624,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonTable(); } else { - functionFactory.jsonValue_postgresql(); + functionFactory.jsonValue_postgresql( false ); functionFactory.jsonQuery_postgresql(); functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { @@ -688,12 +688,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); functionFactory.dateTrunc(); - if ( getVersion().isSameOrAfter( 17 ) ) { - functionFactory.unnest( null, "ordinality" ); - } - else { - functionFactory.unnest_postgresql(); - } + functionFactory.unnest_postgresql( getVersion().isSameOrAfter( 17 ) ); functionFactory.generateSeries( null, "ordinality", false ); functionFactory.hex( "encode(?1, 'hex')" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index c21bbc100829..3b9796ced56a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -3319,13 +3319,6 @@ public void arrayToString_oracle() { functionRegistry.register( "array_to_string", new OracleArrayToStringFunction( typeConfiguration ) ); } - /** - * json_value() function - */ - public void jsonValue() { - functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, true ) ); - } - /** * HANA json_value() function */ @@ -3350,8 +3343,8 @@ public void jsonValue_db2() { /** * PostgreSQL json_value() function */ - public void jsonValue_postgresql() { - functionRegistry.register( "json_value", new PostgreSQLJsonValueFunction( typeConfiguration ) ); + public void jsonValue_postgresql(boolean supportsStandard) { + functionRegistry.register( "json_value", new PostgreSQLJsonValueFunction( supportsStandard, typeConfiguration ) ); } /** @@ -4232,8 +4225,8 @@ public void unnest_oracle() { /** * PostgreSQL unnest() function */ - public void unnest_postgresql() { - functionRegistry.register( "unnest", new PostgreSQLUnnestFunction() ); + public void unnest_postgresql(boolean supportsJsonTable) { + functionRegistry.register( "unnest", new PostgreSQLUnnestFunction( supportsJsonTable ) ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java index 94d6576475f6..21ccec81e6c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java @@ -23,8 +23,11 @@ */ public class PostgreSQLUnnestFunction extends UnnestFunction { - public PostgreSQLUnnestFunction() { + private final boolean supportsJsonTable; + + public PostgreSQLUnnestFunction(boolean supportsJsonTable) { super( null, "ordinality" ); + this.supportsJsonTable = supportsJsonTable; } @Override @@ -36,41 +39,54 @@ protected void renderJsonTable( AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator walker) { - final AggregateSupport aggregateSupport = walker.getSessionFactory().getJdbcServices().getDialect() - .getAggregateSupport(); - sqlAppender.appendSql( "(select" ); - tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { - if ( selectionIndex == 0 ) { - sqlAppender.append( ' ' ); - } - else { - sqlAppender.append( ',' ); - } - if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { - sqlAppender.appendSql( "t.i" ); + if ( supportsJsonTable ) { + super.renderJsonTable( + sqlAppender, + array, + pluralType, + sqlTypedMapping, + tupleType, + tableIdentifierVariable, + walker + ); + } + else { + final AggregateSupport aggregateSupport = walker.getSessionFactory().getJdbcServices().getDialect() + .getAggregateSupport(); + sqlAppender.appendSql( "(select" ); + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.appendSql( "t.i" ); + } + else { + sqlAppender.append( aggregateSupport.aggregateComponentCustomReadExpression( + "", + "", + "t.v", + selectableMapping.getSelectableName(), + SqlTypes.JSON, + selectableMapping, + walker.getSessionFactory().getTypeConfiguration() + ) ); + } + sqlAppender.append( " as " ); + sqlAppender.append( selectableMapping.getSelectionExpression() ); + } ); + sqlAppender.appendSql( " from jsonb_array_elements(" ); + array.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { + sqlAppender.appendSql( " with ordinality t(v,i))" ); } else { - sqlAppender.append( aggregateSupport.aggregateComponentCustomReadExpression( - "", - "", - "t.v", - selectableMapping.getSelectableName(), - SqlTypes.JSON, - selectableMapping, - walker.getSessionFactory().getTypeConfiguration() - ) ); + sqlAppender.appendSql( " t(v))" ); } - sqlAppender.append( " as " ); - sqlAppender.append( selectableMapping.getSelectionExpression() ); - } ); - sqlAppender.appendSql( " from jsonb_array_elements(" ); - array.accept( walker ); - sqlAppender.appendSql( ')' ); - if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { - sqlAppender.appendSql( " with ordinality t(v,i))" ); - } - else { - sqlAppender.appendSql( " t(v))" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java index 39896a229156..de77ddaf9bb6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java @@ -248,6 +248,12 @@ protected void renderColumnPath(String name, @Nullable String jsonPath, SqlAppen sqlAppender.appendSql( " path " ); sqlAppender.appendSingleQuoteEscapedString( jsonPath ); } + else { + // Always append implicit path to avoid identifier case sensitivity issues + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( name ); + sqlAppender.appendSql( '\'' ); + } } protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java index f47e6aa5b3c3..e8911dc85401 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -26,8 +26,11 @@ */ public class PostgreSQLJsonValueFunction extends JsonValueFunction { - public PostgreSQLJsonValueFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsStandard; + + public PostgreSQLJsonValueFunction(boolean supportsStandard, TypeConfiguration typeConfiguration) { super( typeConfiguration, true, true ); + this.supportsStandard = supportsStandard; } @Override @@ -36,23 +39,50 @@ protected void render( JsonValueArguments arguments, ReturnableType returnType, SqlAstTranslator walker) { - // jsonb_path_query_first errors by default - if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { - throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + if ( supportsStandard ) { + super.render( sqlAppender, arguments, returnType, walker ); + // PostgreSQL unfortunately renders `t`/`f` for JSON booleans instead of `true`/`false` like every other DB. + // To work around this, extract the jsonb node directly and then use the `#>>` operator to unquote values + // Also see https://stackoverflow.com/questions/79483975/postgresql-json-value-boolean-behavior + if ( isString( arguments.returningType() ) ) { + // Unquote the value + sqlAppender.appendSql( "#>>'{}'" ); + } } - if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { - throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + else { + // jsonb_path_query_first errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + + appendJsonValue( + sqlAppender, + arguments.jsonDocument(), + arguments.jsonPath(), + arguments.isJsonType(), + arguments.returningType(), + arguments.passingClause(), + walker + ); } + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments arguments, SqlAstTranslator walker) { + // See #render for an explanation of this behavior + if ( supportsStandard && isString( arguments.returningType() ) ) { + sqlAppender.appendSql( " returning jsonb" ); + } + else { + super.renderReturningClause( sqlAppender, arguments, walker ); + } + } - appendJsonValue( - sqlAppender, - arguments.jsonDocument(), - arguments.jsonPath(), - arguments.isJsonType(), - arguments.returningType(), - arguments.passingClause(), - walker - ); + private boolean isString(@Nullable CastTarget castTarget) { + return castTarget == null || castTarget.getJdbcMapping().getJdbcType().isString(); } static void appendJsonValue(SqlAppender sqlAppender, Expression jsonDocument, SqlAstNode jsonPath, boolean isJsonType, @Nullable CastTarget castTarget, @Nullable JsonPathPassingClause passingClause, SqlAstTranslator walker) {