From 736dfac693ae56a854e620ee0ee02be31f88a4fe Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Dec 2022 16:14:16 +0100 Subject: [PATCH] add ability to change column types to TableMigrator --- .../org/hibernate/dialect/DB2Dialect.java | 11 + .../java/org/hibernate/dialect/Dialect.java | 59 +++--- .../java/org/hibernate/dialect/H2Dialect.java | 12 ++ .../org/hibernate/dialect/MySQLDialect.java | 12 +- .../org/hibernate/dialect/OracleDialect.java | 16 +- .../hibernate/dialect/PostgreSQLDialect.java | 11 + .../hibernate/dialect/SQLServerDialect.java | 11 +- .../org/hibernate/dialect/SybaseDialect.java | 10 + .../jdbc/internal/DDLFormatterImpl.java | 22 +- .../internal/AbstractSchemaValidator.java | 19 +- .../schema/internal/ColumnDefinitions.java | 193 ++++++++++++++++++ .../internal/StandardTableExporter.java | 95 +-------- .../internal/StandardTableMigrator.java | 35 +++- .../java/org/hibernate/type/SqlTypes.java | 13 ++ .../orm/test/schemaupdate/1_Version.hbm.xml | 3 +- .../orm/test/schemaupdate/2_Version.hbm.xml | 3 +- .../orm/test/schemaupdate/3_Version.hbm.xml | 1 + .../orm/test/schemaupdate/4_Version.hbm.xml | 23 +++ .../orm/test/schemaupdate/MigrationTest.java | 52 +++++ .../orm/test/schemaupdate/Version.java | 9 + 20 files changed, 429 insertions(+), 181 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/4_Version.hbm.xml diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 2eb3b61188..1e4121cc5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -656,6 +656,17 @@ public class DB2Dialect extends Dialect { return true; } + @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 SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( EntityMappingType rootEntityDescriptor, 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 1509170cb6..6a21aff049 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -32,6 +32,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.TimeZone; @@ -192,40 +193,7 @@ import jakarta.persistence.TemporalType; import static java.lang.Math.ceil; import static java.lang.Math.log; import static org.hibernate.internal.util.StringHelper.parseCommaSeparatedString; -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.BLOB; -import static org.hibernate.type.SqlTypes.BOOLEAN; -import static org.hibernate.type.SqlTypes.CHAR; -import static org.hibernate.type.SqlTypes.CLOB; -import static org.hibernate.type.SqlTypes.DATE; -import static org.hibernate.type.SqlTypes.DECIMAL; -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.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.NUMERIC; -import static org.hibernate.type.SqlTypes.NVARCHAR; -import static org.hibernate.type.SqlTypes.REAL; -import static org.hibernate.type.SqlTypes.SMALLINT; -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_WITH_TIMEZONE; -import static org.hibernate.type.SqlTypes.TINYINT; -import static org.hibernate.type.SqlTypes.VARBINARY; -import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.SqlTypes.isFloatOrRealOrDouble; -import static org.hibernate.type.SqlTypes.isIntegral; -import static org.hibernate.type.SqlTypes.isNumericOrDecimal; -import static org.hibernate.type.SqlTypes.isVarbinaryType; -import static org.hibernate.type.SqlTypes.isVarcharType; +import static org.hibernate.type.SqlTypes.*; import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_END; import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_DATE; import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIME; @@ -1358,10 +1326,21 @@ public abstract class Dialect implements ConversionContext { public boolean equivalentTypes(int typeCode1, int typeCode2) { return typeCode1==typeCode2 || isNumericOrDecimal(typeCode1) && isNumericOrDecimal(typeCode2) - || isIntegral(typeCode1) && isIntegral(typeCode2) + || isSmallOrTinyInt(typeCode1) && isSmallOrTinyInt(typeCode2) //special case for HHH-15288 migration +// || isIntegral(typeCode1) && isIntegral(typeCode2) || isFloatOrRealOrDouble(typeCode1) && isFloatOrRealOrDouble(typeCode2) || isVarcharType(typeCode1) && isVarcharType(typeCode2) - || isVarbinaryType(typeCode1) && isVarbinaryType(typeCode2); + || isVarbinaryType(typeCode1) && isVarbinaryType(typeCode2) + || sameColumnType(typeCode1, typeCode2); + } + + private boolean sameColumnType(int typeCode1, int typeCode2) { + try { + return Objects.equals( columnType(typeCode1), columnType(typeCode2) ); + } + catch (IllegalArgumentException iae) { + return false; + } } /** @@ -2008,6 +1987,14 @@ public abstract class Dialect implements ConversionContext { return sb.toString(); } + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return null; + } + + public boolean supportsAlterColumnType() { + return false; + } + /** * Slight variation on {@link #getCreateTableString}. Here, we have the * command used to create a table when there is no primary key and diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 66295106a0..e17ded2d04 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -590,6 +590,18 @@ public class H2Dialect extends Dialect { : super.getCascadeConstraintsString(); } + @Override + public boolean supportsAlterColumnType() { + return true; + } + + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return "alter column " + columnName + " set data type " + columnType; + // if only altering the type, no need to specify the whole definition +// return "alter column " + columnName + " " + columnDefinition; + } + @Override public boolean supportsCommentOn() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 06ad1d0280..9e90e10611 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -11,7 +11,6 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; -import java.util.Arrays; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -855,6 +854,17 @@ public class MySQLDialect extends Dialect { return " drop index "; } + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + // no way to change just the column type, leaving other attributes intact + return "modify column " + columnName + " " + columnDefinition; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + @Override public LimitHandler getLimitHandler() { //also supports LIMIT n OFFSET m diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index d3d2cfb327..67c77f392c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -15,8 +15,6 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; import org.hibernate.LockOptions; import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.TypeContributions; @@ -51,7 +49,6 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.model.convert.spi.BasicValueConverter; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.StandardCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; @@ -80,12 +77,9 @@ import org.hibernate.type.JavaObjectType; import org.hibernate.type.NullType; import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; -import org.hibernate.type.descriptor.java.BooleanJavaType; -import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; -import org.hibernate.type.descriptor.jdbc.BooleanJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.NullJdbcType; @@ -882,6 +876,16 @@ public class OracleDialect extends Dialect { return false; } + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return "modify " + columnName + " " + columnType; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + @Override public SequenceSupport getSequenceSupport() { return OracleSequenceSupport.INSTANCE; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 6bf7eadb40..84f1c2188c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -670,6 +670,17 @@ public class PostgreSQLDialect extends Dialect { return true; } + @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; 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 7cfce9ea1b..d9bc6a8649 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -74,7 +74,6 @@ import org.hibernate.type.descriptor.jdbc.XmlJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; -import org.hibernate.type.spi.TypeConfiguration; import java.util.List; @@ -1001,6 +1000,16 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { } } + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return "alter column " + columnName + " " + columnType; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + @Override public NameQualifierSupport getNameQualifierSupport() { return NameQualifierSupport.BOTH; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java index 89230830ac..38c3b6111f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -365,4 +365,14 @@ public class SybaseDialect extends AbstractTransactSQLDialect { public CallableStatementSupport getCallableStatementSupport() { return jtdsDriver ? JTDSCallableStatementSupport.INSTANCE : super.getCallableStatementSupport(); } + + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return "modify " + columnName + " " + columnType; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/DDLFormatterImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/DDLFormatterImpl.java index 049197912a..dcfb0eac26 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/DDLFormatterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/DDLFormatterImpl.java @@ -7,6 +7,7 @@ package org.hibernate.engine.jdbc.internal; import java.util.Locale; +import java.util.Set; import java.util.StringTokenizer; import static org.hibernate.internal.util.StringHelper.isEmpty; @@ -26,6 +27,9 @@ public class DDLFormatterImpl implements Formatter { */ public static final DDLFormatterImpl INSTANCE = new DDLFormatterImpl(); + private static final Set BREAKS = Set.of( "drop", "alter", "modify", "add", "references", "foreign", "on" ); + private static final Set QUOTES = Set.of( "\"", "`", "]", "[", "'" ); + @Override public String format(String sql) { if ( isEmpty( sql ) ) { @@ -79,6 +83,7 @@ public class DDLFormatterImpl implements Formatter { final StringBuilder result = new StringBuilder( 60 ).append( INITIAL_LINE ); final StringTokenizer tokens = new StringTokenizer( sql, " (,)'[]\"", true ); + boolean first = true; boolean quoted = false; while ( tokens.hasMoreTokens() ) { final String token = tokens.nextToken(); @@ -86,11 +91,12 @@ public class DDLFormatterImpl implements Formatter { quoted = !quoted; } else if ( !quoted ) { - if ( isBreak( token ) ) { + if ( !first && isBreak( token ) ) { result.append( OTHER_LINES ); } } result.append( token ); + first = false; } return result.toString(); @@ -135,19 +141,11 @@ public class DDLFormatterImpl implements Formatter { } private static boolean isBreak(String token) { - return "drop".equals( token ) - || "add".equals( token ) - || "references".equals( token ) - || "foreign".equals( token ) - || "on".equals( token ); + return BREAKS.contains( token ); } - private static boolean isQuote(String tok) { - return "\"".equals( tok ) - || "`".equals( tok ) - || "]".equals( tok ) - || "[".equals( tok ) - || "'".equals( tok ); + private static boolean isQuote(String token) { + return QUOTES.contains( token ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java index a71b90b440..bcda2305be 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java @@ -7,7 +7,6 @@ package org.hibernate.tool.schema.internal; import java.util.Locale; -import java.util.StringTokenizer; import org.hibernate.boot.Metadata; import org.hibernate.boot.model.naming.Identifier; @@ -31,7 +30,6 @@ import org.hibernate.tool.schema.spi.SchemaFilter; import org.hibernate.tool.schema.spi.SchemaManagementException; import org.hibernate.tool.schema.spi.SchemaValidator; import org.hibernate.type.descriptor.JdbcTypeNameMapper; -import org.hibernate.type.descriptor.jdbc.JdbcType; import org.jboss.logging.Logger; @@ -160,22 +158,7 @@ public abstract class AbstractSchemaValidator implements SchemaValidator { Metadata metadata, ExecutionOptions options, Dialect dialect) { - boolean typesMatch = dialect.equivalentTypes( column.getSqlTypeCode( metadata ), columnInformation.getTypeCode() ) - || column.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata ).toLowerCase(Locale.ROOT) - .startsWith( columnInformation.getTypeName().toLowerCase(Locale.ROOT) ); - if ( !typesMatch ) { - // Try to resolve the JdbcType by type name and check for a match again based on that type code. - // This is used to handle SqlTypes type codes like TIMESTAMP_UTC etc. - final JdbcType jdbcType = dialect.resolveSqlTypeDescriptor( - columnInformation.getTypeName(), - columnInformation.getTypeCode(), - columnInformation.getColumnSize(), - columnInformation.getDecimalDigits(), - metadata.getDatabase().getTypeConfiguration().getJdbcTypeRegistry() - ); - typesMatch = dialect.equivalentTypes( column.getSqlTypeCode( metadata ), jdbcType.getDefaultSqlTypeCode() ); - } - if ( !typesMatch ) { + if ( !ColumnDefinitions.hasMatchingType( column, columnInformation, metadata, dialect ) ) { throw new SchemaManagementException( String.format( "Schema-validation: wrong column type encountered in column [%s] in " + diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java new file mode 100644 index 0000000000..77ede79a19 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnDefinitions.java @@ -0,0 +1,193 @@ +/* + * 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.tool.schema.internal; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Constraint; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.tool.schema.extract.spi.ColumnInformation; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +class ColumnDefinitions { + + static boolean hasMatchingType(Column column, ColumnInformation columnInformation, Metadata metadata, Dialect dialect) { + boolean typesMatch = dialect.equivalentTypes( column.getSqlTypeCode(metadata), columnInformation.getTypeCode() ) + || stripArgs( column.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata ) ) + .equalsIgnoreCase( columnInformation.getTypeName() ); + if ( typesMatch ) { + return true; + } + else { + // Try to resolve the JdbcType by type name and check for a match again based on that type code. + // This is used to handle SqlTypes type codes like TIMESTAMP_UTC etc. + final JdbcType jdbcType = dialect.resolveSqlTypeDescriptor( + columnInformation.getTypeName(), + columnInformation.getTypeCode(), + columnInformation.getColumnSize(), + columnInformation.getDecimalDigits(), + metadata.getDatabase().getTypeConfiguration().getJdbcTypeRegistry() + ); + return dialect.equivalentTypes( column.getSqlTypeCode(metadata), jdbcType.getDefaultSqlTypeCode() ); + } + } + + static boolean hasMatchingLength(Column column, ColumnInformation columnInformation, Metadata metadata, Dialect dialect) { + final int actualSize = columnInformation.getColumnSize(); + if ( actualSize == 0 ) { + return true; + } + else { + final Size size = column.getColumnSize( dialect, metadata ); + final Long requiredLength = size.getLength(); + final Integer requiredPrecision = size.getPrecision(); + return requiredLength != null && requiredLength == actualSize + || requiredPrecision != null && requiredPrecision == actualSize + || requiredPrecision == null && requiredLength == null; + } + } + + static String getFullColumnDeclaration( + Column column, + Table table, + Metadata metadata, + Dialect dialect, + SqlStringGenerationContext context) { + StringBuilder definition = new StringBuilder(); + appendColumn( definition, column, table, metadata, dialect, context ); + return definition.toString(); + } + + + static String getColumnDefinition(Column column, Table table, Metadata metadata, Dialect dialect) { + StringBuilder definition = new StringBuilder(); + appendColumnDefinition( definition, column, table, metadata, dialect ); + appendComment( definition, column, dialect ); + return definition.toString(); + } + + static void appendColumn( + StringBuilder statement, + Column column, + Table table, + Metadata metadata, + Dialect dialect, + SqlStringGenerationContext context) { + statement.append( column.getQuotedName( dialect ) ); + appendColumnDefinition( statement, column, table, metadata, dialect ); + appendConstraints( statement, column, table, dialect, context ); + appendComment( statement, column, dialect ); + } + + private static void appendConstraints( + StringBuilder definition, + Column column, + Table table, + Dialect dialect, + SqlStringGenerationContext context) { + if ( column.isUnique() && !table.isPrimaryKey( column ) ) { + final String keyName = Constraint.generateName( "UK_", table, column); + final UniqueKey uniqueKey = table.getOrCreateUniqueKey( keyName ); + uniqueKey.addColumn(column); + definition.append( dialect.getUniqueDelegate().getColumnDefinitionUniquenessFragment( column, context ) ); + } + + if ( dialect.supportsColumnCheck() && column.hasCheckConstraint() ) { + definition.append( column.checkConstraint() ); + } + } + + private static void appendComment(StringBuilder definition, Column column, Dialect dialect) { + final String columnComment = column.getComment(); + if ( columnComment != null ) { + definition.append( dialect.getColumnComment( columnComment ) ); + } + } + + private static void appendColumnDefinition( + StringBuilder definition, + Column column, + Table table, + Metadata metadata, + Dialect dialect) { + final String columnType = getColumnType( column, metadata, dialect ); + if ( isIdentityColumn(column, table, metadata, dialect) ) { + // to support dialects that have their own identity data type + if ( dialect.getIdentityColumnSupport().hasDataTypeInIdentityColumn() ) { + definition.append( ' ' ).append( columnType ); + } + final String identityColumnString = dialect.getIdentityColumnSupport() + .getIdentityColumnString( column.getSqlTypeCode(metadata) ); + definition.append( ' ' ).append( identityColumnString ); + } + else { + if ( column.hasSpecializedTypeDeclaration() ) { + definition.append( ' ' ).append( column.getSpecializedTypeDeclaration() ); + } + else if ( column.getGeneratedAs() == null || dialect.hasDataTypeBeforeGeneratedAs() ) { + definition.append( ' ' ).append( columnType ); + } + + final String defaultValue = column.getDefaultValue(); + if ( defaultValue != null ) { + definition.append( " default " ).append( defaultValue ); + } + + final String generatedAs = column.getGeneratedAs(); + if ( generatedAs != null) { + definition.append( dialect.generatedAs( generatedAs ) ); + } + + if ( column.isNullable() ) { + definition.append( dialect.getNullColumnString( columnType ) ); + } + else { + definition.append( " not null" ); + } + } + } + + static String getColumnType(Column column, Metadata metadata, Dialect dialect) { + return column.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata ); + } + + private static boolean isIdentityColumn(Column column, Table table, Metadata metadata, Dialect dialect) { + // Try to find out the name of the primary key in case the dialect needs it to create an identity + return isPrimaryKeyIdentity( table, metadata, dialect ) + && column.getQuotedName( dialect ).equals( getPrimaryKeyColumnName( table, dialect ) ); + } + + private static String getPrimaryKeyColumnName(Table table, Dialect dialect) { + return table.hasPrimaryKey() + ? table.getPrimaryKey().getColumns().get(0).getQuotedName( dialect ) + : null; + } + + private static boolean isPrimaryKeyIdentity(Table table, Metadata metadata, Dialect dialect) { + // TODO: this is the much better form moving forward as we move to metamodel + //return hasPrimaryKey + // && table.getPrimaryKey().getColumnSpan() == 1 + // && table.getPrimaryKey().getColumn( 0 ).isIdentity(); + MetadataImplementor metadataImplementor = (MetadataImplementor) metadata; + return table.hasPrimaryKey() + && table.getIdentifierValue() != null + && table.getIdentifierValue().isIdentityColumn( + metadataImplementor.getMetadataBuildingOptions().getIdentifierGeneratorFactory(), + dialect + ); + } + + private static String stripArgs(String string) { + int i = string.indexOf('('); + return i>0 ? string.substring(0,i) : string; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java index 3687fb27a9..0b6f412eea 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableExporter.java @@ -17,22 +17,20 @@ import org.hibernate.boot.model.relational.QualifiedName; import org.hibernate.boot.model.relational.QualifiedNameParser; import org.hibernate.boot.model.relational.QualifiedTableName; import org.hibernate.boot.model.relational.SqlStringGenerationContext; -import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Column; import org.hibernate.mapping.Component; -import org.hibernate.mapping.Constraint; import org.hibernate.mapping.Property; import org.hibernate.mapping.Table; -import org.hibernate.mapping.UniqueKey; import org.hibernate.mapping.Value; import org.hibernate.sql.Template; import org.hibernate.tool.schema.spi.Exporter; import static java.util.Collections.addAll; import static org.hibernate.internal.util.StringHelper.EMPTY_STRINGS; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.appendColumn; /** * An {@link Exporter} for {@linkplain Table tables}. @@ -100,97 +98,6 @@ public class StandardTableExporter implements Exporter { } } - static void appendColumn( - StringBuilder statement, - Column column, - Table table, - Metadata metadata, - Dialect dialect, - SqlStringGenerationContext context) { - - statement.append( column.getQuotedName( dialect ) ); - - final String columnType = column.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata ); - if ( isIdentityColumn( column, table, metadata, dialect ) ) { - // to support dialects that have their own identity data type - if ( dialect.getIdentityColumnSupport().hasDataTypeInIdentityColumn() ) { - statement.append( ' ' ).append( columnType ); - } - final String identityColumnString = dialect.getIdentityColumnSupport() - .getIdentityColumnString( column.getSqlTypeCode( metadata ) ); - statement.append( ' ' ).append( identityColumnString ); - } - else { - if ( column.hasSpecializedTypeDeclaration() ) { - statement.append( ' ' ).append( column.getSpecializedTypeDeclaration() ); - } - else if ( column.getGeneratedAs() == null || dialect.hasDataTypeBeforeGeneratedAs() ) { - statement.append( ' ' ).append( columnType ); - } - - final String defaultValue = column.getDefaultValue(); - if ( defaultValue != null ) { - statement.append( " default " ).append( defaultValue ); - } - - final String generatedAs = column.getGeneratedAs(); - if ( generatedAs != null) { - statement.append( dialect.generatedAs( generatedAs ) ); - } - - if ( column.isNullable() ) { - statement.append( dialect.getNullColumnString(columnType) ); - } - else { - statement.append( " not null" ); - } - } - - if ( column.isUnique() && !table.isPrimaryKey(column) ) { - final String keyName = Constraint.generateName( "UK_", table, column); - final UniqueKey uk = table.getOrCreateUniqueKey( keyName ); - uk.addColumn(column); - statement.append( - dialect.getUniqueDelegate().getColumnDefinitionUniquenessFragment( column, context ) - ); - } - - if ( dialect.supportsColumnCheck() && column.hasCheckConstraint() ) { - statement.append( column.checkConstraint() ); - } - - final String columnComment = column.getComment(); - if ( columnComment != null ) { - statement.append( dialect.getColumnComment( columnComment ) ); - } - } - - private static boolean isIdentityColumn(Column column, Table table, Metadata metadata, Dialect dialect) { - // Try to find out the name of the primary key in case the dialect needs it to create an identity - return isPrimaryKeyIdentity( table, metadata, dialect ) - && column.getQuotedName( dialect ).equals( getPrimaryKeyColumnName( table, dialect ) ); - } - - private static String getPrimaryKeyColumnName(Table table, Dialect dialect) { - return table.hasPrimaryKey() - ? table.getPrimaryKey().getColumns().get(0).getQuotedName( dialect ) - : null; - } - - private static boolean isPrimaryKeyIdentity(Table table, Metadata metadata, Dialect dialect) { - // TODO: this is the much better form moving forward as we move to metamodel - //return hasPrimaryKey - // && table.getPrimaryKey().getColumnSpan() == 1 - // && table.getPrimaryKey().getColumn( 0 ).isIdentity(); - MetadataImplementor metadataImplementor = (MetadataImplementor) metadata; - return table.hasPrimaryKey() - && table.getIdentifierValue() != null - && table.getIdentifierValue().isIdentityColumn( - metadataImplementor.getMetadataBuildingOptions().getIdentifierGeneratorFactory(), - dialect - ); - } - /** * @param table The table. * @param tableName The qualified table name. diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableMigrator.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableMigrator.java index 578c57678f..81b65e8bef 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableMigrator.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/StandardTableMigrator.java @@ -24,7 +24,11 @@ import java.util.ArrayList; import java.util.List; import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_STRING_ARRAY; -import static org.hibernate.tool.schema.internal.StandardTableExporter.appendColumn; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.getColumnDefinition; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.getColumnType; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.hasMatchingLength; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.hasMatchingType; +import static org.hibernate.tool.schema.internal.ColumnDefinitions.getFullColumnDeclaration; /** * A {@link TableMigrator} that only knows how to add new columns. @@ -57,7 +61,7 @@ public class StandardTableMigrator implements TableMigrator { Table table, Dialect dialect, Metadata metadata, - TableInformation tableInfo, + TableInformation tableInformation, SqlStringGenerationContext sqlStringGenerationContext) throws HibernateException { final String tableName = sqlStringGenerationContext.format( new QualifiedTableName( @@ -66,22 +70,31 @@ public class StandardTableMigrator implements TableMigrator { table.getNameIdentifier() ) ); - final StringBuilder root = new StringBuilder( dialect.getAlterTableString( tableName ) ) - .append( ' ' ) - .append( dialect.getAddColumnString() ); + final String alterTable = dialect.getAlterTableString( tableName ) + ' '; final List results = new ArrayList<>(); for ( Column column : table.getColumns() ) { - final ColumnInformation columnInfo = tableInfo.getColumn( + final ColumnInformation columnInformation = tableInformation.getColumn( Identifier.toIdentifier( column.getName(), column.isQuoted() ) ); - if ( columnInfo == null ) { + if ( columnInformation == null ) { // the column doesn't exist at all. - final StringBuilder alterTable = new StringBuilder( root.toString() ).append( ' ' ); - appendColumn( alterTable, column, table, metadata, dialect, sqlStringGenerationContext ); - alterTable.append( dialect.getAddColumnSuffixString() ); - results.add( alterTable.toString() ); + final String addColumn = dialect.getAddColumnString() + ' ' + + getFullColumnDeclaration( column, table, metadata, dialect, sqlStringGenerationContext ) + + dialect.getAddColumnSuffixString(); + results.add( alterTable + addColumn ); + } + else if ( dialect.supportsAlterColumnType() ) { + if ( !hasMatchingType( column, columnInformation, metadata, dialect ) + || !hasMatchingLength( column, columnInformation, metadata, dialect ) ) { + final String alterColumn = dialect.getAlterColumnTypeString( + column.getQuotedName( dialect ), + getColumnType( column, metadata, dialect ), + getColumnDefinition( column, table, metadata, dialect ) + ); + results.add( alterTable + alterColumn ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index 4929e44754..c1d08fcc54 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -6,6 +6,8 @@ */ package org.hibernate.type; +import org.hibernate.Internal; + import java.sql.Types; /** @@ -614,6 +616,17 @@ public class SqlTypes { } } + @Internal + public static boolean isSmallOrTinyInt(int typeCode) { + switch ( typeCode ) { + case SMALLINT: + case TINYINT: + return true; + default: + return false; + } + } + /** * Does the given typecode represent a SQL date, time, or timestamp type? * @param typeCode a JDBC type code from {@link Types} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/1_Version.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/1_Version.hbm.xml index e584ea471d..acfa3dcc74 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/1_Version.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/1_Version.hbm.xml @@ -15,7 +15,8 @@ - + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/2_Version.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/2_Version.hbm.xml index 74f8917a10..c8812ca7d6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/2_Version.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/2_Version.hbm.xml @@ -16,7 +16,8 @@ - + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/3_Version.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/3_Version.hbm.xml index 0021fe4aba..dd2a07462d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/3_Version.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/3_Version.hbm.xml @@ -21,6 +21,7 @@ + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/4_Version.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/4_Version.hbm.xml new file mode 100644 index 0000000000..741cd4763c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/4_Version.hbm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/MigrationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/MigrationTest.java index 746c570ddd..6a853be9ef 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/MigrationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/MigrationTest.java @@ -63,6 +63,10 @@ public class MigrationTest extends BaseUnitTestCase { v1metadata ); + v1schemaUpdate.getExceptions().forEach( + e -> System.out.println( e.getCause().getMessage() ) + ); + assertEquals( 0, v1schemaUpdate.getExceptions().size() ); MetadataImplementor v2metadata = (MetadataImplementor) new MetadataSources( serviceRegistry ) @@ -74,11 +78,59 @@ public class MigrationTest extends BaseUnitTestCase { EnumSet.of( TargetType.DATABASE, TargetType.STDOUT ), v2metadata ); + + v2schemaUpdate.getExceptions().forEach( + e -> System.out.println( e.getCause().getMessage() ) + ); + assertEquals( 0, v2schemaUpdate.getExceptions().size() ); new SchemaExport().drop( EnumSet.of( TargetType.DATABASE ), v2metadata ); } + + @Test + public void testSimpleColumnTypeChange() { + String resource1 = "org/hibernate/orm/test/schemaupdate/1_Version.hbm.xml"; + String resource4 = "org/hibernate/orm/test/schemaupdate/4_Version.hbm.xml"; + + MetadataImplementor v1metadata = (MetadataImplementor) new MetadataSources( serviceRegistry ) + .addResource( resource1 ) + .buildMetadata(); + + new SchemaExport().drop( EnumSet.of( TargetType.DATABASE ), v1metadata ); + + final SchemaUpdate v1schemaUpdate = new SchemaUpdate(); + v1schemaUpdate.execute( + EnumSet.of( TargetType.DATABASE, TargetType.STDOUT ), + v1metadata + ); + + v1schemaUpdate.getExceptions().forEach( + e -> System.out.println( e.getCause().getMessage() ) + ); + + assertEquals( 0, v1schemaUpdate.getExceptions().size() ); + + MetadataImplementor v2metadata = (MetadataImplementor) new MetadataSources( serviceRegistry ) + .addResource( resource4 ) + .buildMetadata(); + + final SchemaUpdate v2schemaUpdate = new SchemaUpdate(); + v2schemaUpdate.execute( + EnumSet.of( TargetType.DATABASE, TargetType.STDOUT ), + v2metadata + ); + + v2schemaUpdate.getExceptions().forEach( + e -> System.out.println( e.getCause().getMessage() ) + ); + + assertEquals( 0, v2schemaUpdate.getExceptions().size() ); + + new SchemaExport().drop( EnumSet.of( TargetType.DATABASE ), v2metadata ); + + } // /** // * 3_Version.hbm.xml contains a named unique constraint and an un-named diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/Version.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/Version.java index 43854106f7..7d51a6c376 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/Version.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/Version.java @@ -12,6 +12,7 @@ public class Version { long id; String name; String description; + int versionNumber; public long getId() { return id; @@ -34,4 +35,12 @@ public class Version { public void setDescription(String description) { this.description = description; } + + public void setVersionNumber(int versionNumber) { + this.versionNumber = versionNumber; + } + + public int getVersionNumber() { + return versionNumber; + } }