diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 762469a85469..6f12a415ba5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: - rdbms: mysql - rdbms: mariadb - rdbms: postgresql + - rdbms: gaussdb - rdbms: edb - rdbms: oracle - rdbms: db2 diff --git a/ci/build.sh b/ci/build.sh index 26cc4a067f86..7f1bb82a2341 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -14,6 +14,8 @@ elif [ "$RDBMS" == "mariadb" ] || [ "$RDBMS" == "mariadb_10_4" ]; then goal="-Pdb=mariadb_ci" elif [ "$RDBMS" == "postgresql" ] || [ "$RDBMS" == "postgresql_13" ]; then goal="-Pdb=pgsql_ci" +elif [ "$RDBMS" == "gaussdb" ]; then + goal="-Pdb=gaussdb -DdbHost=localhost:8000" elif [ "$RDBMS" == "edb" ] || [ "$RDBMS" == "edb_13" ]; then goal="-Pdb=edb_ci -DdbHost=localhost:5444" elif [ "$RDBMS" == "oracle" ]; then diff --git a/ci/database-start.sh b/ci/database-start.sh index da8f3a3e78b6..a6949226d6e8 100755 --- a/ci/database-start.sh +++ b/ci/database-start.sh @@ -8,6 +8,8 @@ elif [ "$RDBMS" == 'mariadb' ]; then bash $DIR/../docker_db.sh mariadb elif [ "$RDBMS" == 'postgresql' ]; then bash $DIR/../docker_db.sh postgresql +elif [ "$RDBMS" == 'gaussdb' ]; then + bash $DIR/../docker_db.sh gaussdb elif [ "$RDBMS" == 'edb' ]; then bash $DIR/../docker_db.sh edb elif [ "$RDBMS" == 'db2' ]; then diff --git a/docker_db.sh b/docker_db.sh index e280694fb29f..d180b71765d2 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -211,6 +211,47 @@ postgresql_17() { $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;"' } +gaussdb() { + $CONTAINER_CLI rm -f opengauss || true + + # config param + CONTAINER_NAME=opengauss + IMAGE=opengauss/opengauss:7.0.0-RC1 + PORT=8000 + DB_USER=hibernate_orm_test + DB_PASSWORD=Hibernate_orm_test@1234 + DB_NAME=hibernate_orm_test + PSQL_IMAGE=postgres:14 + + echo "start OpenGauss container..." + $CONTAINER_CLI run --name ${CONTAINER_NAME} \ + --privileged \ + -e GS_PASSWORD=${DB_PASSWORD} \ + -e GS_NODENAME=opengauss \ + -e GS_PORT=${PORT} \ + -e GS_CGROUP_DISABLE=YES \ + -p ${PORT}:8000 \ + -d ${IMAGE} + + echo "wait OpenGauss starting..." + sleep 20 + + echo " Initialize the database using the PostgreSQL client container..." + + $CONTAINER_CLI run --rm --network=host ${PSQL_IMAGE} \ + bash -c " + PGPASSWORD='${DB_PASSWORD}' psql -h localhost -p ${PORT} -U gaussdb -d postgres -c \"CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';\" && + PGPASSWORD='${DB_PASSWORD}' psql -h localhost -p ${PORT} -U gaussdb -d postgres -c \"CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};\" && + PGPASSWORD='${DB_PASSWORD}' psql -h localhost -p ${PORT} -U gaussdb -d ${DB_NAME} -c \"CREATE SCHEMA test AUTHORIZATION ${DB_USER};\" + " + + echo "Initialization completed" + echo "connection information" + echo " Host: localhost" + echo " Port: ${PORT}" + echo " Database: ${DB_NAME}" +} + edb() { edb_17 } @@ -1089,6 +1130,7 @@ if [ -z ${1} ]; then echo -e "\toracle" echo -e "\toracle_23" echo -e "\toracle_21" + echo -e "\tgaussdb" echo -e "\tpostgresql" echo -e "\tpostgresql_17" echo -e "\tpostgresql_16" diff --git a/hibernate-agroal/src/test/java/org/hibernate/test/agroal/AgroalTransactionIsolationConfigTest.java b/hibernate-agroal/src/test/java/org/hibernate/test/agroal/AgroalTransactionIsolationConfigTest.java index b95ae6c3cb26..f9c8cdd34184 100644 --- a/hibernate-agroal/src/test/java/org/hibernate/test/agroal/AgroalTransactionIsolationConfigTest.java +++ b/hibernate-agroal/src/test/java/org/hibernate/test/agroal/AgroalTransactionIsolationConfigTest.java @@ -6,6 +6,7 @@ import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.TiDBDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.agroal.internal.AgroalConnectionProvider; @@ -17,6 +18,7 @@ */ @SkipForDialect(value = TiDBDialect.class, comment = "Doesn't support SERIALIZABLE isolation") @SkipForDialect(value = AltibaseDialect.class, comment = "Altibase cannot change isolation level in autocommit mode") +@SkipForDialect(value = GaussDBDialect.class, comment = "GaussDB query serialization level of SERIALIZABLE has some problem") public class AgroalTransactionIsolationConfigTest extends BaseTransactionIsolationConfigTest { @Override protected ConnectionProvider getConnectionProviderUnderTest() { diff --git a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java index 7e002cc81ca5..2dcbe9ed51e8 100644 --- a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java +++ b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java @@ -8,6 +8,7 @@ import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.c3p0.internal.C3P0ConnectionProvider; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.community.dialect.TiDBDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; @@ -23,6 +24,7 @@ @SkipForDialect(value = TiDBDialect.class, comment = "Doesn't support SERIALIZABLE isolation") @SkipForDialect(value = AltibaseDialect.class, comment = "Altibase cannot change isolation level in autocommit mode") @SkipForDialect(value = SybaseASEDialect.class, comment = "JtdsConnection.isValid not implemented") +@SkipForDialect(value = GaussDBDialect.class, comment = "GaussDB query serialization level of SERIALIZABLE has some problem") public class C3p0TransactionIsolationConfigTest extends BaseTransactionIsolationConfigTest { private StandardServiceRegistry ssr; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractGaussDBStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractGaussDBStructJdbcType.java new file mode 100644 index 000000000000..e0c65ccefd6c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractGaussDBStructJdbcType.java @@ -0,0 +1,1585 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; +import java.lang.reflect.Array; +import java.sql.CallableStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.TimeZone; + +import org.hibernate.internal.util.CharSequenceHelper; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.IntegerJavaType; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.StructAttributeValues; +import org.hibernate.type.descriptor.jdbc.StructHelper; +import org.hibernate.type.descriptor.jdbc.StructuredJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.type.descriptor.jdbc.StructHelper.getEmbeddedPart; +import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; + +/** + * Implementation for serializing/deserializing an embeddable aggregate to/from the GaussDB component format. + * For regular queries, we select the individual struct elements because the GaussDB component format encoding + * is probably not very efficient. + * + * @author liubao + * + * Notes: Original code of this class is based on AbstractPostgreSQLStructJdbcType. + */ +public abstract class AbstractGaussDBStructJdbcType implements StructuredJdbcType { + + private static final DateTimeFormatter LOCAL_DATE_TIME; + static { + LOCAL_DATE_TIME = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .appendOffset( "+HH:mm", "+00" ) + .toFormatter(); + } + + // Need a custom formatter for parsing what PostgresPlus/EDB produces + private static final DateTimeFormatter LOCAL_DATE; + static { + LOCAL_DATE = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .optionalStart() + .appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .appendOffset( "+HH:mm", "+00" ) + .toFormatter(); + } + private final String typeName; + private final int[] orderMapping; + private final int[] inverseOrderMapping; + private final EmbeddableMappingType embeddableMappingType; + + protected AbstractGaussDBStructJdbcType( + EmbeddableMappingType embeddableMappingType, + String typeName, + int[] orderMapping) { + this.typeName = typeName; + this.embeddableMappingType = embeddableMappingType; + this.orderMapping = orderMapping; + if ( orderMapping == null ) { + this.inverseOrderMapping = null; + } + else { + final int[] inverseOrderMapping = new int[orderMapping.length]; + for ( int i = 0; i < orderMapping.length; i++ ) { + inverseOrderMapping[orderMapping[i]] = i; + } + this.inverseOrderMapping = inverseOrderMapping; + } + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.STRUCT; + } + + @Override + public String getStructTypeName() { + return typeName; + } + + @Override + public EmbeddableMappingType getEmbeddableMappingType() { + return embeddableMappingType; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer precision, + Integer scale, + TypeConfiguration typeConfiguration) { + if ( embeddableMappingType == null ) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( Object[].class ); + } + else { + //noinspection unchecked + return (JavaType) embeddableMappingType.getMappedJavaType(); + } + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getObject( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getObject( statement.getObject( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return getObject( statement.getObject( name ), options ); + } + + private X getObject(Object object, WrapperOptions options) throws SQLException { + if ( object == null ) { + return null; + } + return ( (AbstractGaussDBStructJdbcType) getJdbcType() ).fromString( + object.toString(), + getJavaType(), + options + ); + } + }; + } + + protected X fromString(String string, JavaType javaType, WrapperOptions options) throws SQLException { + if ( string == null ) { + return null; + } + final boolean returnEmbeddable = javaType.getJavaTypeClass() != Object[].class; + final int end; + final Object[] array; + if ( embeddableMappingType == null ) { + assert !returnEmbeddable; + final ArrayList values = new ArrayList<>( 8 ); + end = deserializeStruct( string, 0, string.length() - 1, values ); + array = values.toArray(); + } + else { + array = new Object[embeddableMappingType.getJdbcValueCount() + ( embeddableMappingType.isPolymorphic() ? 1 : 0 )]; + end = deserializeStruct( string, 0, 0, array, returnEmbeddable, options ); + } + assert end == string.length(); + if ( returnEmbeddable ) { + final StructAttributeValues attributeValues = getAttributeValues( embeddableMappingType, orderMapping, array, options ); + //noinspection unchecked + return (X) instantiate( embeddableMappingType, attributeValues ); + } + else if ( inverseOrderMapping != null ) { + StructHelper.orderJdbcValues( embeddableMappingType, inverseOrderMapping, array.clone(), array ); + } + //noinspection unchecked + return (X) array; + } + + private int deserializeStruct( + String string, + int begin, + int end, + ArrayList values) { + int column = 0; + boolean inQuote = false; + boolean hasEscape = false; + assert string.charAt( begin ) == '('; + int start = begin + 1; + int element = 1; + for ( int i = start; i < string.length(); i++ ) { + final char c = string.charAt( i ); + switch ( c ) { + case '"': + if ( inQuote ) { + if ( i + 1 != end && string.charAt( i + 1 ) == '"' ) { + // Skip double quotes as that will be unescaped later + i++; + hasEscape = true; + continue; + } + if ( hasEscape ) { + values.add( unescape( string, start, i ) ); + } + else { + values.add( string.substring( start, i ) ); + } + column++; + inQuote = false; + } + else { + inQuote = true; + } + hasEscape = false; + start = i + 1; + break; + case ',': + if ( !inQuote ) { + if ( column < element ) { + if ( start == i ) { + values.add( null ); + } + else { + values.add( string.substring( start, i ) ); + } + column++; + } + start = i + 1; + element++; + } + break; + case ')': + if ( !inQuote ) { + if ( column < element ) { + if ( start == i ) { + values.add( null ); + } + else { + values.add( string.substring( start, i ) ); + } + } + return i + 1; + } + break; + } + } + + throw new IllegalArgumentException( "Struct not properly formed: " + string.subSequence( start, end ) ); + } + + private int deserializeStruct( + String string, + int begin, + int quotes, + Object[] values, + boolean returnEmbeddable, + WrapperOptions options) throws SQLException { + int column = 0; + boolean inQuote = false; + StringBuilder escapingSb = null; + assert string.charAt( begin ) == '('; + int start = begin + 1; + for ( int i = start; i < string.length(); i++ ) { + final char c = string.charAt( i ); + switch ( c ) { + case '\\': + if ( inQuote ) { + final int expectedQuoteCount = 1 << quotes; + if ( repeatsChar( string, i, expectedQuoteCount, '\\' ) ) { + if ( isDoubleQuote( string, i + expectedQuoteCount, expectedQuoteCount ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '"' ); + // Move forward to the last quote + i += expectedQuoteCount + expectedQuoteCount - 1; + start = i + 1; + continue; + } + else { + assert repeatsChar( string, i + expectedQuoteCount, expectedQuoteCount, '\\' ); + // Don't create an escaping string builder for binary literals + if ( i != start || !isBinary( column ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '\\' ); + start = i + expectedQuoteCount + expectedQuoteCount; + } + // Move forward to the last backslash + i += expectedQuoteCount + expectedQuoteCount - 1; + continue; + } + } + } + // Fall-through since a backslash is an escaping mechanism for a start quote within arrays + case '"': + if ( inQuote ) { + if ( isDoubleQuote( string, i, 1 << ( quotes + 1 ) ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '"' ); + // Move forward to the last quote + i += ( 1 << ( quotes + 1 ) ) - 1; + start = i + 1; + continue; + } + assert isDoubleQuote( string, i, 1 << quotes ); + final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.DATE: + values[column] = fromRawObject( + jdbcMapping, + parseDate( + CharSequenceHelper.subSequence( + string, + start, + i + ) + ), + options + ); + break; + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + values[column] = fromRawObject( + jdbcMapping, + parseTime( + CharSequenceHelper.subSequence( + string, + start, + i + ) + ), + options + ); + break; + case SqlTypes.TIMESTAMP: + values[column] = fromRawObject( + jdbcMapping, + parseTimestamp( + CharSequenceHelper.subSequence( + string, + start, + i + ), + jdbcMapping.getJdbcJavaType() + ), + options + ); + break; + case SqlTypes.TIMESTAMP_WITH_TIMEZONE: + case SqlTypes.TIMESTAMP_UTC: + values[column] = fromRawObject( + jdbcMapping, + parseTimestampWithTimeZone( + CharSequenceHelper.subSequence( + string, + start, + i + ), + jdbcMapping.getJdbcJavaType() + ), + options + ); + break; + case SqlTypes.BINARY: + case SqlTypes.VARBINARY: + case SqlTypes.LONGVARBINARY: + case SqlTypes.LONG32VARBINARY: + final int backslashes = 1 << ( quotes + 1 ); + assert repeatsChar( string, start, backslashes, '\\' ); + final int xCharPosition = start + backslashes; + assert string.charAt( xCharPosition ) == 'x'; + values[column] = fromString( + jdbcMapping, + string, + xCharPosition + 1, + i + ); + break; + default: + if ( escapingSb == null || escapingSb.length() == 0 ) { + values[column] = fromString( + jdbcMapping, + string, + start, + i + ); + } + else { + escapingSb.append( string, start, i ); + values[column] = fromString( + jdbcMapping, + escapingSb, + 0, + escapingSb.length() + ); + escapingSb.setLength( 0 ); + } + break; + } + column++; + inQuote = false; + // move forward the index by 2 ^ quoteLevel to point to the next char after the quote + i += 1 << quotes; + if ( string.charAt( i ) == ')' ) { + // Return the end position if this is the last element + assert column == values.length; + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + } + else { + // This is a start quote, so move forward the index to the last quote + final int expectedQuotes = 1 << quotes; + assert isDoubleQuote( string, i, expectedQuotes ); + i += expectedQuotes - 1; + if ( string.charAt( i + 1 ) == '(' ) { + // This could be a nested struct + final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + if ( jdbcMapping.getJdbcType() instanceof AbstractGaussDBStructJdbcType structJdbcType ) { + final Object[] subValues = new Object[structJdbcType.embeddableMappingType.getJdbcValueCount()]; + final int subEnd = structJdbcType.deserializeStruct( + string, + i + 1, + quotes + 1, + subValues, + returnEmbeddable, + options + ); + if ( returnEmbeddable ) { + final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( + structJdbcType.embeddableMappingType, + structJdbcType.orderMapping, + subValues, + options + ); + values[column] = instantiate( structJdbcType.embeddableMappingType, attributeValues ); + } + else { + if ( structJdbcType.inverseOrderMapping != null ) { + StructHelper.orderJdbcValues( + structJdbcType.embeddableMappingType, + structJdbcType.inverseOrderMapping, + subValues.clone(), + subValues + ); + } + values[column] = subValues; + } + column++; + // The subEnd points to the first character after the ')', + // so move forward the index to point to the next char after quotes + assert isDoubleQuote( string, subEnd, expectedQuotes ); + i = subEnd + expectedQuotes; + if ( string.charAt( i ) == ')' ) { + // Return the end position if this is the last element + assert column == values.length; + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + } + else { + inQuote = true; + } + } + else if ( string.charAt( i + 1 ) == '{' ) { + // This could be a quoted array + final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + if ( jdbcMapping instanceof BasicPluralType pluralType ) { + final ArrayList arrayList = new ArrayList<>(); + //noinspection unchecked + final int subEnd = deserializeArray( + string, + i + 1, + quotes + 1, + arrayList, + (BasicType) pluralType.getElementType(), + returnEmbeddable, + options + ); + assert string.charAt( subEnd - 1 ) == '}'; + values[column] = pluralType.getJdbcJavaType().wrap( arrayList, options ); + column++; + // The subEnd points to the first character after the ')', + // so move forward the index to point to the next char after quotes + assert isDoubleQuote( string, subEnd, expectedQuotes ); + i = subEnd + expectedQuotes; + if ( string.charAt( i ) == ')' ) { + // Return the end position if this is the last element + assert column == values.length; + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + } + else { + inQuote = true; + } + } + else { + inQuote = true; + } + } + start = i + 1; + break; + case ',': + if ( !inQuote ) { + if ( start == i ) { + values[column] = null; + } + else { + final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + if ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { + values[column] = fromRawObject( + jdbcMapping, + string.charAt( start ) == 't', + options + ); + } + else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() + && jdbcMapping.getJdbcType().isInteger() ) { + values[column] = fromRawObject( + jdbcMapping, + IntegerJavaType.INSTANCE.fromEncodedString( string, start, i ), + options + ); + } + else { + values[column] = fromString( + jdbcMapping, + string, + start, + i + ); + } + } + column++; + start = i + 1; + } + break; + case ')': + if ( !inQuote ) { + if ( column < values.length ) { + if ( start == i ) { + values[column] = null; + } + else { + final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + if ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { + values[column] = fromRawObject( + jdbcMapping, + string.charAt( start ) == 't', + options + ); + } + else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() + && jdbcMapping.getJdbcType().isInteger() ) { + values[column] = fromRawObject( + jdbcMapping, + IntegerJavaType.INSTANCE.fromEncodedString( string, start, i ), + options + ); + } + else { + values[column] = fromString( + jdbcMapping, + string, + start, + i + ); + } + } + } + return i + 1; + } + break; + case '{': + if ( !inQuote ) { + final BasicPluralType pluralType = (BasicPluralType) getJdbcValueSelectable( column ).getJdbcMapping(); + final ArrayList arrayList = new ArrayList<>(); + //noinspection unchecked + i = deserializeArray( + string, + i, + quotes + 1, + arrayList, + (BasicType) pluralType.getElementType(), + returnEmbeddable, + options + ); + assert string.charAt( i - 1 ) == '}'; + values[column] = pluralType.getJdbcJavaType().wrap( arrayList, options ); + column++; + if ( string.charAt( i ) == ')' ) { + // Return the end position if this is the last element + assert column == values.length; + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + start = i + 1; + } + break; + } + } + + throw new IllegalArgumentException( "Struct not properly formed: " + string.substring( start ) ); + } + + private boolean isBinary(int column) { + return isBinary( getJdbcValueSelectable( column ).getJdbcMapping() ); + } + + private static boolean isBinary(JdbcMapping jdbcMapping) { + switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.BINARY: + case SqlTypes.VARBINARY: + case SqlTypes.LONGVARBINARY: + case SqlTypes.LONG32VARBINARY: + return true; + } + return false; + } + + private int deserializeArray( + String string, + int begin, + int quotes, + ArrayList values, + BasicType elementType, + boolean returnEmbeddable, + WrapperOptions options) throws SQLException { + boolean inQuote = false; + StringBuilder escapingSb = null; + assert string.charAt( begin ) == '{'; + int start = begin + 1; + for ( int i = start; i < string.length(); i++ ) { + final char c = string.charAt( i ); + switch ( c ) { + case '\\': + if ( inQuote ) { + final int expectedQuoteCount = 1 << quotes; + if ( repeatsChar( string, i, expectedQuoteCount, '\\' ) ) { + if ( isDoubleQuote( string, i + expectedQuoteCount, expectedQuoteCount ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '"' ); + // Move forward to the last quote + i += expectedQuoteCount + expectedQuoteCount - 1; + start = i + 1; + continue; + } + else { + assert repeatsChar( string, i + expectedQuoteCount, expectedQuoteCount, '\\' ); + // Don't create an escaping string builder for binary literals + if ( i != start || !isBinary( elementType ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '\\' ); + start = i + expectedQuoteCount + expectedQuoteCount; + } + // Move forward to the last backslash + i += expectedQuoteCount + expectedQuoteCount - 1; + continue; + } + } + } + // Fall-through since a backslash is an escaping mechanism for a start quote within arrays + case '"': + if ( inQuote ) { + if ( isDoubleQuote( string, i, 1 << ( quotes + 1 ) ) ) { + // Skip quote escaping as that will be unescaped later + if ( escapingSb == null ) { + escapingSb = new StringBuilder(); + } + escapingSb.append( string, start, i ); + escapingSb.append( '"' ); + // Move forward to the last quote + i += ( 1 << ( quotes + 1 ) ) - 1; + start = i + 1; + continue; + } + assert isDoubleQuote( string, i, 1 << quotes ); + switch ( elementType.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.DATE: + values.add( + fromRawObject( + elementType, + parseDate( + CharSequenceHelper.subSequence( + string, + start, + i + ) + ), + options + ) + ); + break; + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + values.add( + fromRawObject( + elementType, + parseTime( + CharSequenceHelper.subSequence( + string, + start, + i + ) + ), + options + ) + ); + break; + case SqlTypes.TIMESTAMP: + values.add( + fromRawObject( + elementType, + parseTimestamp( + CharSequenceHelper.subSequence( + string, + start, + i + ), + elementType.getJdbcJavaType() + ), + options + ) + ); + break; + case SqlTypes.TIMESTAMP_WITH_TIMEZONE: + case SqlTypes.TIMESTAMP_UTC: + values.add( + fromRawObject( + elementType, + parseTimestampWithTimeZone( + CharSequenceHelper.subSequence( + string, + start, + i + ), + elementType.getJdbcJavaType() + ), + options + ) + ); + break; + case SqlTypes.BINARY: + case SqlTypes.VARBINARY: + case SqlTypes.LONGVARBINARY: + case SqlTypes.LONG32VARBINARY: + final int backslashes = 1 << ( quotes + 1 ); + assert repeatsChar( string, start, backslashes, '\\' ); + final int xCharPosition = start + backslashes; + assert string.charAt( xCharPosition ) == 'x'; + values.add( + fromString( + elementType, + string, + xCharPosition + 1, + i + ) + ); + break; + default: + if ( escapingSb == null || escapingSb.length() == 0 ) { + values.add( + fromString( + elementType, + string, + start, + i + ) + ); + } + else { + escapingSb.append( string, start, i ); + values.add( + fromString( + elementType, + escapingSb, + 0, + escapingSb.length() + ) + ); + escapingSb.setLength( 0 ); + } + break; + } + inQuote = false; + // move forward the index by 2 ^ quotes to point to the next char after the quote + i += 1 << quotes; + if ( string.charAt( i ) == '}' ) { + // Return the end position if this is the last element + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + } + else { + // This is a start quote, so move forward the index to the last quote + final int expectedQuotes = 1 << quotes; + assert isDoubleQuote( string, i, expectedQuotes ); + i += expectedQuotes - 1; + if ( string.charAt( i + 1 ) == '(' ) { + // This could be a nested struct + if ( elementType.getJdbcType() instanceof AbstractGaussDBStructJdbcType structJdbcType ) { + final Object[] subValues = new Object[structJdbcType.embeddableMappingType.getJdbcValueCount()]; + final int subEnd = structJdbcType.deserializeStruct( + string, + i + 1, + quotes + 1, + subValues, + returnEmbeddable, + options + ); + if ( returnEmbeddable ) { + final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( + structJdbcType.embeddableMappingType, + structJdbcType.orderMapping, + subValues, + options + ); + values.add( instantiate( structJdbcType.embeddableMappingType, attributeValues ) ); + } + else { + if ( structJdbcType.inverseOrderMapping != null ) { + StructHelper.orderJdbcValues( + structJdbcType.embeddableMappingType, + structJdbcType.inverseOrderMapping, + subValues.clone(), + subValues + ); + } + values.add( subValues ); + } + // The subEnd points to the first character after the '}', + // so move forward the index to point to the next char after quotes + assert isDoubleQuote( string, subEnd, expectedQuotes ); + i = subEnd + expectedQuotes; + if ( string.charAt( i ) == '}' ) { + // Return the end position if this is the last element + return i + 1; + } + // at this point, we must see a comma to indicate the next element + assert string.charAt( i ) == ','; + } + else { + inQuote = true; + } + } + else { + inQuote = true; + } + } + start = i + 1; + switch ( elementType.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.BINARY: + case SqlTypes.VARBINARY: + case SqlTypes.LONGVARBINARY: + case SqlTypes.LONG32VARBINARY: + // Skip past the backslashes in the binary literal, this will be handled later + final int backslashes = 1 << ( quotes + 1 ); + assert repeatsChar( string, start, backslashes, '\\' ); + i += backslashes; + break; + } + break; + case ',': + if ( !inQuote ) { + if ( start == i ) { + values.add( null ); + } + else { + if ( elementType.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { + values.add( + fromRawObject( + elementType, + string.charAt( start ) == 't', + options + ) + ); + } + else if ( elementType.getJavaTypeDescriptor().getJavaTypeClass().isEnum() + && elementType.getJdbcType().isInteger() ) { + values.add( + fromRawObject( + elementType, + IntegerJavaType.INSTANCE.fromEncodedString( string, start, i ), + options + ) + ); + } + else { + values.add( + fromString( + elementType, + string, + start, + i + ) + ); + } + } + start = i + 1; + } + break; + case '}': + if ( !inQuote ) { + if ( start == i ) { + values.add( null ); + } + else { + if ( elementType.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { + values.add( + fromRawObject( + elementType, + string.charAt( start ) == 't', + options + ) + ); + } + else if ( elementType.getJavaTypeDescriptor().getJavaTypeClass().isEnum() + && elementType.getJdbcType().isInteger() ) { + values.add( + fromRawObject( + elementType, + IntegerJavaType.INSTANCE.fromEncodedString( string, start, i ), + options + ) + ); + } + else { + values.add( + fromString( + elementType, + string, + start, + i + ) + ); + } + } + return i + 1; + } + break; + } + } + + throw new IllegalArgumentException( "Array not properly formed: " + string.substring( start ) ); + } + + private SelectableMapping getJdbcValueSelectable(int jdbcValueSelectableIndex) { + if ( orderMapping != null ) { + final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); + final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + int count = 0; + for ( int i = 0; i < size; i++ ) { + final ValuedModelPart modelPart = getEmbeddedPart( embeddableMappingType, orderMapping[i] ); + if ( modelPart.getMappedType() instanceof EmbeddableMappingType embeddableMappingType ) { + final SelectableMapping aggregateMapping = embeddableMappingType.getAggregateMapping(); + if ( aggregateMapping == null ) { + final SelectableMapping subSelectable = embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex - count ); + if ( subSelectable != null ) { + return subSelectable; + } + count += embeddableMappingType.getJdbcValueCount(); + } + else { + if ( count == jdbcValueSelectableIndex ) { + return aggregateMapping; + } + count++; + } + } + else { + if ( count == jdbcValueSelectableIndex ) { + return (SelectableMapping) modelPart; + } + count += modelPart.getJdbcTypeCount(); + } + } + return null; + } + return embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex ); + } + + private static boolean repeatsChar(String string, int start, int times, char expectedChar) { + final int end = start + times; + if ( end < string.length() ) { + for ( ; start < end; start++ ) { + if ( string.charAt( start ) != expectedChar ) { + return false; + } + } + return true; + } + return false; + } + + private static boolean isDoubleQuote(String string, int start, int escapes) { + if ( escapes == 1 ) { + return string.charAt( start ) == '"'; + } + assert ( escapes & 1 ) == 0 : "Only an even number of escapes allowed"; + final int end = start + escapes; + if ( end < string.length() ) { + for ( ; start < end; start += 2 ) { + final char c1 = string.charAt( start ); + final char c2 = string.charAt( start + 1 ); + switch ( c1 ) { + case '\\': + // After a backslash, another backslash or a double quote may follow + if ( c2 != '\\' && c2 != '"' ) { + return false; + } + break; + case '"': + // After a double quote, only another double quote may follow + if ( c2 != '"' ) { + return false; + } + break; + default: + return false; + } + } + return string.charAt( end - 1 ) == '"'; + } + return false; + } + + private Object fromString( + int selectableIndex, + String string, + int start, + int end) { + return fromString( + getJdbcValueSelectable( selectableIndex ).getJdbcMapping(), + string, + start, + end + ); + } + + private static Object fromString(JdbcMapping jdbcMapping, CharSequence charSequence, int start, int end) { + return jdbcMapping.getJdbcJavaType().fromEncodedString( + charSequence, + start, + end + ); + } + + private static Object fromRawObject(JdbcMapping jdbcMapping, Object raw, WrapperOptions options) { + return jdbcMapping.getJdbcJavaType().wrap( + raw, + options + ); + } + + private Object parseDate(CharSequence subSequence) { + return LOCAL_DATE.parse( subSequence, LocalDate::from ); + } + + private Object parseTime(CharSequence subSequence) { + return DateTimeFormatter.ISO_LOCAL_TIME.parse( subSequence, LocalTime::from ); + } + + private Object parseTimestamp(CharSequence subSequence, JavaType jdbcJavaType) { + final TemporalAccessor temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); + final LocalDateTime localDateTime = LocalDateTime.from( temporalAccessor ); + final Timestamp timestamp = Timestamp.valueOf( localDateTime ); + timestamp.setNanos( temporalAccessor.get( ChronoField.NANO_OF_SECOND ) ); + return timestamp; + } + + private Object parseTimestampWithTimeZone(CharSequence subSequence, JavaType jdbcJavaType) { + final TemporalAccessor temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); + if ( temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { + if ( jdbcJavaType.getJavaTypeClass() == Instant.class ) { + return Instant.from( temporalAccessor ); + } + else { + return OffsetDateTime.from( temporalAccessor ); + } + } + return LocalDateTime.from( temporalAccessor ); + } + + private static String unescape(CharSequence string, int start, int end) { + StringBuilder sb = new StringBuilder( end - start ); + for ( int i = start; i < end; i++ ) { + final char c = string.charAt( i ); + if ( c == '\\' || c == '"' ) { + i++; + sb.append( string.charAt( i ) ); + continue; + } + sb.append( c ); + } + return sb.toString(); + } + + @Override + public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { + assert embeddableMappingType != null; + final StringBuilder sb = new StringBuilder(); + serializeStructTo( new PostgreSQLAppender( sb ), domainValue, options ); + return sb.toString(); + } + + @Override + public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException { + assert embeddableMappingType != null; + final Object[] array = new Object[embeddableMappingType.getJdbcValueCount()]; + deserializeStruct( getRawStructFromJdbcValue( rawJdbcValue ), 0, 0, array, true, options ); + if ( inverseOrderMapping != null ) { + StructHelper.orderJdbcValues( embeddableMappingType, inverseOrderMapping, array.clone(), array ); + } + return array; + } + + protected String getRawStructFromJdbcValue(Object rawJdbcValue) { + return rawJdbcValue.toString(); + } + + protected String toString(X value, JavaType javaType, WrapperOptions options) throws SQLException { + if ( value == null ) { + return null; + } + final StringBuilder sb = new StringBuilder(); + serializeStructTo( new PostgreSQLAppender( sb ), value, options ); + return sb.toString(); + } + + private void serializeStructTo(PostgreSQLAppender appender, Object value, WrapperOptions options) throws SQLException { + serializeDomainValueTo( appender, options, value, '(' ); + appender.append( ')' ); + } + + private void serializeDomainValueTo( + PostgreSQLAppender appender, + WrapperOptions options, + Object domainValue, + char separator) throws SQLException { + serializeJdbcValuesTo( + appender, + options, + StructHelper.getJdbcValues( embeddableMappingType, orderMapping, domainValue, options ), + separator + ); + } + + private void serializeJdbcValuesTo( + PostgreSQLAppender appender, + WrapperOptions options, + Object[] jdbcValues, + char separator) throws SQLException { + for ( int i = 0; i < jdbcValues.length; i++ ) { + appender.append( separator ); + separator = ','; + final Object jdbcValue = jdbcValues[i]; + if ( jdbcValue == null ) { + continue; + } + final SelectableMapping selectableMapping = orderMapping == null ? + embeddableMappingType.getJdbcValueSelectable( i ) : + embeddableMappingType.getJdbcValueSelectable( orderMapping[i] ); + final JdbcMapping jdbcMapping = selectableMapping.getJdbcMapping(); + if ( jdbcMapping.getJdbcType() instanceof AbstractGaussDBStructJdbcType structJdbcType ) { + appender.quoteStart(); + structJdbcType.serializeJdbcValuesTo( + appender, + options, + (Object[]) jdbcValue, + '(' + ); + appender.append( ')' ); + appender.quoteEnd(); + } + else { + serializeConvertedBasicTo( appender, options, jdbcMapping, jdbcValue ); + } + } + } + + private void serializeConvertedBasicTo( + PostgreSQLAppender appender, + WrapperOptions options, + JdbcMapping jdbcMapping, + Object subValue) throws SQLException { + //noinspection unchecked + final JavaType jdbcJavaType = (JavaType) jdbcMapping.getJdbcJavaType(); + switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.TINYINT: + case SqlTypes.SMALLINT: + case SqlTypes.INTEGER: + if ( subValue instanceof Boolean booleanValue ) { + // BooleanJavaType has this as an implicit conversion + appender.append( booleanValue ? '1' : '0' ); + break; + } + if ( subValue instanceof Enum enumValue ) { + appender.appendSql( enumValue.ordinal() ); + break; + } + case SqlTypes.BOOLEAN: + case SqlTypes.BIT: + case SqlTypes.BIGINT: + case SqlTypes.FLOAT: + case SqlTypes.REAL: + case SqlTypes.DOUBLE: + case SqlTypes.DECIMAL: + case SqlTypes.NUMERIC: + case SqlTypes.DURATION: + appender.append( subValue.toString() ); + break; + case SqlTypes.CHAR: + case SqlTypes.NCHAR: + case SqlTypes.VARCHAR: + case SqlTypes.NVARCHAR: + if ( subValue instanceof Boolean booleanValue ) { + // BooleanJavaType has this as an implicit conversion + appender.append( booleanValue ? 'Y' : 'N' ); + break; + } + case SqlTypes.LONGVARCHAR: + case SqlTypes.LONGNVARCHAR: + case SqlTypes.LONG32VARCHAR: + case SqlTypes.LONG32NVARCHAR: + case SqlTypes.ENUM: + case SqlTypes.NAMED_ENUM: + appender.quoteStart(); + appender.append( (String) subValue ); + appender.quoteEnd(); + break; + case SqlTypes.DATE: + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + case SqlTypes.TIMESTAMP: + case SqlTypes.TIMESTAMP_WITH_TIMEZONE: + case SqlTypes.TIMESTAMP_UTC: + appendTemporal( appender, jdbcMapping, subValue, options ); + break; + case SqlTypes.BINARY: + case SqlTypes.VARBINARY: + case SqlTypes.LONGVARBINARY: + case SqlTypes.LONG32VARBINARY: + final byte[] bytes = jdbcJavaType.unwrap( + subValue, + byte[].class, + options + ); + appender.ensureCanFit( appender.quote + 1 + ( bytes.length << 1 ) ); + appender.append( '\\' ); + appender.append( '\\' ); + appender.append( 'x' ); + PrimitiveByteArrayJavaType.INSTANCE.appendString( + appender, + bytes + ); + break; + case SqlTypes.UUID: + appender.append( subValue.toString() ); + break; + case SqlTypes.ARRAY: + if ( subValue != null ) { + final int length = Array.getLength( subValue ); + if ( length == 0 ) { + appender.append( "{}" ); + } + else { + //noinspection unchecked + final BasicType elementType = ((BasicPluralType) jdbcMapping).getElementType(); + appender.quoteStart(); + appender.append( '{' ); + Object arrayElement = Array.get( subValue, 0 ); + if ( arrayElement == null ) { + appender.appendNull(); + } + else { + serializeConvertedBasicTo( appender, options, elementType, arrayElement ); + } + for ( int i = 1; i < length; i++ ) { + arrayElement = Array.get( subValue, i ); + appender.append( ',' ); + if ( arrayElement == null ) { + appender.appendNull(); + } + else { + serializeConvertedBasicTo( appender, options, elementType, arrayElement ); + } + } + + appender.append( '}' ); + appender.quoteEnd(); + } + } + break; + case SqlTypes.STRUCT: + if ( subValue != null ) { + final AbstractGaussDBStructJdbcType structJdbcType = (AbstractGaussDBStructJdbcType) jdbcMapping.getJdbcType(); + appender.quoteStart(); + structJdbcType.serializeJdbcValuesTo( appender, options, (Object[]) subValue, '(' ); + appender.append( ')' ); + appender.quoteEnd(); + } + break; + default: + throw new UnsupportedOperationException( "Unsupported JdbcType nested in struct: " + jdbcMapping.getJdbcType() ); + } + } + + private StructAttributeValues getAttributeValues( + EmbeddableMappingType embeddableMappingType, + int[] orderMapping, + Object[] rawJdbcValues, + WrapperOptions options) throws SQLException { + final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); + final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + final StructAttributeValues attributeValues = new StructAttributeValues( + numberOfAttributeMappings, + orderMapping != null ? + null : + rawJdbcValues + ); + int jdbcIndex = 0; + for ( int i = 0; i < size; i++ ) { + final int attributeIndex; + if ( orderMapping == null ) { + attributeIndex = i; + } + else { + attributeIndex = orderMapping[i]; + } + jdbcIndex += injectAttributeValue( + getEmbeddedPart( embeddableMappingType, attributeIndex ), + attributeValues, + attributeIndex, + rawJdbcValues, + jdbcIndex, + options + ); + } + return attributeValues; + } + + private int injectAttributeValue( + ValuedModelPart modelPart, + StructAttributeValues attributeValues, + int attributeIndex, + Object[] rawJdbcValues, + int jdbcIndex, + WrapperOptions options) throws SQLException { + final MappingType mappedType = modelPart.getMappedType(); + final int jdbcValueCount; + final Object rawJdbcValue = rawJdbcValues[jdbcIndex]; + if ( mappedType instanceof EmbeddableMappingType embeddableMappingType ) { + if ( embeddableMappingType.getAggregateMapping() != null ) { + jdbcValueCount = 1; + attributeValues.setAttributeValue( attributeIndex, rawJdbcValue ); + } + else { + jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + final Object[] subJdbcValues = new Object[jdbcValueCount]; + System.arraycopy( rawJdbcValues, jdbcIndex, subJdbcValues, 0, subJdbcValues.length ); + final StructAttributeValues subValues = getAttributeValues( + embeddableMappingType, + null, + subJdbcValues, + options + ); + attributeValues.setAttributeValue( attributeIndex, instantiate( embeddableMappingType, subValues ) ); + } + } + else { + assert modelPart.getJdbcTypeCount() == 1; + jdbcValueCount = 1; + final JdbcMapping jdbcMapping = modelPart.getSingleJdbcMapping(); + final Object jdbcValue = jdbcMapping.getJdbcJavaType().wrap( + rawJdbcValue, + options + ); + attributeValues.setAttributeValue( attributeIndex, jdbcMapping.convertToDomainValue( jdbcValue ) ); + } + return jdbcValueCount; + } + + private void appendTemporal(SqlAppender appender, JdbcMapping jdbcMapping, Object value, WrapperOptions options) { + final TimeZone jdbcTimeZone = getJdbcTimeZone( options ); + //noinspection unchecked + final JavaType javaType = (JavaType) jdbcMapping.getJdbcJavaType(); + appender.append( '"' ); + switch ( jdbcMapping.getJdbcType().getJdbcTypeCode() ) { + case SqlTypes.DATE: + if ( value instanceof java.util.Date date ) { + appendAsDate( appender, date ); + } + else if ( value instanceof java.util.Calendar calendar ) { + appendAsDate( appender, calendar ); + } + else if ( value instanceof TemporalAccessor temporalAccessor ) { + appendAsDate( appender, temporalAccessor ); + } + else { + appendAsDate( + appender, + javaType.unwrap( value, java.util.Date.class, options ) + ); + } + break; + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + if ( value instanceof java.util.Date date ) { + appendAsTime( appender, date, jdbcTimeZone ); + } + else if ( value instanceof java.util.Calendar calendar ) { + appendAsTime( appender, calendar, jdbcTimeZone ); + } + else if ( value instanceof TemporalAccessor temporalAccessor ) { + if ( temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { + appendAsTime( appender, temporalAccessor, true, jdbcTimeZone ); + } + else { + appendAsLocalTime( appender, temporalAccessor ); + } + } + else { + appendAsTime( + appender, + javaType.unwrap( value, java.sql.Time.class, options ), + jdbcTimeZone + ); + } + break; + case SqlTypes.TIMESTAMP: + case SqlTypes.TIMESTAMP_WITH_TIMEZONE: + case SqlTypes.TIMESTAMP_UTC: + if ( value instanceof java.util.Date date ) { + appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + } + else if ( value instanceof java.util.Calendar calendar ) { + appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + } + else if ( value instanceof TemporalAccessor temporalAccessor ) { + appendAsTimestampWithMicros( appender, temporalAccessor, temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ), jdbcTimeZone ); + } + else { + appendAsTimestampWithMicros( + appender, + javaType.unwrap( value, java.util.Date.class, options ), + jdbcTimeZone + ); + } + break; + default: + throw new IllegalArgumentException(); + } + + appender.append( '"' ); + } + private static TimeZone getJdbcTimeZone(WrapperOptions options) { + return options == null || options.getJdbcTimeZone() == null + ? TimeZone.getDefault() + : options.getJdbcTimeZone(); + } + + protected Object getBindValue(X value, WrapperOptions options) throws SQLException { + return StructHelper.getJdbcValues( embeddableMappingType, orderMapping, value, options ); + } + + private static class PostgreSQLAppender extends StringBuilderSqlAppender { + + private int quote = 1; + + public PostgreSQLAppender(StringBuilder sb) { + super( sb ); + } + + public void quoteStart() { + append( '"' ); + quote = quote << 1; + } + + public void quoteEnd() { + quote = quote >> 1; + append( '"' ); + } + + public void appendNull() { + sb.append( "NULL" ); + } + + @Override + public PostgreSQLAppender append(char fragment) { + if ( quote != 1 ) { + appendWithQuote( fragment ); + } + else { + sb.append( fragment ); + } + return this; + } + + @Override + public PostgreSQLAppender append(CharSequence csq) { + return append( csq, 0, csq.length() ); + } + + @Override + public PostgreSQLAppender append(CharSequence csq, int start, int end) { + if ( quote != 1 ) { + int len = end - start; + sb.ensureCapacity( sb.length() + len ); + for ( int i = start; i < end; i++ ) { + appendWithQuote( csq.charAt( i ) ); + } + } + else { + sb.append( csq, start, end ); + } + return this; + } + + private void appendWithQuote(char fragment) { + if ( fragment == '"' || fragment == '\\' ) { + sb.ensureCapacity( sb.length() + quote ); + for ( int i = 1; i < quote; i++ ) { + sb.append( '\\' ); + } + } + sb.append( fragment ); + } + + public void ensureCanFit(int lengthIncrease) { + sb.ensureCapacity( sb.length() + lengthIncrease ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java index cb24d2ea2839..3f727ac4c82f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java @@ -79,6 +79,21 @@ public String getUrlPrefix() { } }, + GAUSSDB { + @Override + public Dialect createDialect(DialectResolutionInfo info) { + return new GaussDBDialect(info); + } + @Override + public boolean productNameMatches(String databaseName) { + return "GaussDB".equals( databaseName ); + } + @Override + public String getDriverClassName(String jdbcUrl) { + return "com.huawei.gaussdb.jdbc.Driver"; + } + }, + H2 { @Override public Dialect createDialect(DialectResolutionInfo info) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcType.java new file mode 100644 index 000000000000..c6f531ec3f7a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcType.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +/** + * Descriptor for {@link Types#ARRAY ARRAY} handling. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayJdbcType. + */ +public class GaussDBArrayJdbcType extends ArrayJdbcType { + + public GaussDBArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); + } + + @Override + public ValueBinder getBinder(final JavaType javaTypeDescriptor) { + @SuppressWarnings("unchecked") + final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaTypeDescriptor; + final ValueBinder elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); + return new BasicBinder<>( javaTypeDescriptor, this ) { + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setArray( index, getArray( value, options ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final java.sql.Array arr = getArray( value, options ); + try { + st.setObject( name, arr, java.sql.Types.ARRAY ); + } + catch (SQLException ex) { + throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); + } + } + + @Override + public Object getBindValue(X value, WrapperOptions options) throws SQLException { + return ( (GaussDBArrayJdbcType) getJdbcType() ).getArray( this, elementBinder, value, options ); + } + + private java.sql.Array getArray(X value, WrapperOptions options) throws SQLException { + final GaussDBArrayJdbcType arrayJdbcType = (GaussDBArrayJdbcType) getJdbcType(); + final Object[] objects; + + final JdbcType elementJdbcType = arrayJdbcType.getElementJdbcType(); + if ( elementJdbcType instanceof AggregateJdbcType ) { + // The GaussDB JDBC driver does not support arrays of structs, which contain byte[] + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) elementJdbcType; + final Object[] domainObjects = getJavaType().unwrap( + value, + Object[].class, + options + ); + objects = new Object[domainObjects.length]; + for ( int i = 0; i < domainObjects.length; i++ ) { + if ( domainObjects[i] != null ) { + objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); + } + } + } + else { + objects = arrayJdbcType.getArray( this, elementBinder, value, options ); + } + + final SharedSessionContractImplementor session = options.getSession(); + final String typeName = arrayJdbcType.getElementTypeName( getJavaType(), session ); + return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() + .createArrayOf( typeName, objects ); + } + }; + } + + @Override + public String toString() { + return "GaussDBArrayTypeDescriptor(" + getElementJdbcType().toString() + ")"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcTypeConstructor.java new file mode 100644 index 000000000000..b37553dc3657 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBArrayJdbcTypeConstructor.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.sql.Types; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link GaussDBArrayJdbcType}. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayJdbcTypeConstructor. + */ +public class GaussDBArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final GaussDBArrayJdbcTypeConstructor INSTANCE = new GaussDBArrayJdbcTypeConstructor(); + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new GaussDBArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return Types.ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingInetJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingInetJdbcType.java new file mode 100644 index 000000000000..f76ecd18bfc9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingInetJdbcType.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.net.InetAddress; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLCastingInetJdbcType. + */ +public class GaussDBCastingInetJdbcType implements JdbcType { + + public static final GaussDBCastingInetJdbcType INSTANCE = new GaussDBCastingInetJdbcType(); + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as inet)" ); + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.VARBINARY; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.INET; + } + + @Override + public String toString() { + return "InetSecondJdbcType"; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + // No literal support for now + return null; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setString( index, getStringValue( value, options ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setString( name, getStringValue( value, options ) ); + } + + private String getStringValue(X value, WrapperOptions options) { + return getJavaType().unwrap( value, InetAddress.class, options ).getHostAddress(); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getString( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getObject( statement.getString( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return getObject( statement.getString( name ), options ); + } + + private X getObject(String inetString, WrapperOptions options) throws SQLException { + if ( inetString == null ) { + return null; + } + return getJavaType().wrap( inetString, options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingIntervalSecondJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingIntervalSecondJdbcType.java new file mode 100644 index 000000000000..d64a82cfe8d3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingIntervalSecondJdbcType.java @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.math.BigDecimal; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AdjustableJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLCastingIntervalSecondJdbcType. + */ +public class GaussDBCastingIntervalSecondJdbcType implements AdjustableJdbcType { + + public static final GaussDBCastingIntervalSecondJdbcType INSTANCE = new GaussDBCastingIntervalSecondJdbcType(); + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + final int scale = indicators.getColumnScale() == JdbcTypeIndicators.NO_COLUMN_SCALE + ? domainJtd.getDefaultSqlScale( indicators.getDialect(), this ) + : indicators.getColumnScale(); + if ( scale > 6 ) { + // Since the maximum allowed scale on GaussDB is 6 (microsecond precision), + // we have to switch to the numeric type if the value is greater + return indicators.getTypeConfiguration().getJdbcTypeRegistry().getDescriptor( SqlTypes.NUMERIC ); + } + else { + return this; + } + } + + @Override + public Expression wrapTopLevelSelectionExpression(Expression expression) { + return new SelfRenderingExpression() { + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + sqlAppender.append( "extract(epoch from " ); + expression.accept( walker ); + sqlAppender.append( ')' ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return expression.getExpressionType(); + } + }; + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( '(' ); + appender.append( writeExpression ); + appender.append( "*interval'1 second')" ); + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.NUMERIC; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.INTERVAL_SECOND; + } + + @Override + public String toString() { + return "IntervalSecondJdbcType"; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + // No literal support for now + return null; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setBigDecimal( index, getBigDecimalValue( value, options ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setBigDecimal( name, getBigDecimalValue( value, options ) ); + } + + private BigDecimal getBigDecimalValue(X value, WrapperOptions options) { + return getJavaType().unwrap( value, BigDecimal.class, options ).movePointLeft( 9 ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getBigDecimal( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getObject( statement.getBigDecimal( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return getObject( statement.getBigDecimal( name ), options ); + } + + private X getObject(BigDecimal bigDecimal, WrapperOptions options) throws SQLException { + if ( bigDecimal == null ) { + return null; + } + return getJavaType().wrap( bigDecimal.movePointRight( 9 ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcType.java new file mode 100644 index 000000000000..dc7e855a1eac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcType.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLCastingJsonArrayJdbcType. + */ +public class GaussDBCastingJsonArrayJdbcType extends JsonArrayJdbcType { + + private final boolean jsonb; + + public GaussDBCastingJsonArrayJdbcType(JdbcType elementJdbcType, boolean jsonb) { + super( elementJdbcType ); + this.jsonb = jsonb; + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as " ); + if ( jsonb ) { + appender.append( "jsonb)" ); + } + else { + appender.append( "json)" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcTypeConstructor.java new file mode 100644 index 000000000000..e0ec3c638ec4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonArrayJdbcTypeConstructor.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link GaussDBCastingJsonArrayJdbcType}. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLCastingJsonArrayJdbcTypeConstructor. + */ +public class GaussDBCastingJsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final GaussDBCastingJsonArrayJdbcTypeConstructor JSONB_INSTANCE = new GaussDBCastingJsonArrayJdbcTypeConstructor( true ); + public static final GaussDBCastingJsonArrayJdbcTypeConstructor JSON_INSTANCE = new GaussDBCastingJsonArrayJdbcTypeConstructor( false ); + + private final boolean jsonb; + + public GaussDBCastingJsonArrayJdbcTypeConstructor(boolean jsonb) { + this.jsonb = jsonb; + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new GaussDBCastingJsonArrayJdbcType( elementType, jsonb ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonJdbcType.java new file mode 100644 index 000000000000..00f22fe669c2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBCastingJsonJdbcType.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLCastingJsonJdbcType. + */ +public class GaussDBCastingJsonJdbcType extends JsonJdbcType { + + public static final GaussDBCastingJsonJdbcType JSON_INSTANCE = new GaussDBCastingJsonJdbcType( false, null ); + public static final GaussDBCastingJsonJdbcType JSONB_INSTANCE = new GaussDBCastingJsonJdbcType( true, null ); + + private final boolean jsonb; + + public GaussDBCastingJsonJdbcType(boolean jsonb, EmbeddableMappingType embeddableMappingType) { + super( embeddableMappingType ); + this.jsonb = jsonb; + } + + @Override + public int getDdlTypeCode() { + return SqlTypes.JSON; + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new GaussDBCastingJsonJdbcType( jsonb, mappingType ); + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as " ); + if ( jsonb ) { + appender.append( "jsonb)" ); + } + else { + appender.append( "json)" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBDialect.java new file mode 100644 index 000000000000..bac224e9688e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBDialect.java @@ -0,0 +1,1451 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import jakarta.persistence.GenerationType; +import jakarta.persistence.TemporalType; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Length; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.PessimisticLockException; +import org.hibernate.QueryTimeoutException; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.dialect.aggregate.AggregateSupport; +import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.GaussDBMinMaxFunction; +import org.hibernate.dialect.function.GaussDBTruncFunction; +import org.hibernate.dialect.function.GaussDBTruncRoundFunction; +import org.hibernate.dialect.identity.GaussDBIdentityColumnSupport; +import org.hibernate.dialect.identity.IdentityColumnSupport; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.pagination.LimitLimitHandler; +import org.hibernate.dialect.sequence.GaussDBSequenceSupport; +import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.dialect.unique.CreateTableUniqueDelegate; +import org.hibernate.dialect.unique.UniqueDelegate; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; +import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.Table; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.procedure.internal.GaussDBCallableStatementSupport; +import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.query.SemanticException; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.CastType; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.internal.StandardTableExporter; +import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.JavaObjectType; +import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; +import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; +import org.hibernate.type.descriptor.jdbc.XmlJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.NamedNativeEnumDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.NamedNativeOrdinalEnumDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.Scale6IntervalSecondDdlType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import org.hibernate.type.spi.TypeConfiguration; + +import java.sql.CallableStatement; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.query.common.TemporalUnit.DAY; +import static org.hibernate.query.common.TemporalUnit.EPOCH; +import static org.hibernate.type.SqlTypes.ARRAY; +import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.CHAR; +import static org.hibernate.type.SqlTypes.FLOAT; +import static org.hibernate.type.SqlTypes.GEOGRAPHY; +import static org.hibernate.type.SqlTypes.GEOMETRY; +import static org.hibernate.type.SqlTypes.INET; +import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; +import static org.hibernate.type.SqlTypes.LONG32VARBINARY; +import static org.hibernate.type.SqlTypes.LONG32VARCHAR; +import static org.hibernate.type.SqlTypes.NCHAR; +import static org.hibernate.type.SqlTypes.NCLOB; +import static org.hibernate.type.SqlTypes.NVARCHAR; +import static org.hibernate.type.SqlTypes.OTHER; +import static org.hibernate.type.SqlTypes.SQLXML; +import static org.hibernate.type.SqlTypes.STRUCT; +import static org.hibernate.type.SqlTypes.TIME; +import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; +import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.UUID; +import static org.hibernate.type.SqlTypes.VARBINARY; +import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; + +/** + * A {@linkplain Dialect SQL dialect} for GaussDB V2.0-8.201 and above. + *

+ * Please refer to the + * GaussDB documentation. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLDialect. + */ +public class GaussDBDialect extends Dialect { + protected final static DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 2 ); + + private final UniqueDelegate uniqueDelegate = new CreateTableUniqueDelegate(this); + private final StandardTableExporter gaussDBTableExporter = new StandardTableExporter( this ) { + @Override + protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) { + final JdbcType jdbcType = aggregateColumn.getType().getJdbcType(); + if ( jdbcType.isXml() ) { + // Requires the use of xmltable which is not supported in check constraints + return; + } + super.applyAggregateColumnCheck( buf, aggregateColumn ); + } + }; + + private final OptionalTableUpdateStrategy optionalTableUpdateStrategy; + + public GaussDBDialect() { + this(MINIMUM_VERSION); + } + + public GaussDBDialect(DialectResolutionInfo info) { + this( info.makeCopyOrDefault( MINIMUM_VERSION )); + registerKeywords( info ); + } + + public GaussDBDialect(DatabaseVersion version) { + super( version ); + this.optionalTableUpdateStrategy = determineOptionalTableUpdateStrategy( version ); + } + + @Override + public boolean supportsColumnCheck() { + return false; + } + + private static OptionalTableUpdateStrategy determineOptionalTableUpdateStrategy(DatabaseVersion version) { + return version.isSameOrAfter( DatabaseVersion.make( 15, 0 ) ) + ? GaussDBDialect::usingMerge + : GaussDBDialect::withoutMerge; + } + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } + + @Override + public boolean getDefaultNonContextualLobCreation() { + return true; + } + + @Override + protected String columnType(int sqlTypeCode) { + return switch (sqlTypeCode) { + // no tinyint + case TINYINT -> "smallint"; + + // there are no nchar/nvarchar types + case NCHAR -> columnType( CHAR ); + case NVARCHAR -> columnType( VARCHAR ); + + case LONG32VARCHAR, LONG32NVARCHAR -> "text"; + case NCLOB -> "clob"; + + case BINARY, VARBINARY, LONG32VARBINARY -> "bytea"; + + case TIMESTAMP_UTC -> columnType( TIMESTAMP_WITH_TIMEZONE ); + + default -> super.columnType( sqlTypeCode ); + }; + } + + @Override + protected String castType(int sqlTypeCode) { + return switch (sqlTypeCode) { + case CHAR, NCHAR, VARCHAR, NVARCHAR -> "varchar"; + case LONG32VARCHAR, LONG32NVARCHAR -> "text"; + case NCLOB -> "clob"; + case BINARY, VARBINARY, LONG32VARBINARY -> "bytea"; + default -> super.castType( sqlTypeCode ); + }; + } + + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes( typeContributions, serviceRegistry ); + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + + // We need to configure that the array type uses the raw element type for casts + ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, true ) ); + + // Register this type to be able to support Float[] + // The issue is that the JDBC driver can't handle createArrayOf( "float(24)", ... ) + // It requires the use of "real" or "float4" + // Alternatively we could introduce a new API in Dialect for creating such base names + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( FLOAT, columnType( FLOAT ), castType( FLOAT ), this ) + .withTypeCapacity( 24, "float4" ) + .build() + ); + + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( SQLXML, "xml", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "uuid", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "geometry", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOGRAPHY, "geography", this ) ); + ddlTypeRegistry.addDescriptor( new Scale6IntervalSecondDdlType( this ) ); + + // Prefer jsonb if possible + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + + ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); + ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); + } + + @Override + public int getMaxVarcharLength() { + return 10_485_760; + } + + @Override + public int getMaxVarcharCapacity() { + // 1GB-85-4 according to GaussDB docs + return 1_073_741_727; + } + + @Override + public int getMaxVarbinaryLength() { + //has no varbinary-like type + return Length.LONG32; + } + + @Override + public int getDefaultStatementBatchSize() { + return 15; + } + + @Override + public JdbcType resolveSqlTypeDescriptor( + String columnTypeName, + int jdbcTypeCode, + int precision, + int scale, + JdbcTypeRegistry jdbcTypeRegistry) { + switch ( jdbcTypeCode ) { + case OTHER: + switch ( columnTypeName ) { + case "uuid": + jdbcTypeCode = UUID; + break; + case "json": + case "jsonb": + jdbcTypeCode = JSON; + break; + case "xml": + jdbcTypeCode = SQLXML; + break; + case "inet": + jdbcTypeCode = INET; + break; + case "geometry": + jdbcTypeCode = GEOMETRY; + break; + case "geography": + jdbcTypeCode = GEOGRAPHY; + break; + } + break; + case TIME: + // The GaussDB JDBC driver reports TIME for timetz, but we use it only for mapping OffsetTime to UTC + if ( "timetz".equals( columnTypeName ) ) { + jdbcTypeCode = TIME_UTC; + } + break; + case TIMESTAMP: + // The GaussDB JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant + if ( "timestamptz".equals( columnTypeName ) ) { + jdbcTypeCode = TIMESTAMP_UTC; + } + break; + case ARRAY: + // GaussDB names array types by prepending an underscore to the base name + if ( columnTypeName.charAt( 0 ) == '_' ) { + final String componentTypeName = columnTypeName.substring( 1 ); + final Integer sqlTypeCode = resolveSqlTypeCode( componentTypeName, jdbcTypeRegistry.getTypeConfiguration() ); + if ( sqlTypeCode != null ) { + return jdbcTypeRegistry.resolveTypeConstructorDescriptor( + jdbcTypeCode, + jdbcTypeRegistry.getDescriptor( sqlTypeCode ), + ColumnTypeInformation.EMPTY + ); + } + final SqlTypedJdbcType elementDescriptor = jdbcTypeRegistry.findSqlTypedDescriptor( componentTypeName ); + if ( elementDescriptor != null ) { + return jdbcTypeRegistry.resolveTypeConstructorDescriptor( + jdbcTypeCode, + elementDescriptor, + ColumnTypeInformation.EMPTY + ); + } + } + break; + case STRUCT: + final SqlTypedJdbcType descriptor = jdbcTypeRegistry.findSqlTypedDescriptor( + // Skip the schema + columnTypeName.substring( columnTypeName.indexOf( '.' ) + 1 ) + ); + if ( descriptor != null ) { + return descriptor; + } + break; + } + return jdbcTypeRegistry.getDescriptor( jdbcTypeCode ); + } + + @Override + protected Integer resolveSqlTypeCode(String columnTypeName, TypeConfiguration typeConfiguration) { + return switch (columnTypeName) { + case "bool" -> Types.BOOLEAN; + case "float4" -> Types.REAL; // Use REAL instead of FLOAT to get Float as recommended Java type + case "float8" -> Types.DOUBLE; + case "int2" -> Types.SMALLINT; + case "int4" -> Types.INTEGER; + case "int8" -> Types.BIGINT; + default -> super.resolveSqlTypeCode( columnTypeName, typeConfiguration ); + }; + } + + @Override + public String getEnumTypeDeclaration(String name, String[] values) { + return name; + } + + @Override + public String[] getCreateEnumTypeCommand(String name, String[] values) { + StringBuilder type = new StringBuilder(); + type.append( "create type " ) + .append( name ) + .append( " as enum (" ); + String separator = ""; + for ( String value : values ) { + type.append( separator ).append('\'').append( value ).append('\''); + separator = ","; + } + type.append( ')' ); + String cast1 = "create cast (varchar as " + + name + + ") with inout as implicit"; + String cast2 = "create cast (" + + name + + " as varchar) with inout as implicit"; + return new String[] { type.toString(), cast1, cast2 }; + } + + @Override + public String[] getDropEnumTypeCommand(String name) { + return new String[] { "drop type if exists " + name + " cascade" }; + } + + @Override + public String currentTime() { + return "localtime"; + } + + @Override + public String currentTimestamp() { + return "localtimestamp"; + } + + @Override + public String currentTimestampWithTimeZone() { + return "current_timestamp"; + } + + /** + * The {@code extract()} function returns {@link TemporalUnit#DAY_OF_WEEK} + * numbered from 0 to 6. This isn't consistent with what most other + * databases do, so here we adjust the result by generating + * {@code (extract(dow,arg)+1))}. + */ + @Override + public String extractPattern(TemporalUnit unit) { + return switch (unit) { + case DAY_OF_WEEK -> "(" + super.extractPattern( unit ) + "+1)"; + default -> super.extractPattern(unit); + }; + } + + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + + /** + * {@code microsecond} is the smallest unit for an {@code interval}, + * and the highest precision for a {@code timestamp}, so we could + * use it as the "native" precision, but it's more convenient to use + * whole seconds (with the fractional part), since we want to use + * {@code extract(epoch from ...)} in our emulation of + * {@code timestampdiff()}. + */ + @Override + public long getFractionalSecondPrecisionInNanos() { + return 1_000_000_000; //seconds + } + + @Override @SuppressWarnings("deprecation") + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + return intervalType != null + ? "(?2+?3)" + : "cast(?3+" + intervalPattern( unit ) + " as " + temporalType.name().toLowerCase() + ")"; + } + + private static String intervalPattern(TemporalUnit unit) { + return switch (unit) { + case NANOSECOND -> "(?2)/1e3*interval '1 microsecond'"; + case NATIVE -> "(?2)*interval '1 second'"; + case QUARTER -> "(?2)*interval '3 month'"; // quarter is not supported in interval literals + case WEEK -> "(?2)*interval '7 day'"; // week is not supported in interval literals + default -> "(?2)*interval '1 " + unit + "'"; + }; + } + + @Override @SuppressWarnings("deprecation") + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + if ( unit == null ) { + return "(?3-?2)"; + } + return switch (unit) { + case YEAR -> "extract(year from ?3-?2)"; + case QUARTER -> "(extract(year from ?3-?2)*4+extract(month from ?3-?2)/3)"; + case MONTH -> "(extract(year from ?3-?2)*12+extract(month from ?3-?2))"; + case WEEK -> "(extract(day from ?3-?2)/7)"; // week is not supported by extract() when the argument is a duration + case DAY -> "extract(day from ?3-?2)"; + // in order to avoid multiple calls to extract(), + // we use extract(epoch from x - y) * factor for + // all the following units: + case HOUR, MINUTE, SECOND, NANOSECOND, NATIVE -> + "extract(epoch from ?3-?2)" + EPOCH.conversionFactor( unit, this ); + default -> throw new SemanticException( "Unrecognized field: " + unit ); + }; + } + + @Override + public TimeZoneSupport getTimeZoneSupport() { + return TimeZoneSupport.NORMALIZE; + } + + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry(functionContributions); + + CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + + functionFactory.cot(); + functionFactory.radians(); + functionFactory.degrees(); + functionFactory.log(); + functionFactory.mod_operator(); + functionFactory.moreHyperbolic(); + functionFactory.cbrt(); + functionFactory.pi(); + functionFactory.log10_log(); + functionFactory.trim2(); + functionFactory.repeat(); + functionFactory.initcap(); + functionFactory.substr(); + functionFactory.substring_substr(); + //also natively supports ANSI-style substring() + functionFactory.translate(); + functionFactory.toCharNumberDateTimestamp(); + functionFactory.localtimeLocaltimestamp(); + functionFactory.bitLength_pattern( "bit_length(?1)", "length(?1)*8" ); + functionFactory.octetLength_pattern( "octet_length(?1)", "length(?1)" ); + functionFactory.ascii(); + functionFactory.char_chr(); + functionFactory.position(); + functionFactory.bitandorxornot_operator(); + functionFactory.bitAndOr(); + functionFactory.everyAny_boolAndOr(); + functionFactory.median_percentileCont( false ); + functionFactory.stddev(); + functionFactory.stddevPopSamp(); + functionFactory.variance(); + functionFactory.varPopSamp(); + functionFactory.covarPopSamp(); + functionFactory.corr(); + functionFactory.regrLinearRegressionAggregates(); + functionFactory.insert_overlay(); + functionFactory.overlay(); + functionFactory.soundex(); //was introduced apparently + functionFactory.format_toChar_gaussdb(); + + functionFactory.locate_positionSubstring(); + functionFactory.windowFunctions(); + functionFactory.listagg_stringAgg( "varchar" ); + functionFactory.array_gaussdb(); + functionFactory.arrayAggregate(); + functionFactory.arrayRemoveIndex_gaussdb(); + functionFactory.arrayConcat_gaussdb(); + functionFactory.arrayPrepend_gaussdb(); + functionFactory.arrayAppend_gaussdb(); + functionFactory.arrayContains_gaussdb(); + functionFactory.arrayIntersects_gaussdb(); + functionFactory.arrayRemove_gaussdb(); + functionFactory.arraySlice_operator(); + functionFactory.arrayReplace_gaussdb(); + functionFactory.arraySet_gaussdb(); + functionFactory.arrayFill_gaussdb(); + + functionFactory.jsonObject_gaussdb(); + + functionFactory.makeDateTimeTimestamp(); + // Note that GaussDB doesn't support the OVER clause for ordered set-aggregate functions + functionFactory.inverseDistributionOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates(); + + if ( !supportsMinMaxOnUuid() ) { + functionContributions.getFunctionRegistry().register( "min", new GaussDBMinMaxFunction( "min" ) ); + functionContributions.getFunctionRegistry().register( "max", new GaussDBMinMaxFunction( "max" ) ); + } + + // uses # instead of ^ for XOR + functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1 # ?2)" ) + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + functionContributions.getFunctionRegistry().register( + "round", new GaussDBTruncRoundFunction( "round", true ) + ); + functionContributions.getFunctionRegistry().register( + "trunc", + new GaussDBTruncFunction( true, functionContributions.getTypeConfiguration() ) + ); + functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); + functionFactory.dateTrunc(); + + functionFactory.hex( "encode(?1, 'hex')" ); + functionFactory.sha( "sha256(?1)" ); + functionFactory.md5( "decode(md5(?1), 'hex')" ); + } + + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "ordinality"; + } + + /** + * Whether GaussDB supports {@code min(uuid)}/{@code max(uuid)}, + * which it doesn't by default. Since the emulation does not perform well, + * this method may be overridden by any user who ensures that aggregate + * functions for handling uuids exist in the database. + *

+ * The following definitions can be used for this purpose: + *

+	 * create or replace function min(uuid, uuid)
+	 *     returns uuid
+	 *     immutable parallel safe
+	 *     language plpgsql as
+	 * $$
+	 * begin
+	 *     return least($1, $2);
+	 * end
+	 * $$;
+	 *
+	 * create aggregate min(uuid) (
+	 *     sfunc = min,
+	 *     stype = uuid,
+	 *     combinefunc = min,
+	 *     parallel = safe,
+	 *     sortop = operator (<)
+	 *     );
+	 *
+	 * create or replace function max(uuid, uuid)
+	 *     returns uuid
+	 *     immutable parallel safe
+	 *     language plpgsql as
+	 * $$
+	 * begin
+	 *     return greatest($1, $2);
+	 * end
+	 * $$;
+	 *
+	 * create aggregate max(uuid) (
+	 *     sfunc = max,
+	 *     stype = uuid,
+	 *     combinefunc = max,
+	 *     parallel = safe,
+	 *     sortop = operator (>)
+	 *     );
+	 * 
+ */ + protected boolean supportsMinMaxOnUuid() { + return false; + } + + @Override + public NameQualifierSupport getNameQualifierSupport() { + // This method is overridden so the correct value will be returned when + // DatabaseMetaData is not available. + return NameQualifierSupport.SCHEMA; + } + + @Override + public String getCurrentSchemaCommand() { + return "select current_schema()"; + } + + @Override + public boolean supportsDistinctFromPredicate() { + return true; + } + + @Override + public boolean supportsIfExistsBeforeTableName() { + return true; + } + + @Override + public boolean supportsIfExistsBeforeTypeName() { + return true; + } + + @Override + public boolean supportsIfExistsBeforeConstraintName() { + return true; + } + + @Override + public boolean supportsIfExistsAfterAlterTable() { + return true; + } + + @Override + public String getBeforeDropStatement() { + // NOTICE: table "nonexistent" does not exist, skipping + // as a JDBC SQLWarning + return "set client_min_messages = WARNING"; + } + + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + // would need multiple statements to 'set not null'/'drop not null', 'set default'/'drop default', 'set generated', etc + return "alter column " + columnName + " set data type " + columnType; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + + @Override + public boolean supportsValuesList() { + return true; + } + + @Override + public boolean supportsPartitionBy() { + return true; + } + + @Override + public boolean supportsNonQueryWithCTE() { + return true; + } + @Override + public boolean supportsConflictClauseForInsertCTE() { + return true; + } + + @Override + public SequenceSupport getSequenceSupport() { + return GaussDBSequenceSupport.INSTANCE; + } + + @Override + public String getCascadeConstraintsString() { + return " cascade"; + } + + @Override + public String getQuerySequencesString() { + return "select * from information_schema.sequences"; + } + + @Override + public LimitHandler getLimitHandler() { + return LimitLimitHandler.INSTANCE; + } + + @Override + public String getForUpdateString(String aliases) { + return getForUpdateString() + " of " + aliases; + } + + @Override + public String getForUpdateString(String aliases, LockOptions lockOptions) { + // parent's implementation for (aliases, lockOptions) ignores aliases + if ( aliases.isEmpty() ) { + LockMode lockMode = lockOptions.getLockMode(); + for ( Map.Entry entry : lockOptions.getAliasSpecificLocks() ) { + // seek the highest lock mode + if ( entry.getValue().greaterThan(lockMode) ) { + aliases = entry.getKey(); + } + } + } + LockMode lockMode = lockOptions.getAliasSpecificLockMode( aliases ); + if ( lockMode == null ) { + lockMode = lockOptions.getLockMode(); + } + return switch (lockMode) { + case PESSIMISTIC_READ -> getReadLockString( aliases, lockOptions.getTimeOut() ); + case PESSIMISTIC_WRITE -> getWriteLockString( aliases, lockOptions.getTimeOut() ); + case UPGRADE_NOWAIT, PESSIMISTIC_FORCE_INCREMENT -> getForUpdateNowaitString( aliases ); + case UPGRADE_SKIPLOCKED -> getForUpdateSkipLockedString( aliases ); + default -> ""; + }; + } + + @Override + public String getNoColumnsInsertString() { + return "default values"; + } + + @Override + public String getCaseInsensitiveLike(){ + return "ilike"; + } + + @Override + public boolean supportsCaseInsensitiveLike() { + return true; + } + + @Override + public GenerationType getNativeValueGenerationStrategy() { + return GenerationType.SEQUENCE; + } + + @Override + public boolean supportsOuterJoinForUpdate() { + return false; + } + + @Override + public boolean useInputStreamToInsertBlob() { + return false; + } + + @Override + public boolean useConnectionToCreateLob() { + return false; + } + + @Override + public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { + // TODO: adapt this to handle named enum types! + return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")"; + } + + @Override + public String quoteCollation(String collation) { + return '\"' + collation + '\"'; + } + + @Override + public boolean supportsCommentOn() { + return true; + } + + @Override + public boolean supportsCurrentTimestampSelection() { + return true; + } + + @Override + public boolean isCurrentTimestampSelectStringCallable() { + return false; + } + + @Override + public String getCurrentTimestampSelectString() { + return "select now()"; + } + + @Override + public boolean supportsTupleCounts() { + return true; + } + + @Override + public boolean supportsIsTrue() { + return true; + } + + @Override + public boolean requiresParensForTupleDistinctCounts() { + return true; + } + + @Override + public void appendBooleanValueString(SqlAppender appender, boolean bool) { + appender.appendSql( bool ); + } + + @Override + public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) + throws SQLException { + + if ( dbMetaData == null ) { + builder.setUnquotedCaseStrategy( IdentifierCaseStrategy.LOWER ); + builder.setQuotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + } + + return super.buildIdentifierHelper( builder, dbMetaData ); + } + + @Override + public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new CteMutationStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + @Override + public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new CteInsertStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new GaussDBSqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return EXTRACTOR; + } + + /** + * Constraint-name extractor for constraint violation exceptions. + * Originally contributed by Denny Bartelt. + */ + private static final ViolatedConstraintNameExtractor EXTRACTOR = + new TemplatedViolatedConstraintNameExtractor( sqle -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqle ); + if ( sqlState != null ) { + switch ( Integer.parseInt( sqlState ) ) { + // CHECK VIOLATION + case 23514: + return extractUsingTemplate( "violates check constraint \"", "\"", sqle.getMessage() ); + // UNIQUE VIOLATION + case 23505: + return extractUsingTemplate( "violates unique constraint \"", "\"", sqle.getMessage() ); + // FOREIGN KEY VIOLATION + case 23503: + return extractUsingTemplate( "violates foreign key constraint \"", "\"", sqle.getMessage() ); + // NOT NULL VIOLATION + case 23502: + return extractUsingTemplate( + "null value in column \"", + "\" violates not-null constraint", + sqle.getMessage() + ); + // TODO: RESTRICT VIOLATION + case 23001: + return null; + } + } + return null; + } ); + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "40P01": + // DEADLOCK DETECTED + return new LockAcquisitionException( message, sqlException, sql ); + case "55P03": + // LOCK NOT AVAILABLE + return new PessimisticLockException( message, sqlException, sql ); + case "57014": + return new QueryTimeoutException( message, sqlException, sql ); + } + } + return null; + }; + } + + @Override + public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException { + // Register the type of the out param - GaussDB uses Types.OTHER + statement.registerOutParameter( col++, Types.OTHER ); + return col; + } + + @Override + public ResultSet getResultSet(CallableStatement ps) throws SQLException { + ps.execute(); + return (ResultSet) ps.getObject( 1 ); + } + + // Overridden informational metadata ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + public boolean supportsLobValueChangePropagation() { + return false; + } + + @Override + public boolean supportsUnboundedLobLocatorMaterialization() { + return false; + } + + @Override + public SelectItemReferenceStrategy getGroupBySelectItemReferenceStrategy() { + return SelectItemReferenceStrategy.POSITION; + } + + @Override + public CallableStatementSupport getCallableStatementSupport() { + return GaussDBCallableStatementSupport.INSTANCE; + } + + @Override + public ResultSet getResultSet(CallableStatement statement, int position) throws SQLException { + if ( position != 1 ) { + throw new UnsupportedOperationException( "GaussDB only supports REF_CURSOR parameters as the first parameter" ); + } + return (ResultSet) statement.getObject( 1 ); + } + + @Override + public ResultSet getResultSet(CallableStatement statement, String name) throws SQLException { + throw new UnsupportedOperationException( "GaussDB only supports accessing REF_CURSOR parameters by position" ); + } + + @Override + public boolean qualifyIndexName() { + return false; + } + + @Override + public IdentityColumnSupport getIdentityColumnSupport() { + return GaussDBIdentityColumnSupport.INSTANCE; + } + + @Override + public boolean supportsExpectedLobUsagePattern() { + return false; + } + + @Override + public NationalizationSupport getNationalizationSupport() { + return NationalizationSupport.IMPLICIT; + } + + @Override + public int getMaxIdentifierLength() { + return 63; + } + + @Override + public boolean supportsStandardArrays() { + return true; + } + + @Override + public boolean supportsJdbcConnectionLobCreation(DatabaseMetaData databaseMetaData) { + return false; + } + + @Override + public boolean supportsMaterializedLobAccess() { + // Prefer using text and bytea over oid (LOB), because oid is very restricted. + // If someone really wants a type bigger than 1GB, they should ask for it by using @Lob explicitly + return false; + } + + @Override + public boolean supportsTemporalLiteralOffset() { + return true; + } + + @Override + public void appendDatetimeFormat(SqlAppender appender, String format) { + throw new UnsupportedOperationException( "GaussDB not support datetime format yet" ); + } + + @Override + public String translateExtractField(TemporalUnit unit) { + return switch (unit) { + //WEEK means the ISO week number + case DAY_OF_MONTH -> "day"; + case DAY_OF_YEAR -> "doy"; + case DAY_OF_WEEK -> "dow"; + default -> super.translateExtractField( unit ); + }; + } + + @Override + public AggregateSupport getAggregateSupport() { + return null; + } + + @Override + public void appendBinaryLiteral(SqlAppender appender, byte[] bytes) { + appender.appendSql( "bytea '\\x" ); + PrimitiveByteArrayJavaType.INSTANCE.appendString( appender, bytes ); + appender.appendSql( '\'' ); + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( '\'' ); + break; + case TIME: + if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { + appender.appendSql( "time with time zone '" ); + appendAsTime( appender, temporalAccessor, true, jdbcTimeZone ); + } + else { + appender.appendSql( "time '" ); + appendAsLocalTime( appender, temporalAccessor ); + } + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { + appender.appendSql( "timestamp with time zone '" ); + appendAsTimestampWithMicros( appender, temporalAccessor, true, jdbcTimeZone ); + appender.appendSql( '\'' ); + } + else { + appender.appendSql( "timestamp '" ); + appendAsTimestampWithMicros( appender, temporalAccessor, false, jdbcTimeZone ); + appender.appendSql( '\'' ); + } + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + Date date, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, date ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time with time zone '" ); + appendAsTime( appender, date, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp with time zone '" ); + appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + Calendar calendar, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time with time zone '" ); + appendAsTime( appender, calendar, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp with time zone '" ); + appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + private String withTimeout(String lockString, int timeout) { + return switch (timeout) { + case LockOptions.NO_WAIT -> supportsNoWait() ? lockString + " nowait" : lockString; + case LockOptions.SKIP_LOCKED -> supportsSkipLocked() ? lockString + " skip locked" : lockString; + default -> lockString; + }; + } + + @Override + public String getWriteLockString(int timeout) { + return withTimeout( getForUpdateString(), timeout ); + } + + @Override + public String getWriteLockString(String aliases, int timeout) { + return withTimeout( getForUpdateString( aliases ), timeout ); + } + + @Override + public String getReadLockString(int timeout) { + return withTimeout(" for share", timeout ); + } + + @Override + public String getReadLockString(String aliases, int timeout) { + return withTimeout(" for share of " + aliases, timeout ); + } + + @Override + public String getForUpdateNowaitString() { + return supportsNoWait() + ? " for update nowait" + : getForUpdateString(); + } + + @Override + public String getForUpdateNowaitString(String aliases) { + return supportsNoWait() + ? " for update of " + aliases + " nowait" + : getForUpdateString(aliases); + } + + @Override + public String getForUpdateSkipLockedString() { + return supportsSkipLocked() + ? " for update skip locked" + : getForUpdateString(); + } + + @Override + public String getForUpdateSkipLockedString(String aliases) { + return supportsSkipLocked() + ? " for update of " + aliases + " skip locked" + : getForUpdateString( aliases ); + } + + @Override + public boolean supportsNoWait() { + return true; + } + + @Override + public boolean supportsWait() { + return false; + } + + @Override + public boolean supportsSkipLocked() { + return true; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public boolean supportsOffsetInSubquery() { + return true; + } + + @Override + public boolean supportsWindowFunctions() { + return true; + } + + @Override + public boolean supportsLateral() { + return false; + } + + @Override + public boolean supportsRecursiveCTE() { + return false; + } + + @Override + public boolean supportsOrderByInSubquery() { + return false; + } + + @Override + public boolean supportsFetchClause(FetchClauseType type) { + return false; + } + + @Override + public String getForUpdateString() { + return " for update"; + } + + @Override + public boolean supportsFilterClause() { + return false; + } + + @Override + public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() { + return FunctionalDependencyAnalysisSupportImpl.TABLE_REFERENCE; + } + + @Override + public RowLockStrategy getWriteRowLockStrategy() { + return RowLockStrategy.TABLE; + } + + @Override + public void augmentRecognizedTableTypes(List tableTypesList) { + super.augmentRecognizedTableTypes( tableTypesList ); + tableTypesList.add( "MATERIALIZED VIEW" ); + tableTypesList.add( "PARTITIONED TABLE" ); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes(typeContributions, serviceRegistry); + contributeGaussDBTypes( typeContributions, serviceRegistry); + } + + /** + * Allow for extension points to override this only + */ + protected void contributeGaussDBTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() + .getJdbcTypeRegistry(); + // For how BLOB affects Hibernate, see: + // http://in.relation.to/15492.lace + + jdbcTypeRegistry.addDescriptor( XmlJdbcType.INSTANCE ); + + jdbcTypeRegistry.addDescriptorIfAbsent( GaussDBCastingInetJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( GaussDBCastingIntervalSecondJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( GaussDBStructCastingJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( GaussDBCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( GaussDBCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); + + // GaussDB requires a custom binder for binding untyped nulls as VARBINARY + typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); + + // Until we remove StandardBasicTypes, we have to keep this + typeContributions.contributeType( + new JavaObjectType( + ObjectNullAsBinaryTypeJdbcType.INSTANCE, + typeContributions.getTypeConfiguration() + .getJavaTypeRegistry() + .getDescriptor( Object.class ) + ) + ); + + jdbcTypeRegistry.addDescriptor( GaussDBEnumJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GaussDBOrdinalEnumJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GaussDBUUIDJdbcType.INSTANCE ); + + // Replace the standard array constructor + jdbcTypeRegistry.addTypeConstructor( GaussDBArrayJdbcTypeConstructor.INSTANCE ); + } + + @Override + public UniqueDelegate getUniqueDelegate() { + return uniqueDelegate; + } + + @Override + public Exporter getTableExporter() { + return gaussDBTableExporter; + } + + /** + * @return {@code true}, but only because we can "batch" truncate + */ + @Override + public boolean canBatchTruncate() { + return true; + } + + @Override + public String getQueryHintString(String sql, String hints) { + return "/*+ " + hints + " */ " + sql; + } + + @Override + public String addSqlHintOrComment(String sql, QueryOptions queryOptions, boolean commentsEnabled) { + // GaussDB's extension pg_hint_plan needs the hint to be the first comment + if ( commentsEnabled && queryOptions.getComment() != null ) { + sql = prependComment( sql, queryOptions.getComment() ); + } + if ( queryOptions.getDatabaseHints() != null && !queryOptions.getDatabaseHints().isEmpty() ) { + sql = getQueryHintString( sql, queryOptions.getDatabaseHints() ); + } + return sql; + } + + @FunctionalInterface + private interface OptionalTableUpdateStrategy { + MutationOperation buildMutationOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory); + } + + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + return optionalTableUpdateStrategy.buildMutationOperation( mutationTarget, optionalTableUpdate, factory ); + } + + private static MutationOperation usingMerge( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + final GaussDBSqlAstTranslator translator = new GaussDBSqlAstTranslator<>( factory, optionalTableUpdate ); + return translator.createMergeOperation( optionalTableUpdate ); + } + + private static MutationOperation withoutMerge( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + return new OptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); + } + + private static class NativeParameterMarkers implements ParameterMarkerStrategy { + /** + * Singleton access + */ + public static final NativeParameterMarkers INSTANCE = new NativeParameterMarkers(); + + @Override + public String createMarker(int position, JdbcType jdbcType) { + return "$" + position; + } + } + + @Override + public int getDefaultIntervalSecondScale() { + // The maximum scale for `interval second` is 6 unfortunately + return 6; + } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBEnumJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBEnumJdbcType.java new file mode 100644 index 000000000000..3a1d399826b4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBEnumJdbcType.java @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; + +import static java.util.Collections.emptySet; +import static org.hibernate.type.SqlTypes.NAMED_ENUM; +import static org.hibernate.type.SqlTypes.OTHER; +import static org.hibernate.type.descriptor.converter.internal.EnumHelper.getEnumeratedValues; + +/** + * Represents a named {@code enum} type on GaussDB. + *

+ * Hibernate does not automatically use this for enums + * mapped as {@link jakarta.persistence.EnumType#STRING}, and + * instead this type must be explicitly requested using: + *

+ * @JdbcTypeCode(SqlTypes.NAMED_ENUM)
+ * 
+ * + * @see org.hibernate.type.SqlTypes#NAMED_ENUM + * @see GaussDBDialect#getEnumTypeDeclaration(String, String[]) + * @see GaussDBDialect#getCreateEnumTypeCommand(String, String[]) + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLEnumJdbcType. + */ +public class GaussDBEnumJdbcType implements JdbcType { + + public static final GaussDBEnumJdbcType INSTANCE = new GaussDBEnumJdbcType(); + + @Override + public int getJdbcTypeCode() { + return OTHER; + } + + @Override + public int getDefaultSqlTypeCode() { + return NAMED_ENUM; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + @SuppressWarnings("unchecked") + final Class> enumClass = (Class>) javaType.getJavaType(); + return (appender, value, dialect, wrapperOptions) -> { + appender.appendSql( "'" ); + appender.appendSql( ((Enum) value).name() ); + appender.appendSql( "'::" ); + appender.appendSql( dialect.getEnumTypeDeclaration( enumClass ) ); + }; + } + + @Override + public String getFriendlyName() { + return "ENUM"; + } + + @Override + public String toString() { + return "EnumTypeDescriptor"; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, Types.OTHER ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { + st.setNull( name, Types.OTHER ); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setObject( index, getJavaType().unwrap( value, String.class, options ), Types.OTHER ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setObject( name, getJavaType().unwrap( value, String.class, options ), Types.OTHER ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getJavaType().wrap( rs.getObject( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getJavaType().wrap( statement.getObject( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return getJavaType().wrap( statement.getObject( name ), options ); + } + }; + } + + @Override + public void addAuxiliaryDatabaseObjects( + JavaType javaType, + BasicValueConverter valueConverter, + Size columnSize, + Database database, + JdbcTypeIndicators context) { + @SuppressWarnings("unchecked") + final Class> enumClass = (Class>) javaType.getJavaType(); + @SuppressWarnings("unchecked") + final String[] enumeratedValues = + valueConverter == null + ? getEnumeratedValues( enumClass ) + : getEnumeratedValues( enumClass, (BasicValueConverter,?>) valueConverter ) ; + if ( getDefaultSqlTypeCode() == NAMED_ENUM ) { + Arrays.sort( enumeratedValues ); + } + final Dialect dialect = database.getDialect(); + final String[] create = + dialect.getCreateEnumTypeCommand( javaType.getJavaTypeClass().getSimpleName(), enumeratedValues ); + final String[] drop = dialect.getDropEnumTypeCommand( enumClass ); + if ( create != null && create.length > 0 ) { + database.addAuxiliaryDatabaseObject( + new NamedAuxiliaryDatabaseObject( + enumClass.getSimpleName(), + database.getDefaultNamespace(), + create, + drop, + emptySet(), + true + ) + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBOrdinalEnumJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBOrdinalEnumJdbcType.java new file mode 100644 index 000000000000..e889105ebb6b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBOrdinalEnumJdbcType.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import jakarta.persistence.EnumType; + +import static org.hibernate.type.SqlTypes.NAMED_ORDINAL_ENUM; + +/** + * Represents a named {@code enum} type on GaussDB. + *

+ * Hibernate does not automatically use this for enums + * mapped as {@link EnumType#ORDINAL}, and + * instead this type must be explicitly requested using: + *

+ * @JdbcTypeCode(SqlTypes.NAMED_ORDINAL_ENUM)
+ * 
+ * + * @see org.hibernate.type.SqlTypes#NAMED_ORDINAL_ENUM + * @see GaussDBDialect#getEnumTypeDeclaration(String, String[]) + * @see GaussDBDialect#getCreateEnumTypeCommand(String, String[]) + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLOrdinalEnumJdbcType. + */ +public class GaussDBOrdinalEnumJdbcType extends GaussDBEnumJdbcType { + + public static final GaussDBOrdinalEnumJdbcType INSTANCE = new GaussDBOrdinalEnumJdbcType(); + + @Override + public int getDefaultSqlTypeCode() { + return NAMED_ORDINAL_ENUM; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBSqlAstTranslator.java new file mode 100644 index 000000000000..ed83b80d8704 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBSqlAstTranslator.java @@ -0,0 +1,308 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.SqlAstTranslatorWithMerge; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; +import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.internal.TableInsertStandard; +import org.hibernate.type.SqlTypes; + +/** + * A SQL AST translator for GaussDB. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLSqlAstTranslator. + */ +public class GaussDBSqlAstTranslator extends SqlAstTranslatorWithMerge { + + public GaussDBSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + @Override + public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) { + inArrayPredicate.getTestExpression().accept( this ); + appendSql( " = any (" ); + inArrayPredicate.getArrayParameter().accept( this ); + appendSql( ")" ); + } + + @Override + protected String getArrayContainsFunction() { + return super.getArrayContainsFunction(); + } + + @Override + protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { + renderIntoIntoAndTable( tableInsert ); + appendSql( "default values" ); + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + final String identificationVariable = tableReference.getIdentificationVariable(); + if ( identificationVariable != null ) { + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.INSERT ) { + // GaussDB requires the "as" keyword for inserts + appendSql( " as " ); + } + else { + append( WHITESPACE ); + } + append( tableReference.getIdentificationVariable() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + final Statement currentStatement = getStatementStack().getCurrent(); + if ( !( currentStatement instanceof UpdateStatement updateStatement ) + || !hasNonTrivialFromClause( updateStatement.getFromClause() ) ) { + // For UPDATE statements we render a full FROM clause and a join condition to match target table rows, + // but for that to work, we have to omit the alias for the target table reference here + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseJoiningDmlTargetReference( statement ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClauseWithDoNothing( conflictClause ); + } + + @Override + protected void renderExpressionAsClauseItem(Expression expression) { + expression.accept( this ); + } + + @Override + protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + final JdbcMappingContainer lhsExpressionType = lhs.getExpressionType(); + if ( lhsExpressionType != null && lhsExpressionType.getJdbcTypeCount() == 1 + && lhsExpressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() == SqlTypes.SQLXML ) { + // In GaussDB, XMLTYPE is not "comparable", so we have to cast the two parts to varchar for this purpose + switch ( operator ) { + case EQUAL: + case NOT_DISTINCT_FROM: + case NOT_EQUAL: + case DISTINCT_FROM: + appendSql( "cast(" ); + lhs.accept( this ); + appendSql( " as text)" ); + appendSql( operator.sqlText() ); + appendSql( "cast(" ); + rhs.accept( this ); + appendSql( " as text)" ); + return; + default: + // Fall through + break; + } + } + renderComparisonStandard( lhs, operator, rhs ); + } + + @Override + public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { + final boolean isNegated = booleanExpressionPredicate.isNegated(); + if ( isNegated ) { + appendSql( "not(" ); + } + booleanExpressionPredicate.getExpression().accept( this ); + if ( isNegated ) { + appendSql( CLOSE_PARENTHESIS ); + } + } + + @Override + public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) { + final Expression expression = nullnessPredicate.getExpression(); + final JdbcMappingContainer expressionType = expression.getExpressionType(); + if ( isStruct( expressionType ) ) { + // Surprise, the null predicate checks if all components of the struct are null or not, + // rather than the column itself, so we have to use the distinct from predicate to implement this instead + expression.accept( this ); + if ( nullnessPredicate.isNegated() ) { + appendSql( " is distinct from null" ); + } + else { + appendSql( " is not distinct from null" ); + } + } + else { + super.visitNullnessPredicate( nullnessPredicate ); + } + } + + @Override + protected void renderMaterializationHint(CteMaterialization materialization) { + if ( materialization == CteMaterialization.NOT_MATERIALIZED ) { + appendSql( "not " ); + } + appendSql( "materialized " ); + } + + @Override + protected String getForUpdate() { + return getDialect().getForUpdateString(); + } + + @Override + protected String getForShare(int timeoutMillis) { + // Note that `for key share` is inappropriate as that only means "prevent PK changes" + return " for share"; + } + + protected boolean shouldEmulateFetchClause(QueryPart queryPart) { + // Check if current query part is already row numbering to avoid infinite recursion + if ( getQueryPartForRowNumbering() == queryPart || isRowsOnlyFetchClauseType( queryPart ) ) { + return false; + } + return !getDialect().supportsFetchClause( queryPart.getFetchClauseType() ); + } + + @Override + public void visitQueryGroup(QueryGroup queryGroup) { + if ( shouldEmulateFetchClause( queryGroup ) ) { + emulateFetchOffsetWithWindowFunctions( queryGroup, true ); + } + else { + super.visitQueryGroup( queryGroup ); + } + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + if ( !isRowNumberingCurrentQueryPart() ) { + if ( getDialect().supportsFetchClause( FetchClauseType.ROWS_ONLY ) ) { + renderOffsetFetchClause( queryPart, true ); + } + else { + renderLimitOffsetClause( queryPart ); + } + } + } + + @Override + protected void renderStandardCycleClause(CteStatement cte) { + super.renderStandardCycleClause( cte ); + if ( cte.getCycleMarkColumn() != null && cte.getCyclePathColumn() == null && getDialect().supportsRecursiveCycleUsingClause() ) { + appendSql( " using " ); + appendSql( determineCyclePathColumnName( cte ) ); + } + } + + @Override + protected void renderPartitionItem(Expression expression) { + // We render an empty group instead of literals as some DBs don't support grouping by literals + // Note that integer literals, which refer to select item positions, are handled in #visitGroupByClause + if ( expression instanceof Literal ) { + appendSql( "()" ); + } + else if ( expression instanceof Summarization summarization ) { + appendSql( summarization.getKind().sqlText() ); + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparated( summarization.getGroupings() ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + expression.accept( this ); + } + } + + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + // We need a custom implementation here because GaussDB + // uses the backslash character as default escape character + // According to the documentation, we can overcome this by specifying an empty escape character + // See https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE + likePredicate.getMatchExpression().accept( this ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + if ( likePredicate.isCaseSensitive() ) { + appendSql( " like " ); + } + else { + appendSql( WHITESPACE ); + appendSql( getDialect().getCaseInsensitiveLike() ); + appendSql( WHITESPACE ); + } + likePredicate.getPattern().accept( this ); + if ( likePredicate.getEscapeCharacter() != null ) { + appendSql( " escape " ); + likePredicate.getEscapeCharacter().accept( this ); + } else { + appendSql( " escape ''''" ); + } + } + + @Override + public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) { + if ( isIntegerDivisionEmulationRequired( arithmeticExpression ) ) { + appendSql( "floor" ); + } + appendSql( OPEN_PARENTHESIS ); + visitArithmeticOperand( arithmeticExpression.getLeftHandOperand() ); + appendSql( arithmeticExpression.getOperator().getOperatorSqlTextString() ); + visitArithmeticOperand( arithmeticExpression.getRightHandOperand() ); + appendSql( CLOSE_PARENTHESIS ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBStructCastingJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBStructCastingJdbcType.java new file mode 100644 index 000000000000..b2d93f7f426d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBStructCastingJdbcType.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLStructCastingJdbcType. + */ +public class GaussDBStructCastingJdbcType extends AbstractGaussDBStructJdbcType { + + public static final GaussDBStructCastingJdbcType INSTANCE = new GaussDBStructCastingJdbcType(); + public GaussDBStructCastingJdbcType() { + this( null, null, null ); + } + + private GaussDBStructCastingJdbcType( + EmbeddableMappingType embeddableMappingType, + String typeName, + int[] orderMapping) { + super( embeddableMappingType, typeName, orderMapping ); + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new GaussDBStructCastingJdbcType( + mappingType, + sqlType, + creationContext.getBootModel() + .getDatabase() + .getDefaultNamespace() + .locateUserDefinedType( Identifier.toIdentifier( sqlType ) ) + .getOrderMapping() + ); + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as " ); + appender.append( getStructTypeName() ); + appender.append( ')' ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String stringValue = ( (GaussDBStructCastingJdbcType) getJdbcType() ).toString( + value, + getJavaType(), + options + ); + st.setString( index, stringValue ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String stringValue = ( (GaussDBStructCastingJdbcType) getJdbcType() ).toString( + value, + getJavaType(), + options + ); + st.setString( name, stringValue ); + } + + @Override + public Object getBindValue(X value, WrapperOptions options) throws SQLException { + return ( (GaussDBStructCastingJdbcType) getJdbcType() ).getBindValue( value, options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBUUIDJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBUUIDJdbcType.java new file mode 100644 index 000000000000..9c4a106aa5e4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/GaussDBUUIDJdbcType.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.UUID; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; + +/** + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLUUIDJdbcType. + */ +public class GaussDBUUIDJdbcType extends UUIDJdbcType { + + /** + * Singleton access + */ + public static final GaussDBUUIDJdbcType INSTANCE = new GaussDBUUIDJdbcType(); + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, getJdbcType().getJdbcTypeCode(), "uuid" ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { + st.setNull( name, getJdbcType().getJdbcTypeCode(), "uuid" ); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setObject( index, getJavaType().unwrap( value, UUID.class, options ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setObject( name, getJavaType().unwrap( value, UUID.class, options ) ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/GaussDBAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/GaussDBAggregateSupport.java new file mode 100644 index 000000000000..90d300f481fe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/GaussDBAggregateSupport.java @@ -0,0 +1,630 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.aggregate; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.hibernate.dialect.Dialect; +import org.hibernate.internal.util.StringHelper; +import org.hibernate.mapping.Column; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.XmlHelper; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.type.SqlTypes.ARRAY; +import static org.hibernate.type.SqlTypes.BIGINT; +import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.BOOLEAN; +import static org.hibernate.type.SqlTypes.DOUBLE; +import static org.hibernate.type.SqlTypes.FLOAT; +import static org.hibernate.type.SqlTypes.INTEGER; +import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; +import static org.hibernate.type.SqlTypes.LONG32VARBINARY; +import static org.hibernate.type.SqlTypes.SMALLINT; +import static org.hibernate.type.SqlTypes.SQLXML; +import static org.hibernate.type.SqlTypes.STRUCT; +import static org.hibernate.type.SqlTypes.STRUCT_ARRAY; +import static org.hibernate.type.SqlTypes.STRUCT_TABLE; +import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.VARBINARY; +import static org.hibernate.type.SqlTypes.XML_ARRAY; + +/** + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLAggregateSupport. + */ +public class GaussDBAggregateSupport extends AggregateSupportImpl { + + private static final AggregateSupport INSTANCE = new GaussDBAggregateSupport(); + + private static final String XML_EXTRACT_START = "xmlelement(name \"" + XmlHelper.ROOT_TAG + "\",(select xmlagg(t.v) from xmltable("; + private static final String XML_EXTRACT_SEPARATOR = "/*' passing "; + private static final String XML_EXTRACT_END = " columns v xml path '.')t))"; + private static final String XML_QUERY_START = "(select xmlagg(t.v) from xmltable("; + private static final String XML_QUERY_SEPARATOR = "' passing "; + private static final String XML_QUERY_END = " columns v xml path '.')t)"; + + public static AggregateSupport valueOf(Dialect dialect) { + return GaussDBAggregateSupport.INSTANCE; + } + + @Override + public String aggregateComponentCustomReadExpression( + String template, + String placeholder, + String aggregateParentReadExpression, + String columnExpression, + int aggregateColumnTypeCode, + SqlTypedMapping column, + TypeConfiguration typeConfiguration) { + switch ( aggregateColumnTypeCode ) { + case JSON_ARRAY: + case JSON: + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case JSON: + case JSON_ARRAY: + return template.replace( + placeholder, + aggregateParentReadExpression + "->'" + columnExpression + "'" + ); + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + "decode(" + aggregateParentReadExpression + "->>'" + columnExpression + "','hex')" + ); + case ARRAY: + final BasicPluralType pluralType = (BasicPluralType) column.getJdbcMapping(); + switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { + case BOOLEAN: + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + // For types that are natively supported in jsonb we can use jsonb_array_elements, + // but note that we can't use that for string types, + // because casting a jsonb[] to text[] will not omit the quotes of the jsonb text values + return template.replace( + placeholder, + "cast(array(select jsonb_array_elements(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getColumnDefinition() + ')' + ); + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + "array(select decode(jsonb_array_elements_text(" + aggregateParentReadExpression + "->'" + columnExpression + "'),'hex'))" + ); + default: + return template.replace( + placeholder, + "cast(array(select jsonb_array_elements_text(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getColumnDefinition() + ')' + ); + } + default: + return template.replace( + placeholder, + "cast(" + aggregateParentReadExpression + "->>'" + columnExpression + "' as " + column.getColumnDefinition() + ')' + ); + } + case XML_ARRAY: + case SQLXML: + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case SQLXML: + return template.replace( + placeholder, + XML_EXTRACT_START + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + XML_EXTRACT_END + ); + case XML_ARRAY: + if ( typeConfiguration.getCurrentBaseSqlTypeIndicators().isXmlFormatMapperLegacyFormatEnabled() ) { + throw new IllegalArgumentException( "XML array '" + columnExpression + "' in '" + aggregateParentReadExpression + "' is not supported with legacy format enabled." ); + } + else { + return template.replace( + placeholder, + "xmlelement(name \"Collection\",(select xmlagg(t.v order by t.i) from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + " columns v xml path '.', i for ordinality)t))" + ); + } + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + "decode((select t.v from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression )+ " columns v text path '.') t),'hex')" + ); + case ARRAY: + throw new UnsupportedOperationException( "Transforming XML_ARRAY to native arrays is not supported on GaussDB!" ); + default: + return template.replace( + placeholder, + "(select t.v from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + " columns v " + column.getColumnDefinition() + " path '.') t)" + ); + } + case STRUCT: + case STRUCT_ARRAY: + case STRUCT_TABLE: + return template.replace( placeholder, '(' + aggregateParentReadExpression + ")." + columnExpression ); + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + } + + private static String xmlExtractArguments(String aggregateParentReadExpression, String xpathFragment) { + final String extractArguments; + int separatorIndex; + if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) + && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) + && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR )) != -1 ) { + final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + sb.append( aggregateParentReadExpression, XML_EXTRACT_START.length(), separatorIndex ); + sb.append( '/' ); + sb.append( xpathFragment ); + sb.append( aggregateParentReadExpression, separatorIndex + 2, aggregateParentReadExpression.length() - XML_EXTRACT_END.length() ); + extractArguments = sb.toString(); + } + else if ( aggregateParentReadExpression.startsWith( XML_QUERY_START ) + && aggregateParentReadExpression.endsWith( XML_QUERY_END ) + && (separatorIndex = aggregateParentReadExpression.indexOf( XML_QUERY_SEPARATOR )) != -1 ) { + final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); + sb.append( aggregateParentReadExpression, XML_QUERY_START.length(), separatorIndex ); + sb.append( '/' ); + sb.append( xpathFragment ); + sb.append( aggregateParentReadExpression, separatorIndex, aggregateParentReadExpression.length() - XML_QUERY_END.length() ); + extractArguments = sb.toString(); + } + else { + extractArguments = "'/" + XmlHelper.ROOT_TAG + "/" + xpathFragment + "' passing " + aggregateParentReadExpression; + } + return extractArguments; + } + + private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { + final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode(); + switch ( sqlTypeCode ) { + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex + return "to_jsonb(encode(" + customWriteExpression + ",'hex'))"; + case ARRAY: + final BasicPluralType pluralType = (BasicPluralType) jdbcMapping; + switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex + return "to_jsonb(array(select encode(unnest(" + customWriteExpression + "),'hex')))"; + default: + return "to_jsonb(" + customWriteExpression + ")"; + } + default: + return "to_jsonb(" + customWriteExpression + ")"; + } + } + + private static String xmlCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { + final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode(); + switch ( sqlTypeCode ) { + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex + return "encode(" + customWriteExpression + ",'hex')"; +// case ARRAY: +// final BasicPluralType pluralType = (BasicPluralType) jdbcMapping; +// switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { +// case BINARY: +// case VARBINARY: +// case LONG32VARBINARY: +// // We encode binary data as hex +// return "to_jsonb(array(select encode(unnest(" + customWriteExpression + "),'hex')))"; +// default: +// return "to_jsonb(" + customWriteExpression + ")"; +// } + default: + return customWriteExpression; + } + } + + @Override + public String aggregateComponentAssignmentExpression( + String aggregateParentAssignmentExpression, + String columnExpression, + int aggregateColumnTypeCode, + Column column) { + switch ( aggregateColumnTypeCode ) { + case JSON: + case JSON_ARRAY: + case SQLXML: + case XML_ARRAY: + // For JSON/XML we always have to replace the whole object + return aggregateParentAssignmentExpression; + case STRUCT: + case STRUCT_ARRAY: + case STRUCT_TABLE: + return aggregateParentAssignmentExpression + "." + columnExpression; + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + } + + @Override + public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { + switch ( aggregateSqlTypeCode ) { + case JSON: + case SQLXML: + return true; + } + return false; + } + + @Override + public boolean preferSelectAggregateMapping(int aggregateSqlTypeCode) { + // The JDBC driver does not support selecting java.sql.Struct, so return false to select individual parts + return aggregateSqlTypeCode != STRUCT; + } + + @Override + public WriteExpressionRenderer aggregateCustomWriteExpressionRenderer( + SelectableMapping aggregateColumn, + SelectableMapping[] columnsToUpdate, + TypeConfiguration typeConfiguration) { + final int aggregateSqlTypeCode = aggregateColumn.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); + switch ( aggregateSqlTypeCode ) { + case JSON: + return new RootJsonWriteExpression( aggregateColumn, columnsToUpdate ); + case SQLXML: + return new RootXmlWriteExpression( aggregateColumn, columnsToUpdate ); + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateSqlTypeCode ); + } + + interface JsonWriteExpression { + void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression); + } + private static class AggregateJsonWriteExpression implements JsonWriteExpression { + private final LinkedHashMap subExpressions = new LinkedHashMap<>(); + + protected void initializeSubExpressions(SelectableMapping[] columns) { + for ( SelectableMapping column : columns ) { + final SelectablePath selectablePath = column.getSelectablePath(); + final SelectablePath[] parts = selectablePath.getParts(); + AggregateJsonWriteExpression currentAggregate = this; + for ( int i = 1; i < parts.length - 1; i++ ) { + currentAggregate = (AggregateJsonWriteExpression) currentAggregate.subExpressions.computeIfAbsent( + parts[i].getSelectableName(), + k -> new AggregateJsonWriteExpression() + ); + } + final String customWriteExpression = column.getWriteExpression(); + currentAggregate.subExpressions.put( + parts[parts.length - 1].getSelectableName(), + new BasicJsonWriteExpression( + column, + jsonCustomWriteExpression( customWriteExpression, column.getJdbcMapping() ) + ) + ); + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( "||jsonb_build_object" ); + char separator = '('; + for ( Map.Entry entry : subExpressions.entrySet() ) { + final String column = entry.getKey(); + final JsonWriteExpression value = entry.getValue(); + final String subPath = path + "->'" + column + "'"; + sb.append( separator ); + if ( value instanceof AggregateJsonWriteExpression ) { + sb.append( '\'' ); + sb.append( column ); + sb.append( "',coalesce(" ); + sb.append( subPath ); + sb.append( ",'{}')" ); + value.append( sb, subPath, translator, expression ); + } + else { + value.append( sb, subPath, translator, expression ); + } + separator = ','; + } + sb.append( ')' ); + } + } + + private static class RootJsonWriteExpression extends AggregateJsonWriteExpression + implements WriteExpressionRenderer { + private final boolean nullable; + private final String path; + + RootJsonWriteExpression(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + this.nullable = aggregateColumn.isNullable(); + this.path = aggregateColumn.getSelectionExpression(); + initializeSubExpressions( columns ); + } + + @Override + public void render( + SqlAppender sqlAppender, + SqlAstTranslator translator, + AggregateColumnWriteExpression aggregateColumnWriteExpression, + String qualifier) { + final String basePath; + if ( qualifier == null || qualifier.isBlank() ) { + basePath = path; + } + else { + basePath = qualifier + "." + path; + } + if ( nullable ) { + sqlAppender.append( "coalesce(" ); + sqlAppender.append( basePath ); + sqlAppender.append( ",'{}')" ); + } + else { + sqlAppender.append( basePath ); + } + append( sqlAppender, basePath, translator, aggregateColumnWriteExpression ); + } + } + private static class BasicJsonWriteExpression implements JsonWriteExpression { + + private final SelectableMapping selectableMapping; + private final String customWriteExpressionStart; + private final String customWriteExpressionEnd; + + BasicJsonWriteExpression(SelectableMapping selectableMapping, String customWriteExpression) { + this.selectableMapping = selectableMapping; + if ( customWriteExpression.equals( "?" ) ) { + this.customWriteExpressionStart = ""; + this.customWriteExpressionEnd = ""; + } + else { + final String[] parts = StringHelper.split( "?", customWriteExpression ); + assert parts.length == 2; + this.customWriteExpressionStart = parts[0]; + this.customWriteExpressionEnd = parts[1]; + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( '\'' ); + sb.append( selectableMapping.getSelectableName() ); + sb.append( "'," ); + sb.append( customWriteExpressionStart ); + // We use NO_UNTYPED here so that expressions which require type inference are casted explicitly, + // since we don't know how the custom write expression looks like where this is embedded, + // so we have to be pessimistic and avoid ambiguities + translator.render( expression.getValueExpression( selectableMapping ), SqlAstNodeRenderingMode.NO_UNTYPED ); + sb.append( customWriteExpressionEnd ); + } + } + + interface XmlWriteExpression { + void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression); + } + private static class AggregateXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + private final String columnDefinition; + private final LinkedHashMap subExpressions = new LinkedHashMap<>(); + + private AggregateXmlWriteExpression(SelectableMapping selectableMapping, String columnDefinition) { + this.selectableMapping = selectableMapping; + this.columnDefinition = columnDefinition; + } + + protected void initializeSubExpressions(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + for ( SelectableMapping column : columns ) { + final SelectablePath selectablePath = column.getSelectablePath(); + final SelectablePath[] parts = selectablePath.getParts(); + AggregateXmlWriteExpression currentAggregate = this; + for ( int i = 1; i < parts.length - 1; i++ ) { + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) currentAggregate.selectableMapping.getJdbcMapping().getJdbcType(); + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int selectableIndex = embeddableMappingType.getSelectableIndex( parts[i].getSelectableName() ); + currentAggregate = (AggregateXmlWriteExpression) currentAggregate.subExpressions.computeIfAbsent( + parts[i].getSelectableName(), + k -> new AggregateXmlWriteExpression( embeddableMappingType.getJdbcValueSelectable( selectableIndex ), columnDefinition ) + ); + } + final String customWriteExpression = column.getWriteExpression(); + currentAggregate.subExpressions.put( + parts[parts.length - 1].getSelectableName(), + new BasicXmlWriteExpression( + column, + xmlCustomWriteExpression( customWriteExpression, column.getJdbcMapping() ) + ) + ); + } + passThroughUnsetSubExpressions( aggregateColumn ); + } + + protected void passThroughUnsetSubExpressions(SelectableMapping aggregateColumn) { + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) aggregateColumn.getJdbcMapping().getJdbcType(); + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + for ( int i = 0; i < jdbcValueCount; i++ ) { + final SelectableMapping selectableMapping = embeddableMappingType.getJdbcValueSelectable( i ); + + final XmlWriteExpression xmlWriteExpression = subExpressions.get( selectableMapping.getSelectableName() ); + if ( xmlWriteExpression == null ) { + subExpressions.put( + selectableMapping.getSelectableName(), + new PassThroughXmlWriteExpression( selectableMapping ) + ); + } + else if ( xmlWriteExpression instanceof AggregateXmlWriteExpression writeExpression ) { + writeExpression.passThroughUnsetSubExpressions( selectableMapping ); + } + } + } + + protected String getTagName() { + return selectableMapping.getSelectableName(); + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( "xmlelement(name " ); + sb.appendDoubleQuoteEscapedString( getTagName() ); + sb.append( ",xmlconcat" ); + char separator = '('; + for ( Map.Entry entry : subExpressions.entrySet() ) { + sb.append( separator ); + + final XmlWriteExpression value = entry.getValue(); + if ( value instanceof AggregateXmlWriteExpression ) { + final String subPath = XML_QUERY_START + xmlExtractArguments( path, entry.getKey() ) + XML_QUERY_END; + value.append( sb, subPath, translator, expression ); + } + else { + value.append( sb, path, translator, expression ); + } + separator = ','; + } + sb.append( "))" ); + } + } + + private static class RootXmlWriteExpression extends AggregateXmlWriteExpression + implements WriteExpressionRenderer { + private final String path; + + RootXmlWriteExpression(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + super( aggregateColumn, aggregateColumn.getColumnDefinition() ); + path = aggregateColumn.getSelectionExpression(); + initializeSubExpressions( aggregateColumn, columns ); + } + + @Override + protected String getTagName() { + return XmlHelper.ROOT_TAG; + } + + @Override + public void render( + SqlAppender sqlAppender, + SqlAstTranslator translator, + AggregateColumnWriteExpression aggregateColumnWriteExpression, + String qualifier) { + final String basePath; + if ( qualifier == null || qualifier.isBlank() ) { + basePath = path; + } + else { + basePath = qualifier + "." + path; + } + append( sqlAppender, XML_QUERY_START + "'/" + getTagName() + "' passing " + basePath + XML_QUERY_END, translator, aggregateColumnWriteExpression ); + } + } + private static class BasicXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + private final String[] customWriteExpressionParts; + + BasicXmlWriteExpression(SelectableMapping selectableMapping, String customWriteExpression) { + this.selectableMapping = selectableMapping; + if ( customWriteExpression.equals( "?" ) ) { + this.customWriteExpressionParts = new String[]{ "", "" }; + } + else { + assert !customWriteExpression.startsWith( "?" ); + final String[] parts = StringHelper.split( "?", customWriteExpression ); + assert parts.length == 2 || (parts.length & 1) == 1; + this.customWriteExpressionParts = parts; + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + final JdbcType jdbcType = selectableMapping.getJdbcMapping().getJdbcType(); + final boolean isArray = jdbcType.getDefaultSqlTypeCode() == XML_ARRAY; + sb.append( "xmlelement(name " ); + sb.appendDoubleQuoteEscapedString( selectableMapping.getSelectableName() ); + sb.append( ',' ); + if ( isArray ) { + // Remove the tag to wrap the value into the selectable specific tag + sb.append( "(select xmlagg(t.v order by t.i) from xmltable('/Collection/*' passing " ); + } + sb.append( customWriteExpressionParts[0] ); + for ( int i = 1; i < customWriteExpressionParts.length; i++ ) { + // We use NO_UNTYPED here so that expressions which require type inference are casted explicitly, + // since we don't know how the custom write expression looks like where this is embedded, + // so we have to be pessimistic and avoid ambiguities + translator.render( expression.getValueExpression( selectableMapping ), SqlAstNodeRenderingMode.NO_UNTYPED ); + sb.append( customWriteExpressionParts[i] ); + } + if ( isArray ) { + sb.append( " columns v xml path '.', i for ordinality)t)" ); + } + sb.append( ')' ); + } + } + + private static class PassThroughXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + + PassThroughXmlWriteExpression(SelectableMapping selectableMapping) { + this.selectableMapping = selectableMapping; + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( XML_QUERY_START ); + sb.append( xmlExtractArguments( path, selectableMapping.getSelectableName() ) ); + sb.append( XML_QUERY_END ); + } + } + +} 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 5dfe7936ab3f..0737ac678806 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 @@ -2572,6 +2572,15 @@ public void format_toChar() { functionRegistry.register( "format", new FormatFunction( "to_char", typeConfiguration ) ); } + /** + * Usually Oracle-style (except for Informix which quite close to MySQL-style) + * + * @see org.hibernate.dialect.OracleDialect#datetimeFormat + */ + public void format_toChar_gaussdb() { + functionRegistry.register( "format", new GaussDBFormatFunction( "to_char", typeConfiguration ) ); + } + /** * MySQL-style (also Ingres) * @@ -2662,6 +2671,14 @@ public void array_postgresql() { functionRegistry.register( "array_list", new PostgreSQLArrayConstructorFunction( true ) ); } + /** + * GaussDB array() constructor function + */ + public void array_gaussdb() { + functionRegistry.register( "array", new GaussDBArrayConstructorFunction( false ) ); + functionRegistry.register( "array_list", new GaussDBArrayConstructorFunction( true ) ); + } + /** * Google Spanner array() constructor function */ @@ -2746,6 +2763,15 @@ public void arrayContains_postgresql() { functionRegistry.register( "array_includes_nullable", new ArrayIncludesOperatorFunction( true, typeConfiguration ) ); } + /** + * GaussDB array contains operator + */ + public void arrayContains_gaussdb() { + functionRegistry.register( "array_contains_nullable", new GaussDBArrayContainsOperatorFunction( true, typeConfiguration ) ); + functionRegistry.register( "array_includes", new ArrayIncludesOperatorFunction( false, typeConfiguration ) ); + functionRegistry.register( "array_includes_nullable", new ArrayIncludesOperatorFunction( true, typeConfiguration ) ); + } + /** * Oracle array_contains() function */ @@ -2798,6 +2824,16 @@ public void arrayIntersects_postgresql() { functionRegistry.registerAlternateKey( "array_overlaps_nullable", "array_intersects_nullable" ); } + /** + * GaussDB array intersects operator + */ + public void arrayIntersects_gaussdb() { + functionRegistry.register( "array_intersects", new ArrayIntersectsOperatorFunction( false, typeConfiguration ) ); + functionRegistry.register( "array_intersects_nullable", new ArrayIntersectsOperatorFunction( true, typeConfiguration ) ); + functionRegistry.registerAlternateKey( "array_overlaps", "array_intersects" ); + functionRegistry.registerAlternateKey( "array_overlaps_nullable", "array_intersects_nullable" ); + } + /** * Oracle array_intersects() function */ @@ -2937,6 +2973,13 @@ public void arrayConcat_postgresql() { functionRegistry.register( "array_concat", new PostgreSQLArrayConcatFunction() ); } + /** + * PostgreSQL array_concat() function + */ + public void arrayConcat_gaussdb() { + functionRegistry.register( "array_concat", new GaussDBArrayConcatFunction() ); + } + /** * Oracle array_concat() function */ @@ -2958,6 +3001,13 @@ public void arrayPrepend_postgresql() { functionRegistry.register( "array_prepend", new PostgreSQLArrayConcatElementFunction( true ) ); } + /** + * GaussDB array_prepend() function + */ + public void arrayPrepend_gaussdb() { + functionRegistry.register( "array_prepend", new GaussDBArrayConcatElementFunction( true ) ); + } + /** * Oracle array_prepend() function */ @@ -2979,6 +3029,13 @@ public void arrayAppend_postgresql() { functionRegistry.register( "array_append", new PostgreSQLArrayConcatElementFunction( false ) ); } + /** + * GaussDB array_append() function + */ + public void arrayAppend_gaussdb() { + functionRegistry.register( "array_append", new GaussDBArrayConcatElementFunction( false ) ); + } + /** * Oracle array_append() function */ @@ -3054,6 +3111,13 @@ public void arraySet_unnest() { functionRegistry.register( "array_set", new ArraySetUnnestFunction() ); } + /** + * GaussDB array_set() function + */ + public void arraySet_gaussdb() { + functionRegistry.register( "array_set", new GaussDBArraySetFunction() ); + } + /** * Oracle array_set() function */ @@ -3084,6 +3148,13 @@ public void arrayRemove_h2(int maximumArraySize) { functionRegistry.register( "array_remove", new H2ArrayRemoveFunction( maximumArraySize ) ); } + /** + * GaussDB array_remove() function + */ + public void arrayRemove_gaussdb() { + functionRegistry.register( "array_remove", new GaussDBArrayRemoveFunction()); + } + /** * HSQL array_remove() function */ @@ -3098,6 +3169,13 @@ public void arrayRemove_oracle() { functionRegistry.register( "array_remove", new OracleArrayRemoveFunction() ); } + /** + * GaussDB array_remove_index() function + */ + public void arrayRemoveIndex_gaussdb() { + functionRegistry.register( "array_remove_index", new GaussDBArrayRemoveIndexFunction(false) ); + } + /** * H2 array_remove_index() function */ @@ -3215,6 +3293,13 @@ public void arrayReplace_oracle() { functionRegistry.register( "array_replace", new OracleArrayReplaceFunction() ); } + /** + * GaussDB array_replace() function + */ + public void arrayReplace_gaussdb() { + functionRegistry.register( "array_replace", new GaussDBArrayReplaceFunction() ); + } + /** * H2, HSQLDB, CockroachDB and PostgreSQL array_trim() function */ @@ -3275,6 +3360,14 @@ public void arrayFill_postgresql() { functionRegistry.register( "array_fill_list", new PostgreSQLArrayFillFunction( true ) ); } + /** + * GaussDB array_fill() function + */ + public void arrayFill_gaussdb() { + functionRegistry.register( "array_fill", new GaussDBArrayFillFunction( false ) ); + functionRegistry.register( "array_fill_list", new GaussDBArrayFillFunction( true ) ); + } + /** * Cockroach array_fill() function */ @@ -3564,6 +3657,13 @@ public void jsonObject_postgresql() { functionRegistry.register( "json_object", new PostgreSQLJsonObjectFunction( typeConfiguration ) ); } + /** + * GaussDB json_object() function + */ + public void jsonObject_gaussdb() { + functionRegistry.register( "json_object", new GaussDBJsonObjectFunction( typeConfiguration ) ); + } + /** * json_array() function */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBFormatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBFormatFunction.java new file mode 100644 index 000000000000..34d6e2b1902f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBFormatFunction.java @@ -0,0 +1,793 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.dialect.Dialect; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.MultipatternSqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.DurationUnit; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Format; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.predicate.BetweenPredicate; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import static org.hibernate.internal.util.StringHelper.splitFull; +import static org.hibernate.query.sqm.BinaryArithmeticOperator.DIVIDE_PORTABLE; +import static org.hibernate.query.sqm.BinaryArithmeticOperator.MODULO; +import static org.hibernate.query.sqm.ComparisonOperator.GREATER_THAN_OR_EQUAL; +import static org.hibernate.query.sqm.ComparisonOperator.LESS_THAN; +import static org.hibernate.query.sqm.ComparisonOperator.LESS_THAN_OR_EQUAL; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL; +import static org.hibernate.query.sqm.produce.function.StandardArgumentsValidators.exactly; +import static org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers.invariant; +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.invariant; + +/** + * A format function with support for composite temporal expressions. + * + * @author chenzhida + * + * Notes: Original code of this class is based on FormatFunction. + * + */ +public class GaussDBFormatFunction extends AbstractSqmFunctionDescriptor implements FunctionRenderer { + + private final String nativeFunctionName; + private final boolean reversedArguments; + private final boolean concatPattern; + private final boolean supportsTime; + + public GaussDBFormatFunction(String nativeFunctionName, TypeConfiguration typeConfiguration) { + this( nativeFunctionName, false, true, typeConfiguration ); + } + + public GaussDBFormatFunction( + String nativeFunctionName, + boolean reversedArguments, + boolean concatPattern, + TypeConfiguration typeConfiguration) { + this( nativeFunctionName, reversedArguments, concatPattern, true, typeConfiguration ); + } + + public GaussDBFormatFunction( + String nativeFunctionName, + boolean reversedArguments, + boolean concatPattern, + boolean supportsTime, + TypeConfiguration typeConfiguration) { + super( + "format", + new ArgumentTypesValidator( exactly( 2 ), TEMPORAL, STRING ), + invariant( typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) ), + invariant( typeConfiguration, TEMPORAL, STRING ) + ); + this.nativeFunctionName = nativeFunctionName; + this.reversedArguments = reversedArguments; + this.concatPattern = concatPattern; + this.supportsTime = supportsTime; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( nativeFunctionName ); + sqlAppender.append( '(' ); + final SqlAstNode expression = sqlAstArguments.get( 0 ); + final SqlAstNode format = sqlAstArguments.get( 1 ); + if ( reversedArguments ) { + format.accept( walker ); + sqlAppender.append( ',' ); + if ( !supportsTime && isTimeTemporal( expression ) ) { + sqlAppender.append( "date'1970-01-01'+" ); + } + expression.accept( walker ); + } + else { + if ( !supportsTime && isTimeTemporal( expression ) ) { + sqlAppender.append( "date'1970-01-01'+" ); + } + expression.accept( walker ); + sqlAppender.append( ',' ); + format.accept( walker ); + } + sqlAppender.append( ')' ); + } + + private boolean isTimeTemporal(SqlAstNode expression) { + if ( expression instanceof Expression ) { + final JdbcMappingContainer expressionType = ( (Expression) expression ).getExpressionType(); + if ( expressionType.getJdbcTypeCount() == 1 ) { + switch ( expressionType.getSingleJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + return true; + default: + break; + } + } + } + return false; + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + return new FormatSqmFunction<>( + this, + this, + arguments, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + concatPattern, + queryEngine + ); + } + + @Override + public String getArgumentListSignature() { + return "(TEMPORAL datetime as STRING pattern)"; + } + + protected static class FormatSqmFunction extends SelfRenderingSqmFunction { + + private final boolean supportsPatternLiterals; + private final TypeConfiguration typeConfiguration; + private QueryEngine queryEngine; + + public FormatSqmFunction( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + ReturnableType impliedResultType, + ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + boolean supportsPatternLiterals, + QueryEngine queryEngine) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + queryEngine.getCriteriaBuilder(), + "format" + ); + this.supportsPatternLiterals = supportsPatternLiterals; + this.typeConfiguration = queryEngine.getTypeConfiguration(); + this.queryEngine = queryEngine; + } + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final ReturnableType resultType = resolveResultType( walker ); + final MappingModelExpressible mappingModelExpressible = + resultType == null + ? null + : getMappingModelExpressible( walker, resultType, arguments ); + final SqlAstNode expression = arguments.get( 0 ); + if ( expression instanceof SqlTupleContainer ) { + // SqlTupleContainer means this is a composite temporal type i.e. uses `@TimeZoneStorage(COLUMN)` + // The support for this kind of type requires that we inject the offset from the second column + // as literal into the pattern, and apply the formatting on the date time part + final SqlTuple sqlTuple = ( (SqlTupleContainer) expression ).getSqlTuple(); + final FunctionRenderer timestampaddFunction = getFunction( walker, "timestampadd" ); + final BasicType integerType = typeConfiguration.getBasicTypeRegistry() + .resolve( StandardBasicTypes.INTEGER ); + arguments.set( 0, getOffsetAdjusted( sqlTuple, timestampaddFunction, integerType ) ); + if ( getArgumentsValidator() != null ) { + getArgumentsValidator().validateSqlTypes( arguments, getFunctionName() ); + } + final Format format = (Format) arguments.get( 1 ); + // If the format contains a time zone or offset, we must replace that with the offset column + if ( format.getFormat().contains( "x" ) || !supportsPatternLiterals ) { + final FunctionRenderer concatFunction = getFunction( walker, "concat" ); + final FunctionRenderer substringFunction = getFunction( walker, "substring", 3 ); + final BasicType stringType = typeConfiguration.getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); + final Dialect dialect = walker.getCreationContext().getDialect(); + Expression formatExpression = null; + final StringBuilder sb = new StringBuilder(); + final StringBuilderSqlAppender sqlAppender = new StringBuilderSqlAppender( sb ); + final String delimiter; + if ( supportsPatternLiterals ) { + dialect.appendDatetimeFormat( sqlAppender, "'a'" ); + delimiter = sb.substring( 0, sb.indexOf( "a" ) ).replace( "''", "'" ); + } + else { + delimiter = ""; + } + final String[] chunks = splitFull( "'", format.getFormat() ); + final Expression offsetExpression = sqlTuple.getExpressions().get( 1 ); + // Splitting by `'` will put actual format pattern parts to even indices and literal pattern parts + // to uneven indices. We will only replace the time zone and offset pattern in the format pattern parts + for ( int i = 0; i < chunks.length; i += 2 ) { + // The general idea is to replace the various patterns `xxx`, `xx` and `x` by concatenating + // the offset column as literal i.e. `HH:mmxxx` is translated to `HH:mm'''||offset||'''` + // xxx stands for the full offset i.e. `+01:00` + // xx stands for the medium offset i.e. `+0100` + // x stands for the small offset i.e. `+01` + final String[] fullParts = splitFull( "xxx", chunks[i] ); + for ( int j = 0; j < fullParts.length; j++ ) { + if ( fullParts[j].isEmpty() ) { + continue; + } + final String[] mediumParts = splitFull( "xx", fullParts[j] ); + for ( int k = 0; k < mediumParts.length; k++ ) { + if ( mediumParts[k].isEmpty() ) { + continue; + } + final String[] smallParts = splitFull( "x", mediumParts[k] ); + for ( int l = 0; l < smallParts.length; l++ ) { + if ( smallParts[l].isEmpty() ) { + continue; + } + sb.setLength( 0 ); + dialect.appendDatetimeFormat( sqlAppender, smallParts[l] ); + final String formatPart = sb.toString(); + if ( supportsPatternLiterals ) { + formatExpression = concat( + concatFunction, + stringType, + formatExpression, + new QueryLiteral<>( formatPart, stringType ) + ); + } + else { + formatExpression = concat( + concatFunction, + stringType, + formatExpression, + new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + List.of( + arguments.get( 0 ), + new QueryLiteral<>( formatPart, stringType ) + ), + resultType, + mappingModelExpressible + ) + ); + } + if ( l + 1 < smallParts.length ) { + // This is for `x` patterns, which require `+01` + // so we concat `substring(offset, 1, 4)` + // Since the offset is always in the full format + formatExpression = concatAsLiteral( + concatFunction, + stringType, + delimiter, + formatExpression, + createSmallOffset( + concatFunction, + substringFunction, + stringType, + integerType, + offsetExpression + ) + ); + } + } + if ( k + 1 < mediumParts.length ) { + // This is for `xx` patterns, which require `+0100` + // so we concat `substring(offset, 1, 4)||substring(offset, 4, 6)` + // Since the offset is always in the full format + formatExpression = concatAsLiteral( + concatFunction, + stringType, + delimiter, + formatExpression, + createMediumOffset( + concatFunction, + substringFunction, + stringType, + integerType, + offsetExpression + ) + ); + } + } + if ( j + 1 < fullParts.length ) { + formatExpression = concatAsLiteral( + concatFunction, + stringType, + delimiter, + formatExpression, + createFullOffset( + concatFunction, + stringType, + integerType, + offsetExpression + ) + ); + } + } + + if ( i + 1 < chunks.length ) { + // Handle the pattern literal content + final String formatLiteralPart; + if ( supportsPatternLiterals ) { + sb.setLength( 0 ); + dialect.appendDatetimeFormat( sqlAppender, "'" + chunks[i + 1] + "'" ); + formatLiteralPart = sb.toString().replace( "''", "'" ); + } + else { + formatLiteralPart = chunks[i + 1]; + } + formatExpression = concat( + concatFunction, + stringType, + formatExpression, + new QueryLiteral<>( + formatLiteralPart, + stringType + ) + ); + } + } + + if ( supportsPatternLiterals ) { + arguments.set( 1, formatExpression ); + } + else { + return formatExpression; + } + } + } + else { + if ( getArgumentsValidator() != null ) { + getArgumentsValidator().validateSqlTypes( arguments, getFunctionName() ); + } + + if ( !supportsPatternLiterals ) { + final FunctionRenderer concatFunction = getFunction( walker, "concat" ); + final BasicType stringType = typeConfiguration.getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); + Expression formatExpression = null; + final Format format = (Format) arguments.get( 1 ); + final String[] chunks = splitFull( "'", format.getFormat() ); + // Splitting by `'` will put actual format pattern parts to even indices and literal pattern parts + // to uneven indices. We need to apply the format parts and then concatenate because the pattern + // doesn't support literals + for ( int i = 0; i < chunks.length; i += 2 ) { + formatExpression = concat( + concatFunction, + stringType, + formatExpression, + new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + List.of( arguments.get( 0 ), new Format( chunks[i] ) ), + resultType, + mappingModelExpressible + ) + ); + if ( i + 1 < chunks.length ) { + // Handle the pattern literal content + formatExpression = concat( + concatFunction, + stringType, + formatExpression, + new QueryLiteral<>( chunks[i + 1], stringType ) + ); + } + } + return formatExpression; + } + } + return new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + arguments, + resultType, + mappingModelExpressible + ); + } + + private FunctionRenderer getFunction(SqmToSqlAstConverter walker, String name) { + return (FunctionRenderer) + walker.getCreationContext().getSqmFunctionRegistry().findFunctionDescriptor( name ); + } + + private FunctionRenderer getFunction(SqmToSqlAstConverter walker, String name, int argumentCount) { + final SqmFunctionDescriptor functionDescriptor = + walker.getCreationContext().getSqmFunctionRegistry() + .findFunctionDescriptor( name ); + if ( functionDescriptor instanceof MultipatternSqmFunctionDescriptor multipatternSqmFunctionDescriptor ) { + return (FunctionRenderer) multipatternSqmFunctionDescriptor.getFunction( argumentCount ); + } + else { + return (FunctionRenderer) functionDescriptor; + } + } + + private SqlAstNode getOffsetAdjusted( + SqlTuple sqlTuple, + FunctionRenderer timestampaddFunction, + BasicType integerType) { + final Expression instantExpression = sqlTuple.getExpressions().get( 0 ); + final Expression offsetExpression = sqlTuple.getExpressions().get( 1 ); + + return new SelfRenderingFunctionSqlAstExpression( + "timestampadd", + timestampaddFunction, + List.of( + new DurationUnit( TemporalUnit.SECOND, integerType ), + offsetExpression, + instantExpression + ), + (ReturnableType) instantExpression.getExpressionType(), + instantExpression.getExpressionType() + ); + } + + private Expression createFullOffset( + FunctionRenderer concatFunction, + BasicType stringType, + BasicType integerType, + Expression offsetExpression) { + if ( offsetExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString() ) { + return offsetExpression; + } + else { + // ZoneOffset as seconds + final CaseSearchedExpression caseSearchedExpression = + zoneOffsetSeconds( stringType, integerType, offsetExpression ); + final Expression hours = getHours( integerType, offsetExpression ); + final Expression minutes = getMinutes( integerType, offsetExpression ); + + final CaseSearchedExpression minuteStart = new CaseSearchedExpression( stringType ); + minuteStart.getWhenFragments().add( + new CaseSearchedExpression.WhenFragment( + new BetweenPredicate( + minutes, + new QueryLiteral<>( Integer.MIN_VALUE, integerType ), + new QueryLiteral<>( 10, integerType ), + false, + null + ), + new QueryLiteral<>( ":0", stringType ) + ) + ); + minuteStart.otherwise( new QueryLiteral<>( ":", stringType ) ); + return concat( + concatFunction, + stringType, + concat( + concatFunction, + stringType, + concat( concatFunction, stringType, caseSearchedExpression, hours ), + minuteStart + ), + minutes + ); + } + } + + private Expression createMediumOffset( + FunctionRenderer concatFunction, + FunctionRenderer substringFunction, + BasicType stringType, + BasicType integerType, + Expression offsetExpression) { + if ( offsetExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString() ) { + return concat( + concatFunction, + stringType, + createSmallOffset( + concatFunction, + substringFunction, + stringType, + integerType, + offsetExpression + ), + new SelfRenderingFunctionSqlAstExpression( + "substring", + substringFunction, + List.of( + offsetExpression, + new QueryLiteral<>( 4, integerType ), + new QueryLiteral<>( 6, integerType ) + ), + stringType, + stringType + ) + ); + } + else { + // ZoneOffset as seconds + final CaseSearchedExpression caseSearchedExpression = + zoneOffsetSeconds( stringType, integerType, offsetExpression ); + + final Expression hours = getHours( integerType, offsetExpression ); + final Expression minutes = getMinutes( integerType, offsetExpression ); + + final CaseSearchedExpression minuteStart = new CaseSearchedExpression( stringType ); + minuteStart.getWhenFragments().add( + new CaseSearchedExpression.WhenFragment( + new BetweenPredicate( + minutes, + new QueryLiteral<>( Integer.MIN_VALUE, integerType ), + new QueryLiteral<>( 10, integerType ), + false, + null + ), + new QueryLiteral<>( "0", stringType ) + ) + ); + minuteStart.otherwise( new QueryLiteral<>( "", stringType ) ); + return concat( + concatFunction, + stringType, + concat( + concatFunction, + stringType, + concat( concatFunction, stringType, caseSearchedExpression, hours ), + minuteStart + ), + minutes + ); + } + } + + private Expression createSmallOffset( + FunctionRenderer concatFunction, + FunctionRenderer substringFunction, + BasicType stringType, + BasicType integerType, + Expression offsetExpression) { + if ( offsetExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString() ) { + return new SelfRenderingFunctionSqlAstExpression( + "substring", + substringFunction, + List.of( + offsetExpression, + new QueryLiteral<>( 1, integerType ), + new QueryLiteral<>( 4, integerType ) + ), + stringType, + stringType + ); + } + else { + // ZoneOffset as seconds + final CaseSearchedExpression caseSearchedExpression = + zoneOffsetSeconds( stringType, integerType, offsetExpression ); + final Expression hours = getHours( integerType, offsetExpression ); + return concat( concatFunction, stringType, caseSearchedExpression, hours ); + } + } + + private Expression concatAsLiteral( + FunctionRenderer concatFunction, + BasicType stringType, + String delimiter, + Expression expression, + Expression expression2) { + return concat( + concatFunction, + stringType, + concat( + concatFunction, + stringType, + concat( + concatFunction, + stringType, + expression, + new QueryLiteral<>( delimiter, stringType ) + ), + expression2 + ), + new QueryLiteral<>( delimiter, stringType ) + ); + } + + private Expression concat( + FunctionRenderer concatFunction, + BasicType stringType, + Expression expression, + Expression expression2) { + if ( expression == null ) { + return expression2; + } + else if ( expression instanceof SelfRenderingFunctionSqlAstExpression selfRenderingFunction + && "concat".equals( selfRenderingFunction.getFunctionName() ) ) { + final List list = (List) selfRenderingFunction.getArguments(); + final SqlAstNode lastOperand = list.get( list.size() - 1 ); + if ( expression2 instanceof QueryLiteral literal2 + && lastOperand instanceof QueryLiteral literalOperand ) { + list.set( + list.size() - 1, + new QueryLiteral<>( + literalOperand.getLiteralValue().toString() + + literal2.getLiteralValue().toString(), + stringType + ) + ); + } + else { + list.add( expression2 ); + } + return expression; + } + else if ( expression2 instanceof SelfRenderingFunctionSqlAstExpression selfRenderingFunction + && "concat".equals( selfRenderingFunction.getFunctionName() ) ) { + final List list = (List) selfRenderingFunction.getArguments(); + final SqlAstNode firstOperand = list.get( 0 ); + if ( expression instanceof QueryLiteral literal + && firstOperand instanceof QueryLiteral literalOperand ) { + list.set( + list.size() - 1, + new QueryLiteral<>( + literal.getLiteralValue().toString() + + literalOperand.getLiteralValue().toString(), + stringType + ) + ); + } + else { + list.add( 0, expression ); + } + return expression2; + } + else if ( expression instanceof QueryLiteral literal + && expression2 instanceof QueryLiteral literal2 ) { + return new QueryLiteral<>( + literal.getLiteralValue().toString() + + literal2.getLiteralValue().toString(), + stringType + ); + } + else { + final List list = new ArrayList<>( 2 ); + list.add( expression ); + list.add( expression2 ); + return new SelfRenderingFunctionSqlAstExpression( + "concat", + concatFunction, + list, + stringType, + stringType + ); + } + } + + private Expression getHours( + BasicType integerType, + Expression offsetExpression) { + BinaryArithmeticExpression divisionExpr = new BinaryArithmeticExpression( + offsetExpression, + DIVIDE_PORTABLE, + new QueryLiteral<>(3600, integerType), + integerType + ); + return floor(divisionExpr); + } + + private Expression getMinutes( + BasicType integerType, + Expression offsetExpression){ + return new BinaryArithmeticExpression( + abs(new BinaryArithmeticExpression( + offsetExpression, + MODULO, + new QueryLiteral<>( 3600, integerType ), + integerType + )), + DIVIDE_PORTABLE, + new QueryLiteral<>( 60, integerType ), + integerType + ); + } + + private Expression abs(Expression expression) { + return new SelfRenderingFunctionSqlAstExpression( + "abs", + findSelfRenderingFunction( "abs", 2 ), + List.of( expression ), + (ReturnableType) expression.getExpressionType(), + expression.getExpressionType() + ); + } + + private Expression floor(Expression expression) { + return new SelfRenderingFunctionSqlAstExpression( + "floor", + findSelfRenderingFunction( "floor", 2 ), + List.of( expression ), + (ReturnableType) expression.getExpressionType(), + expression.getExpressionType() + ); + } + + private FunctionRenderer findSelfRenderingFunction(String functionName, int argumentCount) { + final SqmFunctionDescriptor functionDescriptor = + queryEngine.getSqmFunctionRegistry() + .findFunctionDescriptor( functionName ); + if ( functionDescriptor instanceof MultipatternSqmFunctionDescriptor multiPatternFunction ) { + return (FunctionRenderer) multiPatternFunction.getFunction( argumentCount ); + } + return (FunctionRenderer) functionDescriptor; + } + } + + private static CaseSearchedExpression zoneOffsetSeconds(BasicType stringType, BasicType integerType, Expression offsetExpression) { + final CaseSearchedExpression caseSearchedExpression = new CaseSearchedExpression(stringType); + caseSearchedExpression.getWhenFragments().add( + new CaseSearchedExpression.WhenFragment( + new ComparisonPredicate( + offsetExpression, + LESS_THAN_OR_EQUAL, + new QueryLiteral<>( -36000, integerType) + ), + new QueryLiteral<>( "-", stringType) + ) + ); + caseSearchedExpression.getWhenFragments().add( + new CaseSearchedExpression.WhenFragment( + new ComparisonPredicate( + offsetExpression, + LESS_THAN, + new QueryLiteral<>( 0, integerType) + ), + new QueryLiteral<>( "-0", stringType) + ) + ); + caseSearchedExpression.getWhenFragments().add( + new CaseSearchedExpression.WhenFragment( + new ComparisonPredicate( + offsetExpression, + GREATER_THAN_OR_EQUAL, + new QueryLiteral<>( 36000, integerType) + ), + new QueryLiteral<>( "+", stringType) + ) + ); + caseSearchedExpression.otherwise( new QueryLiteral<>( "+0", stringType) ); + return caseSearchedExpression; + } + + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBMinMaxFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBMinMaxFunction.java new file mode 100644 index 000000000000..6603e4d230c7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBMinMaxFunction.java @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.List; + +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.SqlTypes; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.COMPARABLE; + +/** + * GaussDB doesn't support min/max for uuid yet, + * but since that type is comparable we want to support this operation. + * The workaround is to cast uuid to text and aggregate that, which preserves the ordering, + * and finally cast the result back to uuid. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLDialect. + */ +public class GaussDBMinMaxFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public GaussDBMinMaxFunction(String name) { + super( + name, + FunctionKind.AGGREGATE, + new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 1 ), COMPARABLE ), + StandardFunctionReturnTypeResolvers.useFirstNonNull(), + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.getSessionFactory().getJdbcServices().getDialect().supportsFilterClause(); + sqlAppender.appendSql( getName() ); + sqlAppender.appendSql( '(' ); + final Expression arg = (Expression) sqlAstArguments.get( 0 ); + final String castTarget; + if ( caseWrapper ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + castTarget = renderArgument( sqlAppender, translator, arg ); + sqlAppender.appendSql( " else null end)" ); + } + else { + castTarget = renderArgument( sqlAppender, translator, arg ); + sqlAppender.appendSql( ')' ); + if ( filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } + if ( castTarget != null ) { + sqlAppender.appendSql( "::" ); + sqlAppender.appendSql( castTarget ); + } + } + + private String renderArgument(SqlAppender sqlAppender, SqlAstTranslator translator, Expression arg) { + final JdbcMapping sourceMapping = arg.getExpressionType().getSingleJdbcMapping(); + // Cast uuid expressions to "text" first, aggregate that, and finally cast to uuid again + if ( sourceMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.UUID ) { + sqlAppender.appendSql( "cast(" ); + arg.accept( translator ); + sqlAppender.appendSql( " as text)" ); + return "uuid"; + } + else { + arg.accept( translator ); + return null; + } + } + + @Override + public String getArgumentListSignature() { + return "(COMPARABLE arg)"; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncFunction.java new file mode 100644 index 000000000000..9c0a7735200e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncFunction.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmExtractUnit; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Custom {@link TruncFunction} for GaussDB which uses the dialect-specific function for numeric truncation + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLTruncFunction. + */ +public class GaussDBTruncFunction extends TruncFunction { + private final GaussDBTruncRoundFunction gaussDBTruncRoundFunction; + + public GaussDBTruncFunction(boolean supportsTwoArguments, TypeConfiguration typeConfiguration) { + super( + "trunc(?1)", + null, + DatetimeTrunc.DATE_TRUNC, + null, + typeConfiguration + ); + this.gaussDBTruncRoundFunction = new GaussDBTruncRoundFunction( "trunc", supportsTwoArguments ); + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + final List> args = new ArrayList<>( arguments ); + if ( arguments.size() != 2 || !( arguments.get( 1 ) instanceof SqmExtractUnit ) ) { + // numeric truncation + return gaussDBTruncRoundFunction.generateSqmFunctionExpression( + arguments, + impliedResultType, + queryEngine + ); + } + // datetime truncation + return new SelfRenderingSqmFunction<>( + this, + datetimeRenderingSupport, + args, + impliedResultType, + TruncArgumentsValidator.DATETIME_VALIDATOR, + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncRoundFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncRoundFunction.java new file mode 100644 index 000000000000..62e61765e6b4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/GaussDBTruncRoundFunction.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC; + +/** + * GaussDB only supports the two-argument {@code trunc} and {@code round} functions + * with the following signatures: + *
    + *
  • {@code trunc(numeric, integer)}
  • + *
  • {@code round(numeric, integer)}
  • + *
+ *

+ * This custom function falls back to using {@code floor} as a workaround only when necessary, + * e.g. when there are 2 arguments to the function and either: + *

    + *
  • The first argument is not of type {@code numeric}
  • + * or + *
  • The dialect doesn't support the two-argument {@code trunc} function
  • + *
+ * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLTruncRoundFunction. + */ +public class GaussDBTruncRoundFunction extends AbstractSqmFunctionDescriptor implements FunctionRenderer { + private final boolean supportsTwoArguments; + + public GaussDBTruncRoundFunction(String name, boolean supportsTwoArguments) { + super( + name, + new ArgumentTypesValidator( StandardArgumentsValidators.between( 1, 2 ), NUMERIC, INTEGER ), + StandardFunctionReturnTypeResolvers.useArgType( 1 ), + StandardFunctionArgumentTypeResolvers.invariant( NUMERIC, INTEGER ) + ); + this.supportsTwoArguments = supportsTwoArguments; + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final int numberOfArguments = arguments.size(); + final Expression firstArg = (Expression) arguments.get( 0 ); + final JdbcType jdbcType = firstArg.getExpressionType().getSingleJdbcMapping().getJdbcType(); + if ( numberOfArguments == 1 || supportsTwoArguments && jdbcType.isDecimal() ) { + // use native two-argument function + sqlAppender.appendSql( getName() ); + sqlAppender.appendSql( "(" ); + firstArg.accept( walker ); + if ( numberOfArguments > 1 ) { + sqlAppender.appendSql( ", " ); + arguments.get( 1 ).accept( walker ); + } + sqlAppender.appendSql( ")" ); + } + else { + // workaround using floor + if ( getName().equals( "trunc" ) ) { + sqlAppender.appendSql( "sign(" ); + firstArg.accept( walker ); + sqlAppender.appendSql( ")*floor(abs(" ); + firstArg.accept( walker ); + sqlAppender.appendSql( ")*1e" ); + arguments.get( 1 ).accept( walker ); + } + else { + sqlAppender.appendSql( "floor(" ); + firstArg.accept( walker ); + sqlAppender.appendSql( "*1e" ); + arguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( "+0.5" ); + } + sqlAppender.appendSql( ")/1e" ); + arguments.get( 1 ).accept( walker ); + } + } + + @Override + public String getArgumentListSignature() { + return "(NUMERIC number[, INTEGER places])"; + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + return new SelfRenderingSqmFunction<>( + this, + this, + arguments, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java index 74d88eab2f87..65ab05672969 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java @@ -24,7 +24,7 @@ */ public class ArrayRemoveIndexUnnestFunction extends AbstractSqmSelfRenderingFunctionDescriptor { - private final boolean castEmptyArrayLiteral; + protected final boolean castEmptyArrayLiteral; public ArrayRemoveIndexUnnestFunction(boolean castEmptyArrayLiteral) { super( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatElementFunction.java new file mode 100644 index 000000000000..c25eb4b12dc6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatElementFunction.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.engine.jdbc.Size; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.BasicPluralType; + +/** + * GaussDB variant of the function to properly return {@code null} when the array argument is null. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayConcatElementFunction. + */ +public class GaussDBArrayConcatElementFunction extends ArrayConcatElementFunction { + + public GaussDBArrayConcatElementFunction(boolean prepend) { + super( "", "||", "", prepend ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression firstArgument = (Expression) sqlAstArguments.get( 0 ); + final Expression secondArgument = (Expression) sqlAstArguments.get( 1 ); + final Expression arrayArgument; + final Expression elementArgument; + if ( prepend ) { + elementArgument = firstArgument; + arrayArgument = secondArgument; + } + else { + arrayArgument = firstArgument; + elementArgument = secondArgument; + } + final String elementCastType; + if ( needsElementCasting( elementArgument ) ) { + final JdbcMappingContainer arrayType = arrayArgument.getExpressionType(); + final Size size = arrayType instanceof SqlTypedMapping ? ( (SqlTypedMapping) arrayType ).toSize() : null; + elementCastType = DdlTypeHelper.getCastTypeName( + ( (BasicPluralType) returnType ).getElementType(), + size, + walker.getSessionFactory().getTypeConfiguration() + ); + } + else { + elementCastType = null; + } + sqlAppender.append( "case when " ); + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( " is not null then " ); + if ( prepend && elementCastType != null) { + sqlAppender.append( "cast(" ); + walker.render( firstArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( " as " ); + sqlAppender.append( elementCastType ); + sqlAppender.append( ')' ); + } + else { + walker.render( firstArgument, SqlAstNodeRenderingMode.DEFAULT ); + } + sqlAppender.append( "||" ); + if ( !prepend && elementCastType != null) { + sqlAppender.append( "cast(" ); + walker.render( secondArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( " as " ); + sqlAppender.append( elementCastType ); + sqlAppender.append( ')' ); + } + else { + walker.render( secondArgument, SqlAstNodeRenderingMode.DEFAULT ); + } + sqlAppender.append( " end" ); + } + + private static boolean needsElementCasting(Expression elementExpression) { + // GaussDB needs casting of null and string literal expressions + return elementExpression instanceof Literal && ( + elementExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString() + || ( (Literal) elementExpression ).getLiteralValue() == null + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatFunction.java new file mode 100644 index 000000000000..b09fea3db34b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConcatFunction.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * GaussDB variant of the function to properly return {@code null} when one of the arguments is null. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayConcatFunction. + */ +public class GaussDBArrayConcatFunction extends ArrayConcatFunction { + + public GaussDBArrayConcatFunction() { + super( "", "||", "" ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.append( "case when " ); + String separator = ""; + for ( SqlAstNode node : sqlAstArguments ) { + sqlAppender.append( separator ); + node.accept( walker ); + sqlAppender.append( " is not null" ); + separator = " and "; + } + + sqlAppender.append( " then " ); + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + sqlAppender.append( " end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConstructorFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConstructorFunction.java new file mode 100644 index 000000000000..2712f2ec4b93 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayConstructorFunction.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; + +/** + * Special array constructor function that also applies a cast to the array literal, + * based on the inferred result type. GaussDB needs this, + * because by default it assumes a {@code text[]}, which is not compatible with {@code varchar[]}. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayConstructorFunction. + */ +public class GaussDBArrayConstructorFunction extends ArrayConstructorFunction { + + public GaussDBArrayConstructorFunction(boolean list) { + super( list, true ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + sqlAppender.append( "cast(" ); + } + } + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + if ( arrayTypeName != null ) { + sqlAppender.appendSql( " as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( ')' ); + } + } + + private static boolean needsArrayCasting(BasicType elementType) { + // GaussDB doesn't do implicit conversion between text[] and varchar[], so we need casting + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayContainsOperatorFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayContainsOperatorFunction.java new file mode 100644 index 000000000000..8a24678c9a73 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayContainsOperatorFunction.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +/** + * Special array contains function that also applies a cast to the element argument. PostgreSQL needs this, + * because by default it assumes a {@code text[]}, which is not compatible with {@code varchar[]}. + * @author chenzhida + * + * Notes: Original code of this class is based on ArrayContainsOperatorFunction. + */ +public class GaussDBArrayContainsOperatorFunction extends ArrayContainsUnnestFunction { + + public GaussDBArrayContainsOperatorFunction(boolean nullable, TypeConfiguration typeConfiguration) { + super( nullable, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression haystackExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression needleExpression = (Expression) sqlAstArguments.get( 1 ); + final JdbcMappingContainer needleTypeContainer = needleExpression.getExpressionType(); + final JdbcMapping needleType = needleTypeContainer == null ? null : needleTypeContainer.getSingleJdbcMapping(); + if ( needleType == null || needleType instanceof BasicPluralType ) { + LOG.deprecatedArrayContainsWithArray(); + if ( nullable ) { + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + } + else { + haystackExpression.accept( walker ); + sqlAppender.append( "@>" ); + needleExpression.accept( walker ); + } + } + else { + if ( nullable ) { + sqlAppender.append( "(array_positions(" ); + haystackExpression.accept( walker ); + sqlAppender.append( ',' ); + needleExpression.accept( walker ); + sqlAppender.append( "))[1] is not null" ); + } + else { + haystackExpression.accept( walker ); + sqlAppender.append( "@>" ); + if ( needsArrayCasting( needleExpression ) ) { + sqlAppender.append( "cast(array[" ); + needleExpression.accept( walker ); + sqlAppender.append( "] as " ); + sqlAppender.append( DdlTypeHelper.getCastTypeName( + haystackExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ) ); + sqlAppender.append( ')' ); + } + else { + sqlAppender.append( "array[" ); + needleExpression.accept( walker ); + sqlAppender.append( ']' ); + } + } + } + } + + private static boolean needsArrayCasting(Expression elementExpression) { + // Gauss doesn't do implicit conversion between text[] and varchar[], so we need casting + return elementExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayFillFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayFillFunction.java new file mode 100644 index 000000000000..f3243bd0eb58 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayFillFunction.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; + +/** + * Custom casting for the array fill function. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayFillFunction. + */ +public class GaussDBArrayFillFunction extends AbstractArrayFillFunction { + + public GaussDBArrayFillFunction(boolean list) { + super( list ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.append( "array_fill(" ); + final String elementCastType; + final Expression elementExpression = (Expression) sqlAstArguments.get( 0 ); + if ( needsElementCasting( elementExpression ) ) { + elementCastType = DdlTypeHelper.getCastTypeName( + elementExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ); + sqlAppender.append( "cast(" ); + } + else { + elementCastType = null; + } + sqlAstArguments.get( 0 ).accept( walker ); + if ( elementCastType != null ) { + sqlAppender.append( " as " ); + sqlAppender.append( elementCastType ); + sqlAppender.append( ')' ); + } + sqlAppender.append( ",array[" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.append( "])" ); + } + + private static boolean needsElementCasting(Expression elementExpression) { + // GaussDB needs casting of null and string literal expressions + return elementExpression instanceof Literal && ( + elementExpression.getExpressionType().getSingleJdbcMapping().getJdbcType().isString() + || ( (Literal) elementExpression ).getLiteralValue() == null + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionFunction.java new file mode 100644 index 000000000000..c1db4df41aa0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionFunction.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * GaussDB variant of the function. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayPositionFunction. + */ +public class GaussDBArrayPositionFunction extends AbstractArrayPositionFunction { + + public GaussDBArrayPositionFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression elementExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "(array_positions(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ", " ); + walker.render( elementExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "))[1]" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionsFunction.java new file mode 100644 index 000000000000..8b3b8b2643cc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayPositionsFunction.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * GaussDB variant of the function. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLArrayPositionsFunction. + */ +public class GaussDBArrayPositionsFunction extends AbstractArrayPositionsFunction { + + public GaussDBArrayPositionsFunction(boolean list, TypeConfiguration typeConfiguration) { + super( list, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression elementExpression = (Expression) sqlAstArguments.get( 1 ); + sqlAppender.append( "array_positions(" ); + walker.render( arrayExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( ',' ); + walker.render( elementExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveFunction.java new file mode 100644 index 000000000000..1ed86b03cdcf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveFunction.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; + +import java.util.List; + +/** + * GaussDB array_remove function. + * @author chenzhida + */ +public class GaussDBArrayRemoveFunction extends AbstractArrayRemoveFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "CASE WHEN "); + arrayExpression.accept( walker ); + sqlAppender.append( " IS NULL THEN NULL ELSE COALESCE(( SELECT array_agg(val) FROM unnest("); + arrayExpression.accept( walker ); + sqlAppender.append( ") AS val" ); + + if ( indexExpression instanceof Literal ) { + Literal literal = (Literal) indexExpression; + Object literalValue = literal.getLiteralValue(); + if ( literalValue != null ) { + appendWhere( sqlAppender, walker, indexExpression ); + } + else { + sqlAppender.append( " where val IS NOT NULL" ); + } + } + else { + appendWhere( sqlAppender, walker, indexExpression ); + } + sqlAppender.append( "), CAST(ARRAY[] AS VARCHAR[]) ) END AS result_array" ); + } + + /** + * can not get value if type like string + * @param sqlAppender + * @param walker + * @param indexExpression + */ + private static void appendWhere(SqlAppender sqlAppender, SqlAstTranslator walker, Expression indexExpression) { + sqlAppender.append( " where val IS NULL OR val not in (" ); + indexExpression.accept( walker ); + sqlAppender.append( ")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveIndexFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveIndexFunction.java new file mode 100644 index 000000000000..375b2aaf3ca9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayRemoveIndexFunction.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; + +import java.util.List; + +/** + * GaussDB array_remove index function. + * @author chenzhida + */ +public class GaussDBArrayRemoveIndexFunction extends ArrayRemoveIndexUnnestFunction { + + + + public GaussDBArrayRemoveIndexFunction(boolean castEmptyArrayLiteral) { + super( castEmptyArrayLiteral ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "case when "); + arrayExpression.accept( walker ); + sqlAppender.append( " IS NOT NULL THEN COALESCE((SELECT array_agg(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "[idx]) FROM generate_subscripts(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ", 1) AS idx " ); + + if ( indexExpression instanceof Literal ) { + Literal literal = (Literal) indexExpression; + Object literalValue = literal.getLiteralValue(); + if ( literalValue != null ) { + appendWhere( sqlAppender, walker, indexExpression ); + } + } + else { + appendWhere( sqlAppender, walker, indexExpression ); + } + + sqlAppender.append( "), CAST(ARRAY[] AS VARCHAR ARRAY)) " ); + if ( castEmptyArrayLiteral ) { + sqlAppender.append( "ELSE CAST(ARRAY[] AS VARCHAR ARRAY) " ); + } + sqlAppender.append( "END AS result_array" ); + } + + private static void appendWhere(SqlAppender sqlAppender, SqlAstTranslator walker, Expression indexExpression) { + sqlAppender.append( "where idx not in (" ); + indexExpression.accept( walker ); + sqlAppender.append( ")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayReplaceFunction.java new file mode 100644 index 000000000000..f48540f8b81b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayReplaceFunction.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; + +import java.util.List; + +/** + * GaussDB array_replace function. + * @author chenzhida + */ +public class GaussDBArrayReplaceFunction extends ArrayReplaceUnnestFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + sqlAppender.append( "CASE WHEN "); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.append( " IS NULL THEN NULL ELSE COALESCE((SELECT array_agg(CASE "); + final Expression originValueExpression = (Expression) sqlAstArguments.get( 1 ); + if ( originValueExpression instanceof Literal ) { + Literal literal = (Literal) originValueExpression; + Object literalValue = literal.getLiteralValue(); + if ( literalValue != null ) { + sqlAppender.append( "WHEN val = "); + sqlAstArguments.get( 1 ).accept( walker ); + } + else { + sqlAppender.append( "WHEN val is null "); + } + } + else { + sqlAppender.append( "WHEN val = "); + sqlAstArguments.get( 1 ).accept( walker ); + } + sqlAppender.append( " THEN "); + sqlAstArguments.get( 2 ).accept( walker ); + sqlAppender.append( " ELSE val END) FROM unnest( "); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.append( ") AS val ), CAST(ARRAY[] AS VARCHAR[]) ) END AS result_array"); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArraySetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArraySetFunction.java new file mode 100644 index 000000000000..9b4dfddddce4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArraySetFunction.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +import java.util.List; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; + +/** + * GaussDB array_set function. + * @author chenzhida + */ +public class GaussDBArraySetFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public GaussDBArraySetFunction() { + super( + "array_set", + StandardArgumentsValidators.composite( + new ArrayAndElementArgumentValidator( 0, 2 ), + new ArgumentTypesValidator( null, ANY, INTEGER, ANY ) + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, + StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, ANY ), + new ArrayAndElementArgumentTypeResolver( 0, 2 ) + ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); + final Expression elementExpression = (Expression) sqlAstArguments.get( 2 ); + + sqlAppender.append( "( SELECT array_agg( CASE WHEN idx_gen = "); + indexExpression.accept( walker ); + sqlAppender.append( " THEN "); + elementExpression.accept( walker ); + sqlAppender.append( " ELSE CASE WHEN idx_gen <= array_length(ewa1_0.the_array, 1) "); + sqlAppender.append( " THEN ewa1_0.the_array[idx_gen] ELSE NULL END END ORDER BY idx_gen ) "); + sqlAppender.append( " FROM generate_series(1, GREATEST(COALESCE(array_length( "); + arrayExpression.accept( walker ); + sqlAppender.append( " , 1), 0), "); + indexExpression.accept( walker ); + sqlAppender.append( " )) AS idx_gen ) AS result_array "); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayTrimFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayTrimFunction.java new file mode 100644 index 000000000000..24ca596e3d9f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/GaussDBArrayTrimFunction.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +import java.util.List; + +/** + * GaussDB array_trim function. + * @author chenzhida + * + * Notes: Original code of this class is based on PostgreSQLArrayTrimEmulation. + */ +public class GaussDBArrayTrimFunction extends AbstractArrayTrimFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression lengthExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "array_trim("); + arrayExpression.accept( walker ); + sqlAppender.append( ","); + lengthExpression.accept( walker ); + sqlAppender.append( ")"); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/GaussDBJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/GaussDBJsonObjectFunction.java new file mode 100644 index 000000000000..3d0fe3a85afa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/GaussDBJsonObjectFunction.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +/** + * GaussDB json_object function. + * @author chenzhida + * + * Notes: Original code of this class is based on PostgreSQLJsonObjectFunction. + */ +public class GaussDBJsonObjectFunction extends JsonObjectFunction { + + public GaussDBJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + sqlAppender.appendSql( "json_build_object" ); + char separator = '('; + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( separator ); + } + else { + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( ( sqlAstArguments.size() & 1 ) == 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( sqlAstArguments.size() - 1 ); + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + sqlAppender.appendSql('('); + separator = ' '; + for ( int i = 0; i < argumentsCount; i += 2 ) { + final SqlAstNode key = sqlAstArguments.get( i ); + Expression valueNode = (Expression) sqlAstArguments.get( i+1 ); + if ( nullBehavior == JsonNullBehavior.ABSENT && walker.getLiteralValue( valueNode ) == null) { + continue; + } + if (separator != ' ') { + sqlAppender.appendSql(separator); + } + else { + separator = ','; + } + key.accept( walker ); + sqlAppender.appendSql( ',' ); + valueNode.accept( walker ); + } + } + sqlAppender.appendSql( ')' ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/identity/GaussDBIdentityColumnSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/identity/GaussDBIdentityColumnSupport.java new file mode 100644 index 000000000000..175f7d8a2280 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/identity/GaussDBIdentityColumnSupport.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.identity; + +import static org.hibernate.internal.util.StringHelper.unquote; + +/** + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLIdentityColumnSupport. + */ +public class GaussDBIdentityColumnSupport extends IdentityColumnSupportImpl { + + public static final GaussDBIdentityColumnSupport INSTANCE = new GaussDBIdentityColumnSupport(); + + @Override + public boolean supportsIdentityColumns() { + return true; + } + + @Override + public boolean hasDataTypeInIdentityColumn() { + return false; + } + + @Override + public String getIdentitySelectString(String table, String column, int type) { + return "select currval('" + unquote(table) + '_' + unquote(column) + "_seq')"; + } + + @Override + public String getIdentityColumnString(int type) { + return "bigserial"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sequence/GaussDBSequenceSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/sequence/GaussDBSequenceSupport.java new file mode 100644 index 000000000000..9b97eaf0b70b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sequence/GaussDBSequenceSupport.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.sequence; + +import org.hibernate.MappingException; +import org.hibernate.dialect.GaussDBDialect; + +/** + * Sequence support for {@link GaussDBDialect}. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLAggregateSupport. + */ +public class GaussDBSequenceSupport implements SequenceSupport { + + public static final SequenceSupport INSTANCE = new GaussDBSequenceSupport(); + + @Override + public String getSelectSequenceNextValString(String sequenceName) { + return "nextval('" + sequenceName + "')"; + } + + @Override + public String getSelectSequencePreviousValString(String sequenceName) throws MappingException { + return "currval('" + sequenceName + "')"; + } + + @Override + public boolean sometimesNeedsStartingValue() { + return true; + } + + @Override + public String getDropSequenceString(String sequenceName) { + return "drop sequence if exists " + sequenceName; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/GaussDBCallableStatementSupport.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/GaussDBCallableStatementSupport.java new file mode 100644 index 000000000000..b7e2818dbc0a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/GaussDBCallableStatementSupport.java @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.procedure.internal; + +import java.util.List; + +import org.hibernate.HibernateException; +import org.hibernate.dialect.AbstractGaussDBStructJdbcType; +import org.hibernate.procedure.spi.FunctionReturnImplementor; +import org.hibernate.procedure.spi.ProcedureCallImplementor; +import org.hibernate.procedure.spi.ProcedureParameterImplementor; +import org.hibernate.query.OutputableType; +import org.hibernate.query.spi.ProcedureParameterMetadataImplementor; +import org.hibernate.sql.exec.internal.JdbcCallImpl; +import org.hibernate.sql.exec.spi.JdbcCallParameterRegistration; +import org.hibernate.sql.exec.spi.JdbcOperationQueryCall; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.ParameterMode; + +/** + * GaussDB implementation of CallableStatementSupport. + * + * @author liubao + * + * Notes: Original code of this class is based on PostgreSQLTruncFunction. + */ +public class GaussDBCallableStatementSupport extends AbstractStandardCallableStatementSupport { + /** + * Singleton access + */ + public static final GaussDBCallableStatementSupport INSTANCE = new GaussDBCallableStatementSupport( true ); + public static final GaussDBCallableStatementSupport V10_INSTANCE = new GaussDBCallableStatementSupport( false ); + + private final boolean supportsProcedures; + + private GaussDBCallableStatementSupport(boolean supportsProcedures) { + this.supportsProcedures = supportsProcedures; + } + + @Override + public JdbcOperationQueryCall interpretCall(ProcedureCallImplementor procedureCall) { + final String procedureName = procedureCall.getProcedureName(); + final FunctionReturnImplementor functionReturn = procedureCall.getFunctionReturn(); + final ProcedureParameterMetadataImplementor parameterMetadata = procedureCall.getParameterMetadata(); + final boolean firstParamIsRefCursor = parameterMetadata.getParameterCount() != 0 + && isFirstParameterModeRefCursor( parameterMetadata ); + + final List> registrations = parameterMetadata.getRegistrationsAsList(); + final int paramStringSizeEstimate; + if ( functionReturn == null && parameterMetadata.hasNamedParameters() ) { + // That's just a rough estimate. I guess most params will have fewer than 8 chars on average + paramStringSizeEstimate = registrations.size() * 10; + } + else { + // For every param rendered as '?' we have a comma, hence the estimate + paramStringSizeEstimate = registrations.size() * 2; + } + final JdbcCallImpl.Builder builder = new JdbcCallImpl.Builder(); + + final int jdbcParameterOffset; + final int startIndex; + final CallMode callMode; + if ( functionReturn != null ) { + if ( functionReturn.getJdbcTypeCode() == SqlTypes.REF_CURSOR ) { + if ( firstParamIsRefCursor ) { + // validate that the parameter strategy is positional (cannot mix, and REF_CURSOR is inherently positional) + if ( parameterMetadata.hasNamedParameters() ) { + throw new HibernateException( "Cannot mix named parameters and REF_CURSOR parameter on GaussDB" ); + } + callMode = CallMode.CALL_RETURN; + startIndex = 1; + jdbcParameterOffset = 1; + builder.addParameterRegistration( registrations.get( 0 ).toJdbcParameterRegistration( 1, procedureCall ) ); + } + else { + callMode = CallMode.TABLE_FUNCTION; + startIndex = 0; + jdbcParameterOffset = 1; + // Old style +// callMode = CallMode.CALL_RETURN; +// startIndex = 0; +// jdbcParameterOffset = 2; +// builder.setFunctionReturn( functionReturn.toJdbcFunctionReturn( procedureCall.getSession() ) ); + } + } + else { + callMode = CallMode.FUNCTION; + startIndex = 0; + jdbcParameterOffset = 1; + } + } + else if ( supportsProcedures ) { + jdbcParameterOffset = 1; + startIndex = 0; + callMode = CallMode.NATIVE_CALL; + } + else if ( firstParamIsRefCursor ) { + // validate that the parameter strategy is positional (cannot mix, and REF_CURSOR is inherently positional) + if ( parameterMetadata.hasNamedParameters() ) { + throw new HibernateException( "Cannot mix named parameters and REF_CURSOR parameter on GaussDB" ); + } + jdbcParameterOffset = 1; + startIndex = 1; + callMode = CallMode.CALL_RETURN; + builder.addParameterRegistration( registrations.get( 0 ).toJdbcParameterRegistration( 1, procedureCall ) ); + } + else { + jdbcParameterOffset = 1; + startIndex = 0; + callMode = CallMode.CALL; + } + + final StringBuilder buffer = new StringBuilder( callMode.start.length() + callMode.end.length() + procedureName.length() + paramStringSizeEstimate ) + .append( callMode.start ); + buffer.append( procedureName ); + + if ( startIndex == registrations.size() ) { + buffer.append( '(' ); + } + else { + char sep = '('; + for ( int i = startIndex; i < registrations.size(); i++ ) { + final ProcedureParameterImplementor parameter = registrations.get( i ); + if ( !supportsProcedures && parameter.getMode() == ParameterMode.REF_CURSOR ) { + throw new HibernateException( + "GaussDB supports only one REF_CURSOR parameter, but multiple were registered" ); + } + buffer.append( sep ); + final JdbcCallParameterRegistration registration = parameter.toJdbcParameterRegistration( + i + jdbcParameterOffset, + procedureCall + ); + final OutputableType type = registration.getParameterType(); + final String castType; + if ( parameter.getName() != null ) { + buffer.append( parameter.getName() ).append( " => " ); + } + if ( type != null && type.getJdbcType() instanceof AbstractGaussDBStructJdbcType ) { + // We have to cast struct type parameters so that GaussDB understands nulls + castType = ( (AbstractGaussDBStructJdbcType) type.getJdbcType() ).getStructTypeName(); + buffer.append( "cast(" ); + } + else { + castType = null; + } + buffer.append( "?" ); + if ( castType != null ) { + buffer.append( " as " ).append( castType ).append( ')' ); + } + sep = ','; + builder.addParameterRegistration( registration ); + } + } + + buffer.append( callMode.end ); + builder.setCallableName( buffer.toString() ); + return builder.buildJdbcCall(); + } + + private static boolean isFirstParameterModeRefCursor(ProcedureParameterMetadataImplementor parameterMetadata) { + return parameterMetadata.getRegistrationsAsList().get( 0 ).getMode() == ParameterMode.REF_CURSOR; + } + + enum CallMode { + TABLE_FUNCTION("select * from ", ")"), + FUNCTION("select ", ")"), + NATIVE_CALL("call ", ")"), + CALL_RETURN("{?=call ", ")}"), + CALL("{call ", ")}"); + + private final String start; + private final String end; + + CallMode(String start, String end) { + this.start = start; + this.end = end; + } + + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index d140524d8bd0..7f50687262fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -2050,6 +2050,43 @@ protected void visitOnDuplicateKeyConflictClause(ConflictClause conflictClause) clauseStack.pop(); } + protected void visitOnDuplicateKeyConflictClauseWithDoNothing(ConflictClause conflictClause) { + if ( conflictClause == null ) { + return; + } + // The duplicate key clause does not support specifying the constraint name or constraint column names, + // but to allow compatibility, we have to require the user to specify either one in the SQM conflict clause. + // To allow meaningful usage, we simply ignore the constraint column names in this emulation. + // A possible problem with this is when the constraint column names contain the primary key columns, + // but the insert fails due to a unique constraint violation. This emulation will not cause a failure to be + // propagated, but instead will run the respective conflict action. + final String constraintName = conflictClause.getConstraintName(); + if ( constraintName != null ) { + if ( conflictClause.isDoUpdate() ) { + throw new IllegalQueryOperationException( "Insert conflict 'do update' clause with constraint name is not supported" ); + } + else { + return; + } + } + clauseStack.push( Clause.CONFLICT ); + appendSql( " on duplicate key update" ); + final List assignments = conflictClause.getAssignments(); + if ( assignments.isEmpty() ) { + try { + clauseStack.push( Clause.SET ); + appendSql( " nothing " ); + } + finally { + clauseStack.pop(); + } + } + else { + renderPredicatedSetAssignments( assignments, conflictClause.getPredicate() ); + } + clauseStack.pop(); + } + private void renderPredicatedSetAssignments(List assignments, Predicate predicate) { char separator = ' '; try { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java index e6bd54878626..7683ea53de72 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java @@ -11,9 +11,11 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.AbstractCollection; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; @@ -96,6 +98,33 @@ public static String arrayToString( return sb.toString(); } + public static String parseJsonPath(String path) { + if (path == null || !path.startsWith("$")) { + throw new IllegalArgumentException("Invalid JSON path"); + } + + List result = new ArrayList<>(); + String[] parts = path.substring(1).split("\\."); + + for (String part : parts) { + while (part.contains("[")) { + int start = part.indexOf("["); + int end = part.indexOf("]", start); + if (end == -1) { + throw new IllegalArgumentException("Invalid JSON path format"); + } + result.add(part.substring(0, start)); + result.add(part.substring(start + 1, end)); + part = part.substring(end + 1); + } + if (!part.isEmpty()) { + result.add(part); + } + } + + return String.join(",", result); + } + private static void toString(EmbeddableMappingType embeddableMappingType, Object value, WrapperOptions options, JsonAppender appender) { toString( embeddableMappingType, options, appender, value, '{' ); appender.append( '}' ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/ElementCollectionOfEmbeddableWithEntityWithEntityCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/ElementCollectionOfEmbeddableWithEntityWithEntityCollectionTest.java index 689e5de9a2c1..a3554c13560b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/ElementCollectionOfEmbeddableWithEntityWithEntityCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/ElementCollectionOfEmbeddableWithEntityWithEntityCollectionTest.java @@ -122,7 +122,8 @@ public void testInitializeCollection(SessionFactoryScope scope) { } @Entity(name = "Plan") - @Table(name = "PLAN_TABLE") + // add a table prefix to avoid conflict with system view + @Table(name = "PLAN_TEST_TABLE") public static class Plan { @Id public Integer id; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyBasicFieldMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyBasicFieldMergeTest.java index 12b7b9905b3a..74d919893e43 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyBasicFieldMergeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyBasicFieldMergeTest.java @@ -20,6 +20,8 @@ import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.dialect.GaussDBDialect; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,6 +43,7 @@ public class LazyBasicFieldMergeTest { @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB does not support byte array operations through lob type") public void test(SessionFactoryScope scope) { scope.inTransaction( session -> { Manager manager = new Manager(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyInitializationWithoutInlineDirtyTrackingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyInitializationWithoutInlineDirtyTrackingTest.java index 3f361fe9c266..f7cfa442f134 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyInitializationWithoutInlineDirtyTrackingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyInitializationWithoutInlineDirtyTrackingTest.java @@ -21,6 +21,8 @@ import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.dialect.GaussDBDialect; import org.junit.jupiter.api.Test; /** @@ -38,6 +40,7 @@ public class LazyInitializationWithoutInlineDirtyTrackingTest { @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB does not support byte array operations through lob type") public void test(SessionFactoryScope scope) { scope.inTransaction( s -> { File file = new File(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinTest.java index d96296c4b93d..67c9bbcf5f32 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinTest.java @@ -112,7 +112,8 @@ private void executeQuery(SessionFactoryScope scope, boolean criteria, boolean l final EntityGraph entityGraph = session.getEntityGraph( "test-graph" ); final List resultList = query.setHint( HINT_SPEC_FETCH_GRAPH, entityGraph ).getResultList(); assertThat( resultList ).hasSize( 2 ); - assertThat( resultList.stream().map( p -> p.getAddress().getId() ) ).containsExactly( 1L, 2L ); + // No order by, there is no guarantee of the data order. + assertThat( resultList.stream().map( p -> p.getAddress().getId() ) ).containsExactlyInAnyOrder( 1L, 2L ); inspector.assertExecutedCount( 1 ); inspector.assertNumberOfOccurrenceInQuery( 0, "join", where ? 2 : 1 ); } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterParameterTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterParameterTests.java index 150f6cbafe3d..dd3d80d02aec 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterParameterTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterParameterTests.java @@ -21,6 +21,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; @@ -146,6 +147,7 @@ public void testNumeric(BiConsumer s.persist(new Datetimes())); Object[] result = scope.fromTransaction(s -> (Object[]) s.createNativeQuery("select ctime, cdate, cdatetime from tdatetimes", Object[].class).getSingleResult()); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryTest.java index e601476775b5..04ee8ebdb369 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryTest.java @@ -28,6 +28,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.community.dialect.DerbyDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.PostgresPlusDialect; @@ -381,6 +382,7 @@ public Class getParameterType() { @Test @SkipForDialect(value = PostgreSQLDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") + @SkipForDialect(value = GaussDBDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = PostgresPlusDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = CockroachDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") public void testNativeQueryNullPositionalParameter() throws Exception { @@ -416,6 +418,7 @@ public void testNativeQueryNullPositionalParameter() throws Exception { @Test @JiraKey(value = "HHH-10161") @SkipForDialect(value = PostgreSQLDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") + @SkipForDialect(value = GaussDBDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = PostgresPlusDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = CockroachDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") public void testNativeQueryNullPositionalParameterParameter() throws Exception { @@ -467,6 +470,7 @@ public Class getParameterType() { @Test @SkipForDialect(value = PostgreSQLDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") + @SkipForDialect(value = GaussDBDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = PostgresPlusDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = CockroachDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") public void testNativeQueryNullNamedParameter() throws Exception { @@ -502,6 +506,7 @@ public void testNativeQueryNullNamedParameter() throws Exception { @Test @JiraKey(value = "HHH-10161") @SkipForDialect(value = PostgreSQLDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") + @SkipForDialect(value = GaussDBDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = PostgresPlusDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") @SkipForDialect(value = CockroachDialect.class, jiraKey = "HHH-10312", comment = "Cannot determine the parameter types and bind type is unknown because the value is null") public void testNativeQueryNullNamedParameterParameter() throws Exception { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaDatabaseFileGenerationFailureTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaDatabaseFileGenerationFailureTest.java index 056673d79be7..9f5452fd9e3f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaDatabaseFileGenerationFailureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaDatabaseFileGenerationFailureTest.java @@ -19,6 +19,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Environment; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.jpa.boot.spi.Bootstrap; import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder; @@ -74,6 +75,7 @@ public void destroy() { @JiraKey(value = "HHH-12192") @SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "on postgres we send 'set client_min_messages = WARNING'") + @SkipForDialect( dialectClass = GaussDBDialect.class, reason = "on gauss we send 'set client_min_messages = WARNING'") public void testErrorMessageContainsTheFailingDDLCommand() { try { entityManagerFactoryBuilder.generateSchema(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaScriptFileGenerationFailureTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaScriptFileGenerationFailureTest.java index 6e8f8ad4461d..7803e6f740c5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaScriptFileGenerationFailureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/schemagen/SchemaScriptFileGenerationFailureTest.java @@ -17,6 +17,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Environment; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.jpa.boot.spi.Bootstrap; import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder; @@ -65,6 +66,7 @@ public void destroy() { @JiraKey(value = "HHH-12192") @SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "on postgres we send 'set client_min_messages = WARNING'") + @SkipForDialect( dialectClass = GaussDBDialect.class, reason = "on gauss we send 'set client_min_messages = WARNING'") public void testErrorMessageContainsTheFailingDDLCommand() { try { entityManagerFactoryBuilder.generateSchema(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java index 988b2e50b3aa..9f9850f1c77f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java @@ -9,6 +9,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.type.SqlTypes; @@ -39,6 +40,7 @@ public class ArrayOfArraysTest { @ServiceRegistry( settings = @Setting( name = AvailableSettings.HBM2DDL_AUTO, value = "create-drop" ) ) @Test @SkipForDialect( dialectClass = CockroachDialect.class, reason = "Unable to find server array type for provided name bytes" ) + @SkipForDialect( dialectClass = GaussDBDialect.class, reason = "type:resolved.Method com.huawei.gaussdb.jdbc.jdbc.PgArray.getArrayImpl(long,int,Map) is not yet implemented.") public void testDoubleByteArrayWorks(SessionFactoryScope scope) { final Long id = scope.fromTransaction( session -> { final EntityWithDoubleByteArray entity = new EntityWithDoubleByteArray(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobByteArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobByteArrayTest.java index d057f7b7b7a8..473ce9e5bb7d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobByteArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobByteArrayTest.java @@ -13,6 +13,8 @@ import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.dialect.GaussDBDialect; import static org.junit.Assert.assertArrayEquals; /** @@ -28,6 +30,7 @@ protected Class[] getAnnotatedClasses() { } @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB does not support byte array operations through lob type") public void test() { Integer productId = doInJPA(this::entityManagerFactory, entityManager -> { final Product product = new Product(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java index 7f6a7f1fdbbc..2c1dbbcf2356 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java @@ -13,6 +13,7 @@ import org.hibernate.annotations.JavaType; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; @@ -27,6 +28,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -47,6 +49,7 @@ public class ByteArrayMappingTests { @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB does not support byte array operations through lob type") public void verifyMappings(SessionFactoryScope scope) { final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() .getRuntimeMetamodels() diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java index cb37bc6228dc..52ea6dcbe25a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java @@ -9,6 +9,7 @@ import org.hibernate.annotations.Nationalized; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; @@ -21,6 +22,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -47,6 +49,7 @@ public class WrapperArrayHandlingLegacyTests { @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB does not support byte array operations through lob type") public void verifyByteArrayMappings(SessionFactoryScope scope) { final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() .getRuntimeMetamodels() diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/XmlMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/XmlMappingTests.java index 077af442343e..263f79d45209 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/XmlMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/XmlMappingTests.java @@ -12,6 +12,7 @@ import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.HANADialect; import org.hibernate.community.dialect.DerbyDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; @@ -99,6 +100,7 @@ public void tearDown(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB don't support this xml feature") public void verifyMappings(SessionFactoryScope scope) { final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() .getRuntimeMetamodels() @@ -123,6 +125,7 @@ public void verifyMappings(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB don't support this xml feature") public void verifyReadWorks(SessionFactoryScope scope) { scope.inTransaction( (session) -> { @@ -140,6 +143,7 @@ public void verifyReadWorks(SessionFactoryScope scope) { @SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase doesn't support comparing LOBs with the = operator") @SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "Oracle doesn't support comparing JSON with the = operator") @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase doesn't support comparing CLOBs with the = operator") + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB doesn't support comparing CLOBs with the = operator") public void verifyComparisonWorks(SessionFactoryScope scope) { scope.inTransaction( (session) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index 9dadefc0c7b1..ab30a3196799 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -14,6 +14,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.community.dialect.DerbyDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.HSQLDialect; @@ -1016,7 +1017,7 @@ public void testCastFunction(SessionFactoryScope scope) { assertThat( session.createQuery("select cast('1911-10-09' as Date)", Date.class).getSingleResult(), instanceOf(Date.class) ); assertThat( session.createQuery("select cast('1911-10-09 12:13:14.123' as Timestamp)", Timestamp.class).getSingleResult(), instanceOf(Timestamp.class) ); - assertThat( session.createQuery("select cast(date 1911-10-09 as String)", String.class).getSingleResult(), is("1911-10-09") ); + assertThat( session.createQuery("select cast(date 1911-10-09 as String)", String.class).getSingleResult(), anyOf( is("1911-10-09"), is("1911-10-09 00:00:00") ) ); assertThat( session.createQuery("select cast(time 12:13:14 as String)", String.class).getSingleResult(), anyOf( is("12:13:14"), is("12:13:14.0000"), is("12.13.14") ) ); assertThat( session.createQuery("select cast(datetime 1911-10-09 12:13:14 as String)", String.class).getSingleResult(), anyOf( startsWith("1911-10-09 12:13:14"), startsWith("1911-10-09-12.13.14") ) ); @@ -1169,6 +1170,7 @@ public void testCastFunctionWithLength(SessionFactoryScope scope) { @SkipForDialect(dialectClass = DB2Dialect.class, majorVersion = 10, minorVersion = 5, reason = "On this version the length of the cast to the parameter appears to be > 2") @SkipForDialect( dialectClass = AltibaseDialect.class, reason = "Altibase cast to raw does not do truncatation") @SkipForDialect(dialectClass = HSQLDialect.class, reason = "HSQL interprets string as hex literal and produces error") + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "GaussDB bytea doesn't have a length") public void testCastBinaryWithLength(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -1216,7 +1218,8 @@ public void testStrFunction(SessionFactoryScope scope) { session.createQuery("select str(e.id), str(e.theInt), str(e.theDouble) from EntityOfBasics e", Object[].class) .list(); assertThat( session.createQuery("select str(69)", String.class).getSingleResult(), is("69") ); - assertThat( session.createQuery("select str(date 1911-10-09)", String.class).getSingleResult(), is("1911-10-09") ); + // str() do not specify the standard of date & time format + assertThat( session.createQuery("select str(date 1911-10-09)", String.class).getSingleResult(), anyOf( is("1911-10-09"), is( "1911-10-09 00:00:00" ) ) ); assertThat( session.createQuery("select str(time 12:13:14)", String.class).getSingleResult(), anyOf( is( "12:13:14"), is( "12:13:14.0000"), is( "12.13.14") ) ); } ); @@ -1784,6 +1787,8 @@ public void testDurationArithmeticOverflowing(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = GaussDBDialect.class, + reason = "GaussDB driver does not support implicit type casting to Long or Duration.") public void testDurationArithmeticWithLiterals(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -1886,6 +1891,8 @@ public void testDurationSubtractionWithTimeLiterals(SessionFactoryScope scope) { @SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "numeric overflow") + @SkipForDialect(dialectClass = GaussDBDialect.class, + reason = "numeric overflow") public void testDurationSubtractionWithDatetimeLiterals(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -1966,6 +1973,8 @@ public void testDurationArithmeticWithParameters(SessionFactoryScope scope) { } @Test + @SkipForDialect( dialectClass = GaussDBDialect.class, + reason = "GaussDB driver does not support implicit type casting to Long or Duration.") public void testIntervalDiffExpressions(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java index bba7fd5af42a..d4f453c37052 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java @@ -6,6 +6,7 @@ import java.time.LocalDate; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.query.criteria.HibernateCriteriaBuilder; @@ -130,6 +131,10 @@ else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof Sy // Sybase seems to report all matched rows as affected and ignores additional predicates assertEquals( 1, updated ); } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof GaussDBDialect ) { + // GaussDB seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } else { assertEquals( 0, updated ); } @@ -166,6 +171,10 @@ else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof Sy // Sybase seems to report all matched rows as affected and ignores additional predicates assertEquals( 1, updated ); } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof GaussDBDialect ) { + // GaussDB seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } else { assertEquals( 0, updated ); } @@ -250,6 +259,10 @@ else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof Sy // Sybase seems to report all matched rows as affected and ignores additional predicates assertEquals( 1, updated ); } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof GaussDBDialect ) { + // GaussDB seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } else { assertEquals( 0, updated ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/resource/ResultSetReleaseWithStatementDelegationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/resource/ResultSetReleaseWithStatementDelegationTest.java index ed7fb0f19173..cb2b7e41fe8d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/resource/ResultSetReleaseWithStatementDelegationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/resource/ResultSetReleaseWithStatementDelegationTest.java @@ -11,11 +11,11 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl; -import org.hibernate.testing.DialectChecks; -import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.testing.logger.Triggerable; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -39,7 +39,7 @@ provider = ResultSetReleaseWithStatementDelegationTest.ConnectionProviderDelegateProvider.class) ) @SessionFactory -@RequiresDialectFeature(DialectChecks.SupportsIdentityColumns.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsIdentityColumns.class) @JiraKey( "HHH-19280" ) class ResultSetReleaseWithStatementDelegationTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java index e222731e81a3..06ef5286d9a1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java @@ -8,6 +8,7 @@ import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.MySQLDialect; @@ -117,6 +118,7 @@ public void testQueryById(SessionFactoryScope scope) { @Test @SkipForDialect(dialectClass = PostgresPlusDialect.class, reason = "Seems that comparing date[] through JDBC is buggy. ERROR: operator does not exist: timestamp without time zone[] = date[]") + @SkipForDialect(dialectClass = GaussDBDialect.class, reason = "Seems that comparing date[] through JDBC is buggy. ERROR: operator does not exist: timestamp without time zone[] = date[]") public void testQuery(SessionFactoryScope scope) { scope.inSession( em -> { TypedQuery tq = em.createNamedQuery( "TableWithDateArrays.JPQL.getByData", TableWithDateArrays.class ); @@ -145,6 +147,7 @@ public void testNativeQueryById(SessionFactoryScope scope) { @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA requires a special function to compare LOBs") @SkipForDialect(dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "MySQL supports distinct from through a special operator") @SkipForDialect(dialectClass = PostgresPlusDialect.class, reason = "Seems that comparing date[] through JDBC is buggy. ERROR: operator does not exist: timestamp without time zone[] = date[]") + @SkipForDialect( dialectClass = GaussDBDialect.class, reason = "type:resolved.Seems that comparing date[] through JDBC is buggy. ERROR: operator does not exist: timestamp without time zone[] = date[]") public void testNativeQuery(SessionFactoryScope scope) { scope.inSession( em -> { final Dialect dialect = em.getDialect(); @@ -163,6 +166,7 @@ public void testNativeQuery(SessionFactoryScope scope) { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class) @SkipForDialect(dialectClass = PostgresPlusDialect.class, reason = "The 'date' type is a synonym for timestamp on Oracle and PostgresPlus, so untyped reading produces Timestamps") + @SkipForDialect( dialectClass = GaussDBDialect.class, reason = "type:resolved.The 'date' type is a synonym for timestamp on Oracle and PostgresPlus, so untyped reading produces Timestamps") public void testNativeQueryUntyped(SessionFactoryScope scope) { scope.inSession( em -> { Query q = em.createNamedQuery( "TableWithDateArrays.Native.getByIdUntyped" ); diff --git a/hibernate-hikaricp/src/test/java/org/hibernate/test/hikaricp/HikariTransactionIsolationConfigTest.java b/hibernate-hikaricp/src/test/java/org/hibernate/test/hikaricp/HikariTransactionIsolationConfigTest.java index 49669c70d2a5..a3d152d933e2 100644 --- a/hibernate-hikaricp/src/test/java/org/hibernate/test/hikaricp/HikariTransactionIsolationConfigTest.java +++ b/hibernate-hikaricp/src/test/java/org/hibernate/test/hikaricp/HikariTransactionIsolationConfigTest.java @@ -5,6 +5,7 @@ package org.hibernate.test.hikaricp; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.community.dialect.TiDBDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; @@ -19,6 +20,7 @@ @SkipForDialect(value = SybaseDialect.class, comment = "The jTDS driver doesn't implement Connection#getNetworkTimeout() so this fails") @SkipForDialect(value = TiDBDialect.class, comment = "Doesn't support SERIALIZABLE isolation") @SkipForDialect(value = AltibaseDialect.class, comment = "Altibase cannot change isolation level in autocommit mode") +@SkipForDialect(value = GaussDBDialect.class, comment = "GaussDB query serialization level of SERIALIZABLE has some problem") public class HikariTransactionIsolationConfigTest extends BaseTransactionIsolationConfigTest { @Override protected ConnectionProvider getConnectionProviderUnderTest() { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index ea7a8b5a0f7a..841c0f4f5963 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -47,6 +47,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.GaussDBDialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.HSQLDialect; @@ -534,6 +535,7 @@ public boolean apply(Dialect dialect) { || dialect instanceof SybaseDialect || dialect instanceof DerbyDialect || dialect instanceof FirebirdDialect + || dialect instanceof GaussDBDialect || dialect instanceof DB2Dialect && ( (DB2Dialect) dialect ).getDB2Version().isBefore( 11 ) ); } } @@ -550,6 +552,7 @@ public boolean apply(Dialect dialect) { || dialect instanceof H2Dialect || dialect instanceof SQLServerDialect || dialect instanceof PostgreSQLDialect + || dialect instanceof GaussDBDialect || dialect instanceof DB2Dialect || dialect instanceof OracleDialect || dialect instanceof SybaseDialect diff --git a/local-build-plugins/src/main/groovy/local.databases.gradle b/local-build-plugins/src/main/groovy/local.databases.gradle index 150ec7f19236..f78dc26ede60 100644 --- a/local-build-plugins/src/main/groovy/local.databases.gradle +++ b/local-build-plugins/src/main/groovy/local.databases.gradle @@ -75,6 +75,17 @@ ext { // 'jdbc.datasource' : 'org.postgresql.ds.PGSimpleDataSource', 'connection.init_sql' : '' ], + gaussdb: [ + 'db.dialect' : 'org.hibernate.dialect.GaussDBDialect', + 'jdbc.driver' : 'com.huawei.gaussdb.jdbc.Driver', + 'jdbc.user' : 'hibernate_orm_test', + 'jdbc.pass' : 'Hibernate_orm_test@1234', + // Disable prepared statement caching to avoid issues with changing schemas + // Make batch verification work, see https://bbs.huaweicloud.com/forum/thread-02104174303512776081-1-1.html + 'jdbc.url' : 'jdbc:gaussdb://' + dbHost + '/hibernate_orm_test?currentSchema=test&preparedStatementCacheQueries=0&batchMode=off', + 'jdbc.datasource' : 'com.huawei.gaussdb.jdbc.Driver', + 'connection.init_sql': '' + ], edb_ci : [ 'db.dialect' : 'org.hibernate.dialect.PostgresPlusDialect', 'jdbc.driver': 'org.postgresql.Driver', @@ -375,4 +386,4 @@ ext { 'jdbc.datasource' : 'Altibase.jdbc.driver.AltibaseDriver' ], ] -} \ No newline at end of file +} diff --git a/local-build-plugins/src/main/groovy/local.java-module.gradle b/local-build-plugins/src/main/groovy/local.java-module.gradle index 40499736c718..c61cd5f561ec 100644 --- a/local-build-plugins/src/main/groovy/local.java-module.gradle +++ b/local-build-plugins/src/main/groovy/local.java-module.gradle @@ -73,6 +73,7 @@ dependencies { testRuntimeOnly jdbcLibs.mssql testRuntimeOnly jdbcLibs.informix testRuntimeOnly jdbcLibs.cockroachdb + testRuntimeOnly jdbcLibs.gaussdb testRuntimeOnly jdbcLibs.sybase testRuntimeOnly rootProject.fileTree(dir: 'drivers', include: '*.jar') diff --git a/settings.gradle b/settings.gradle index b8c10b4d326d..e634f0e87acd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -227,6 +227,7 @@ dependencyResolutionManagement { def mysqlVersion = version "mysql", "9.2.0" def oracleVersion = version "oracle", "23.7.0.25.01" def pgsqlVersion = version "pgsql", "42.7.4" + def gaussdbVersion = version "gaussdb", "506.0.0.b058" def sybaseVersion = version "sybase", "1.3.1" def tidbVersion = version "tidb", mysqlVersion def altibaseVersion = version "altibase", "7.3.0.0.3" @@ -238,6 +239,7 @@ dependencyResolutionManagement { library( "derbyTools", "org.apache.derby", "derbytools" ).versionRef( derbyVersion ) library( "postgresql", "org.postgresql", "postgresql" ).versionRef( pgsqlVersion ) library( "cockroachdb", "org.postgresql", "postgresql" ).versionRef( pgsqlVersion ) + library( "gaussdb", "com.huaweicloud.gaussdb", "gaussdbjdbc" ).versionRef( gaussdbVersion ) library( "mysql", "com.mysql", "mysql-connector-j" ).versionRef( mysqlVersion ) library( "tidb", "com.mysql", "mysql-connector-j" ).versionRef( tidbVersion ) library( "mariadb", "org.mariadb.jdbc", "mariadb-java-client" ).versionRef( mariadbVersion )