From aa8d4361f584a455b05d3b52d31ca94c93c89f9b Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 13 Aug 2020 14:20:49 -0400 Subject: [PATCH] HHH-3326 fix temp table collation issue for SQLServer dialect --- .../java/org/hibernate/dialect/Dialect.java | 10 ++ .../hibernate/dialect/SQLServerDialect.java | 17 ++ .../AbstractMultiTableBulkIdStrategyImpl.java | 8 + ...QLServerDialectTempTableCollationTest.java | 161 ++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/dialect/functional/SQLServerDialectTempTableCollationTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 0811ce630f..10ac712992 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -3050,4 +3050,14 @@ public abstract class Dialect implements ConversionContext { public boolean supportsSelectAliasInGroupByClause() { return false; } + + /** + * Annotation to be appended to the end of each COLUMN clause for temporary tables. + * + * @param sqlTypeCode The SQL type code + * @return The annotation to be appended (e.g. "COLLATE DATABASE_DEFAULT" in SQLServer SQL) + */ + public String getCreateTemporaryTableColumnAnnotation(int sqlTypeCode) { + return ""; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index ff68b8d028..7ac92fe1ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -20,6 +20,8 @@ import org.hibernate.dialect.pagination.LegacyLimitHandler; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.TopLimitHandler; import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.StringType; +import org.hibernate.type.Type; import org.hibernate.type.descriptor.sql.SmallIntTypeDescriptor; import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; @@ -214,4 +216,19 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { public IdentityColumnSupport getIdentityColumnSupport() { return new SQLServerIdentityColumnSupport(); } + + @Override + public String getCreateTemporaryTableColumnAnnotation(int sqlTypeCode) { + switch (sqlTypeCode) { + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + return "collate database_default"; + default: + return ""; + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractMultiTableBulkIdStrategyImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractMultiTableBulkIdStrategyImpl.java index 41430fce38..3b7fa3c7b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractMultiTableBulkIdStrategyImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractMultiTableBulkIdStrategyImpl.java @@ -142,12 +142,20 @@ public abstract class AbstractMultiTableBulkIdStrategyImpl. + */ +package org.hibernate.test.dialect.functional; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.Table; + +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.internal.StandardServiceRegistryImpl; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.SQLServer2005Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; + +import org.hibernate.testing.AfterClassOnce; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Assert; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; + +/** + * @author Nathan Xu + */ +@RequiresDialect( SQLServer2005Dialect.class ) +@TestForIssue( jiraKey = "HHH-3326" ) +public class SQLServerDialectTempTableCollationTest extends BaseCoreFunctionalTestCase { + + private String originalDBCollation; + private final String changedDBCollation = "SQL_Latin1_General_CP437_BIN"; + private boolean collationChanged; + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( AvailableSettings.KEYWORD_AUTO_QUOTING_ENABLED, Boolean.TRUE.toString() ); + return configuration; + } + + @AfterClassOnce + protected void revertBackOriginalDBCollation() { + if ( originalDBCollation != null && collationChanged && !changedDBCollation.equals( originalDBCollation ) ) { + BootstrapServiceRegistry bootRegistry = buildBootstrapServiceRegistry(); + StandardServiceRegistryImpl serviceRegistry = buildServiceRegistry( + bootRegistry, + constructConfiguration() + ); + try (Connection connection = serviceRegistry.getService( JdbcServices.class ) + .getBootstrapJdbcConnectionAccess() + .obtainConnection(); + Statement statement = connection.createStatement()) { + connection.setAutoCommit( true ); + statement.executeUpdate( "ALTER DATABASE CURRENT COLLATE " + originalDBCollation ); + } + catch (SQLException e) { + throw new RuntimeException( "Failed to revert back database collation to " + originalDBCollation, e ); + } + finally { + serviceRegistry.destroy(); + } + } + } + + protected void buildSessionFactory() { + BootstrapServiceRegistry bootRegistry = buildBootstrapServiceRegistry(); + StandardServiceRegistryImpl serviceRegistry = buildServiceRegistry( bootRegistry, constructConfiguration() ); + + try { + try ( Connection connection = serviceRegistry.getService( JdbcServices.class ).getBootstrapJdbcConnectionAccess().obtainConnection(); + Statement statement = connection.createStatement() ) { + connection.setAutoCommit( true ); + try ( ResultSet rs = statement.executeQuery( "SELECT SERVERPROPERTY('collation')" ) ) { + rs.next(); + String instanceCollation = rs.getString( 1 ); + Assert.assertNotEquals( instanceCollation, changedDBCollation ); + } + } + catch (SQLException e) { + log.debug( e.getMessage() ); + } + try ( Connection connection = serviceRegistry.getService( JdbcServices.class ).getBootstrapJdbcConnectionAccess().obtainConnection(); + Statement statement = connection.createStatement() ) { + connection.setAutoCommit( true ); + try ( ResultSet rs = statement.executeQuery( "SELECT CONVERT (varchar(256), DATABASEPROPERTYEX(DB_NAME(),'collation'))" ) ) { + rs.next(); + originalDBCollation = rs.getString( 1 ); + } + } + catch (SQLException e) { + log.debug( e.getMessage() ); + } + try ( Connection connection = serviceRegistry.getService( JdbcServices.class ).getBootstrapJdbcConnectionAccess().obtainConnection(); + Statement statement = connection.createStatement() ) { + connection.setAutoCommit( true ); + statement.executeUpdate( "ALTER DATABASE CURRENT COLLATE " + changedDBCollation ); + collationChanged = true; + } + } + catch ( SQLException e ) { + throw new RuntimeException( e ); + } + finally { + serviceRegistry.destroy(); + } + super.buildSessionFactory(); + } + + @Test + public void testTemporaryTableCreateWithoutCollationConflict() { + // without fixing "HHH-3326", the following exception will be thrown: + // Cannot resolve the collation conflict between "SQL_Latin1_General_CP1_CI_AS" and "SQL_Latin1_General_CP437_BIN" in the equal to operation. + doInHibernate( this::sessionFactory, session -> { + session.createQuery( "update Woman w set w.description = :description where w.age > :age" ) + .setParameter( "description", "your are old" ) + .setParameter( "age", 30 ) + .executeUpdate(); + } ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Human.class, + Woman.class + }; + } + + @Override + protected boolean rebuildSessionFactoryOnError() { + return false; + } + + @Entity(name = "Human") + @Table(name = "Human") + @Inheritance(strategy = InheritanceType.JOINED) + public static abstract class Human { + @Id + String id; + + int age; + } + + @Entity(name = "Woman") + @Table(name = "Woman") + public static class Woman extends Human { + String description; + } +}