Skip to content

Commit

Permalink
HHH-16531 ColumnDefinitions should respect @column(columnDefinition)
Browse files Browse the repository at this point in the history
  • Loading branch information
quaff committed Apr 11, 2024
1 parent ce97a5d commit 9ecad4b
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.hibernate.mapping.Table;
import org.hibernate.mapping.UniqueKey;
import org.hibernate.tool.schema.extract.spi.ColumnInformation;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.jdbc.JdbcType;

import java.util.List;
Expand All @@ -28,11 +29,16 @@
class ColumnDefinitions {

static boolean hasMatchingType(Column column, ColumnInformation columnInformation, Metadata metadata, Dialect dialect) {
boolean typesMatch = dialect.equivalentTypes( column.getSqlTypeCode(metadata), columnInformation.getTypeCode() )
|| normalize( stripArgs( column.getSqlType( metadata ) ) ).equals( normalize( columnInformation.getTypeName() ) );
Integer inferredTypeCode = column.getTypeName() != null ? inferTypeCode( column.getTypeName() ) : null; //infer from @Column(columnDefinition)
int requiredTypeCode = inferredTypeCode != null ? inferredTypeCode : column.getSqlTypeCode(metadata);
boolean typesMatch = dialect.equivalentTypes( requiredTypeCode, columnInformation.getTypeCode() )
|| extractTypeName( column.getSqlType( metadata ) ).equals( normalize( columnInformation.getTypeName() ) );
if ( typesMatch ) {
return true;
}
else if ( column.getTypeName() != null ) {
return false;
}
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.
Expand Down Expand Up @@ -248,12 +254,18 @@ private static String normalize(String typeName) {
else {
final String lowerCaseTypName = typeName.toLowerCase(Locale.ROOT);
switch (lowerCaseTypName) {
case "int":
return "integer";
case "character":
return "char";
case "character varying":
return "varchar";
case "binary varying":
return "varbinary";
case "character large object":
return "clob";
case "binary large object":
return "blob";
case "interval second":
return "interval";
default:
Expand All @@ -262,13 +274,37 @@ private static String normalize(String typeName) {
}
}

private static String stripArgs(String typeExpression) {
if ( typeExpression == null ) {
return null;
}
else {
int i = typeExpression.indexOf('(');
return i>0 ? typeExpression.substring(0,i).trim() : typeExpression;
private static String extractTypeName(String typeExpression) {
String typeName = typeExpression.toLowerCase(Locale.ROOT);
typeName = keepBefore( typeName, " default " ); // Strip default value
typeName = keepBefore( typeName, " not null" );
typeName = stripArgs( typeName );
typeName = normalize( typeName );
return typeName;
}

private static Integer inferTypeCode(String typeExpression) {
String typeName = extractTypeName(typeExpression);
// timestamp with time zone -> timestamp_with_timezone
typeName = typeName.replace( ' ', '_' ).replace( "time zone", "timezone" );
try {
Object value = SqlTypes.class.getField( typeName.toUpperCase(Locale.ROOT) ).get( null );
if ( value instanceof Integer ) {
return (Integer) value;
}
} catch ( IllegalAccessException | NoSuchFieldException ignored ) {
// ignore
}
return null;
}

private static String keepBefore(String input, String token) {
int i = input.indexOf( token );
return i > 0 ? input.substring( 0, i ).trim() : input;
}

private static String stripArgs(String typeExpression) {
// 'timestamp(6) with time zone' should be 'timestamp with time zone'
return typeExpression.replaceAll("\\(.*\\)", "").trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ public static boolean isStringType(int typeCode) {
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
return true;
default:
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.tool.schema.internal;

import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.model.TruthValue;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Value;
import org.hibernate.testing.util.ServiceRegistryUtil;
import org.hibernate.tool.schema.extract.internal.ColumnInformationImpl;
import org.hibernate.tool.schema.extract.spi.ColumnInformation;
import org.hibernate.type.JavaObjectType;
import org.hibernate.type.SqlTypes;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

/**
* @author Yanming Zhou
*/
public class ColumnDefinitionsTest {

@Test
public void matchIntegerType() {
assertHasMatchingType("integer", SqlTypes.INTEGER, "integer", SqlTypes.INTEGER, true);
assertHasMatchingType("integer", SqlTypes.INTEGER, "int", SqlTypes.INTEGER, true);
assertHasMatchingType("int", SqlTypes.INTEGER, "integer", SqlTypes.INTEGER, true);
assertHasMatchingType("integer", SqlTypes.INTEGER, "bigint", SqlTypes.BIGINT, false);
assertHasMatchingType("bigint", SqlTypes.BIGINT, "integer", SqlTypes.INTEGER, false);
}

@Test
public void matchDecimalType() {
assertHasMatchingType("decimal(10,2)", SqlTypes.DECIMAL, "decimal", SqlTypes.DECIMAL, true);
assertHasMatchingType("decimal( 10 , 2 )", SqlTypes.DECIMAL, "decimal", SqlTypes.DECIMAL, true);
}

@Test
public void matchComplexCharacterVaryingType() {
assertHasMatchingType("character varying(255) not null default '-'", SqlTypes.VARCHAR, "varchar", SqlTypes.VARCHAR, true);
}

@Test
public void shouldNotMatchIfColumnDefinitionNotEqualsToActualTypeNameButInferredTypeCodeEqualsToActualTypeCode() {
assertHasMatchingType("integer", SqlTypes.BIGINT, "bigint", SqlTypes.BIGINT, false);
assertHasMatchingType("integer(8) not null default 0", SqlTypes.BIGINT, "bigint", SqlTypes.BIGINT, false);
}

private void assertHasMatchingType(String columnDefinition, int inferredTypeCode, String actualTypeName, int actualTypeCode, boolean matching) {
Column column = new Column();
Value value = mock();
given(value.getType()).willReturn(JavaObjectType.INSTANCE);
column.setValue(value);
column.setSqlType(columnDefinition);
column.setSqlTypeCode(inferredTypeCode);

ColumnInformation columnInformation = new ColumnInformationImpl(
null,
null,
actualTypeCode,
actualTypeName,
255,
0,
TruthValue.TRUE
);

Metadata metadata = new MetadataSources( ServiceRegistryUtil.serviceRegistry() ).buildMetadata();
assertThat(ColumnDefinitions.hasMatchingType(column, columnInformation, metadata, metadata.getDatabase().getDialect()),
is(matching));
}

}

0 comments on commit 9ecad4b

Please sign in to comment.