From 27490657c145eac0aa9da9d4f4880e0ae5a70062 Mon Sep 17 00:00:00 2001 From: Jonathan Bregler Date: Fri, 16 Feb 2018 12:42:41 +0100 Subject: [PATCH] HHH-12302: Schema creation uses non-unicode string types on SAP HANA - add Parameter hibernate.dialect.hana.use_unicode_string_types that allows switching the database string types to unicode (nvarchar, nchar, nclob) --- .../dialect/AbstractHANADialect.java | 58 +++++- ...hemaMigrationTargetScriptCreationTest.java | 187 ++++++++++++++++++ 2 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/schemaupdate/HANASchemaMigrationTargetScriptCreationTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java index b4682607b9..05d9c47981 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -91,10 +91,14 @@ import org.hibernate.type.descriptor.sql.BasicExtractor; import org.hibernate.type.descriptor.sql.BitTypeDescriptor; import org.hibernate.type.descriptor.sql.BlobTypeDescriptor; import org.hibernate.type.descriptor.sql.BooleanTypeDescriptor; +import org.hibernate.type.descriptor.sql.CharTypeDescriptor; import org.hibernate.type.descriptor.sql.ClobTypeDescriptor; +import org.hibernate.type.descriptor.sql.NCharTypeDescriptor; import org.hibernate.type.descriptor.sql.NClobTypeDescriptor; +import org.hibernate.type.descriptor.sql.NVarcharTypeDescriptor; import org.hibernate.type.descriptor.sql.SmallIntTypeDescriptor; import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; +import org.hibernate.type.descriptor.sql.VarcharTypeDescriptor; /** * An abstract base class for HANA dialects.
@@ -418,9 +422,11 @@ public abstract class AbstractHANADialect extends Dialect { private static final long serialVersionUID = -379042275442752102L; final int maxLobPrefetchSize; + final boolean useUnicodeStringTypes; - public HANAClobTypeDescriptor(int maxLobPrefetchSize) { + public HANAClobTypeDescriptor(int maxLobPrefetchSize, boolean useUnicodeStringTypes) { this.maxLobPrefetchSize = maxLobPrefetchSize; + this.useUnicodeStringTypes = useUnicodeStringTypes; } @Override @@ -470,7 +476,14 @@ public abstract class AbstractHANADialect extends Dialect { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { - Clob rsClob = rs.getClob( name ); + Clob rsClob; + if ( HANAClobTypeDescriptor.this.useUnicodeStringTypes ) { + rsClob = rs.getNClob( name ); + } + else { + rsClob = rs.getClob( name ); + } + if ( rsClob == null || rsClob.length() < HANAClobTypeDescriptor.this.maxLobPrefetchSize ) { return javaTypeDescriptor.wrap( rsClob, options ); } @@ -493,6 +506,10 @@ public abstract class AbstractHANADialect extends Dialect { public int getMaxLobPrefetchSize() { return this.maxLobPrefetchSize; } + + public boolean isUseUnicodeStringTypes() { + return this.useUnicodeStringTypes; + } } private static class HANANClobTypeDescriptor extends NClobTypeDescriptor { @@ -666,17 +683,21 @@ public abstract class AbstractHANADialect extends Dialect { private static final String MAX_LOB_PREFETCH_SIZE_PARAMETER_NAME = new String( "hibernate.dialect.hana.max_lob_prefetch_size" ); private static final String USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME = new String( "hibernate.dialect.hana.use_legacy_boolean_type" ); + private static final String USE_UNICODE_STRING_TYPES_PARAMETER_NAME = new String( "hibernate.dialect.hana.use_unicode_string_types" ); private static final int MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE = 1024; private static final Boolean USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE = Boolean.FALSE; + private static final Boolean USE_UNICODE_STRING_TYPES_DEFAULT_VALUE = Boolean.FALSE; private HANANClobTypeDescriptor nClobTypeDescriptor = new HANANClobTypeDescriptor( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); private HANABlobTypeDescriptor blobTypeDescriptor = new HANABlobTypeDescriptor( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); - private HANAClobTypeDescriptor clobTypeDescriptor = new HANAClobTypeDescriptor( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); + private HANAClobTypeDescriptor clobTypeDescriptor = new HANAClobTypeDescriptor( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE, + USE_UNICODE_STRING_TYPES_DEFAULT_VALUE ); private boolean useLegacyBooleanType = USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE.booleanValue(); + private boolean useUnicodeStringTypes = USE_UNICODE_STRING_TYPES_DEFAULT_VALUE.booleanValue(); /* * Tables named "TYPE" need to be quoted @@ -736,15 +757,19 @@ public abstract class AbstractHANADialect extends Dialect { registerColumnType( Types.LONGVARBINARY, "blob" ); registerColumnType( Types.CHAR, "varchar(1)" ); + registerColumnType( Types.NCHAR, "nvarchar(1)" ); registerColumnType( Types.VARCHAR, 5000, "varchar($l)" ); registerColumnType( Types.LONGVARCHAR, 5000, "varchar($l)" ); registerColumnType( Types.NVARCHAR, 5000, "nvarchar($l)" ); + registerColumnType( Types.LONGNVARCHAR, 5000, "nvarchar($l)" ); // for longer values map to clob/nclob registerColumnType( Types.LONGVARCHAR, "clob" ); registerColumnType( Types.VARCHAR, "clob" ); + registerColumnType( Types.LONGNVARCHAR, "nclob" ); registerColumnType( Types.NVARCHAR, "nclob" ); registerColumnType( Types.CLOB, "clob" ); + registerColumnType( Types.NCLOB, "nclob" ); registerColumnType( Types.BOOLEAN, "boolean" ); @@ -1088,6 +1113,10 @@ public abstract class AbstractHANADialect extends Dialect { return SmallIntTypeDescriptor.INSTANCE; case Types.BOOLEAN: return this.useLegacyBooleanType ? BitTypeDescriptor.INSTANCE : BooleanTypeDescriptor.INSTANCE; + case Types.VARCHAR: + return this.useUnicodeStringTypes ? NVarcharTypeDescriptor.INSTANCE : VarcharTypeDescriptor.INSTANCE; + case Types.CHAR: + return this.useUnicodeStringTypes ? NCharTypeDescriptor.INSTANCE : CharTypeDescriptor.INSTANCE; default: return super.getSqlTypeDescriptorOverride( sqlCode ); } @@ -1504,12 +1533,31 @@ public abstract class AbstractHANADialect extends Dialect { this.blobTypeDescriptor = new HANABlobTypeDescriptor( maxLobPrefetchSize ); } - if ( this.clobTypeDescriptor.getMaxLobPrefetchSize() != maxLobPrefetchSize ) { - this.clobTypeDescriptor = new HANAClobTypeDescriptor( maxLobPrefetchSize ); + boolean useUnicodeStringTypes = configurationService.getSetting( USE_UNICODE_STRING_TYPES_PARAMETER_NAME, StandardConverters.BOOLEAN, + USE_UNICODE_STRING_TYPES_DEFAULT_VALUE ).booleanValue(); + + if ( useUnicodeStringTypes ) { + registerColumnType( Types.CHAR, "nvarchar(1)" ); + registerColumnType( Types.VARCHAR, 5000, "nvarchar($l)" ); + registerColumnType( Types.LONGVARCHAR, 5000, "nvarchar($l)" ); + + // for longer values map to clob/nclob + registerColumnType( Types.LONGVARCHAR, "nclob" ); + registerColumnType( Types.VARCHAR, "nclob" ); + registerColumnType( Types.CLOB, "nclob" ); + } + + if ( this.clobTypeDescriptor.getMaxLobPrefetchSize() != maxLobPrefetchSize + || this.clobTypeDescriptor.isUseUnicodeStringTypes() != useUnicodeStringTypes ) { + this.clobTypeDescriptor = new HANAClobTypeDescriptor( maxLobPrefetchSize, useUnicodeStringTypes ); } this.useLegacyBooleanType = configurationService.getSetting( USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME, StandardConverters.BOOLEAN, USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE ).booleanValue(); + + if ( this.useLegacyBooleanType ) { + registerColumnType( Types.BOOLEAN, "tinyint" ); + } } public SqlTypeDescriptor getBlobTypeDescriptor() { diff --git a/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/HANASchemaMigrationTargetScriptCreationTest.java b/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/HANASchemaMigrationTargetScriptCreationTest.java new file mode 100644 index 0000000000..90d217e3a3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/HANASchemaMigrationTargetScriptCreationTest.java @@ -0,0 +1,187 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.schemaupdate; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.EnumSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.AbstractHANADialect; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.tool.hbm2ddl.SchemaExport; +import org.hibernate.tool.schema.TargetType; + +import org.junit.After; +import org.junit.Test; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.ServiceRegistryBuilder; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Jonathan Bregler + */ +@RequiresDialect(value = AbstractHANADialect.class) +public class HANASchemaMigrationTargetScriptCreationTest extends BaseCoreFunctionalTestCase { + + private File output; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ TestEntity.class }; + } + + @Override + protected boolean createSchema() { + return false; + } + + @Override + protected void configure(Configuration configuration) { + try { + output = File.createTempFile( "update_script", ".sql" ); + } + catch (IOException e) { + fail( e.getMessage() ); + } + output.deleteOnExit(); + configuration.setProperty( Environment.HBM2DDL_SCRIPTS_ACTION, "create" ); + configuration.setProperty( Environment.HBM2DDL_SCRIPTS_CREATE_TARGET, output.getAbsolutePath() ); + } + + @After + public void tearDown() { + ServiceRegistry serviceRegistry = ServiceRegistryBuilder.buildServiceRegistry( Environment.getProperties() ); + try { + MetadataImplementor metadata = (MetadataImplementor) new MetadataSources( serviceRegistry ) + .addAnnotatedClass( TestEntity.class ) + .buildMetadata(); + metadata.validate(); + + new SchemaExport().drop( EnumSet.of( TargetType.DATABASE, TargetType.STDOUT ), metadata ); + } + finally { + ServiceRegistryBuilder.destroy( serviceRegistry ); + } + } + + @Test + @TestForIssue(jiraKey = "HHH-12302") + public void testTargetScriptIsCreatedStringTypeDefault() throws Exception { + this.rebuildSessionFactory(); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field varchar.+, b boolean.+, c varchar.+, lob clob.+" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12302") + public void testTargetScriptIsCreatedStringTypeNVarchar() throws Exception { + this.rebuildSessionFactory( config -> { + config.setProperty( "hibernate.dialect.hana.use_unicode_string_types", "true" ); + } ); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field nvarchar.+, b boolean.+, c nvarchar.+, lob nclob" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12302") + public void testTargetScriptIsCreatedStringTypeVarchar() throws Exception { + this.rebuildSessionFactory( config -> { + config.setProperty( "hibernate.dialect.hana.use_nvarchar_string_type", "false" ); + } ); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field varchar.+, b boolean.+, c varchar.+, lob clob" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12132") + public void testTargetScriptIsCreatedBooleanTypeDefault() throws Exception { + this.rebuildSessionFactory(); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field varchar.+, b boolean.+, c varchar.+, lob clob" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12132") + public void testTargetScriptIsCreatedBooleanTypeLegacy() throws Exception { + this.rebuildSessionFactory( config -> { + config.setProperty( "hibernate.dialect.hana.use_legacy_boolean_type", "true" ); + } ); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field varchar.+, b tinyint.+, c varchar.+, lob clob" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Test + @TestForIssue(jiraKey = "HHH-12132") + public void testTargetScriptIsCreatedBooleanType() throws Exception { + this.rebuildSessionFactory( config -> { + config.setProperty( "hibernate.dialect.hana.use_legacy_boolean_type", "false" ); + } ); + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + Pattern fileContentPattern = Pattern.compile( "create( (column|row))? table test_entity \\(field varchar.+, b boolean.+, c varchar.+, lob clob" ); + Matcher fileContentMatcher = fileContentPattern.matcher( fileContent.toLowerCase() ); + assertThat( + "Script file : " + fileContent.toLowerCase(), + fileContentMatcher.find(), + is( true ) ); + } + + @Entity + @Table(name = "test_entity") + public static class TestEntity { + + @Id + private String field; + + private char c; + + @Lob + private String lob; + + private boolean b; + } +}