HHH-15548 Fix schema validation issues on PostgreSQL with Instant type

This commit is contained in:
Christian Beikov 2022-10-04 13:55:11 +02:00
parent 7e1d4cad83
commit 12df6317ec
6 changed files with 269 additions and 38 deletions

View File

@ -242,25 +242,33 @@ public class CockroachLegacyDialect extends Dialect {
int precision,
int scale,
JdbcTypeRegistry jdbcTypeRegistry) {
if ( jdbcTypeCode == OTHER ) {
switch ( columnTypeName ) {
case "uuid":
jdbcTypeCode = UUID;
break;
case "json":
case "jsonb":
jdbcTypeCode = JSON;
break;
case "inet":
jdbcTypeCode = INET;
break;
case "geometry":
jdbcTypeCode = GEOMETRY;
break;
case "geography":
jdbcTypeCode = GEOGRAPHY;
break;
}
switch ( jdbcTypeCode ) {
case OTHER:
switch ( columnTypeName ) {
case "uuid":
jdbcTypeCode = UUID;
break;
case "json":
case "jsonb":
jdbcTypeCode = JSON;
break;
case "inet":
jdbcTypeCode = INET;
break;
case "geometry":
jdbcTypeCode = GEOMETRY;
break;
case "geography":
jdbcTypeCode = GEOGRAPHY;
break;
}
break;
case TIMESTAMP:
// The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant
if ( "timestamptz".equals( columnTypeName ) ) {
jdbcTypeCode = TIMESTAMP_UTC;
}
break;
}
return jdbcTypeRegistry.getDescriptor( jdbcTypeCode );
}

View File

@ -121,6 +121,7 @@ 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.TIMESTAMP;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.TINYINT;
@ -307,6 +308,12 @@ public class PostgreSQLLegacyDialect extends Dialect {
break;
}
break;
case TIMESTAMP:
// The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant
if ( "timestamptz".equals( columnTypeName ) ) {
jdbcTypeCode = TIMESTAMP_UTC;
}
break;
case ARRAY:
final JdbcType jdbcType = jdbcTypeRegistry.getDescriptor( jdbcTypeCode );
// PostgreSQL names array types by prepending an underscore to the base name

View File

@ -86,6 +86,7 @@ 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.TIMESTAMP;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.TINYINT;
@ -251,25 +252,33 @@ public class CockroachDialect extends Dialect {
int precision,
int scale,
JdbcTypeRegistry jdbcTypeRegistry) {
if ( jdbcTypeCode == OTHER ) {
switch ( columnTypeName ) {
case "uuid":
jdbcTypeCode = UUID;
break;
case "json":
case "jsonb":
jdbcTypeCode = JSON;
break;
case "inet":
jdbcTypeCode = INET;
break;
case "geometry":
jdbcTypeCode = GEOMETRY;
break;
case "geography":
jdbcTypeCode = GEOGRAPHY;
break;
}
switch ( jdbcTypeCode ) {
case OTHER:
switch ( columnTypeName ) {
case "uuid":
jdbcTypeCode = UUID;
break;
case "json":
case "jsonb":
jdbcTypeCode = JSON;
break;
case "inet":
jdbcTypeCode = INET;
break;
case "geometry":
jdbcTypeCode = GEOMETRY;
break;
case "geography":
jdbcTypeCode = GEOGRAPHY;
break;
}
break;
case TIMESTAMP:
// The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant
if ( "timestamptz".equals( columnTypeName ) ) {
jdbcTypeCode = TIMESTAMP_UTC;
}
break;
}
return jdbcTypeRegistry.getDescriptor( jdbcTypeCode );
}

View File

@ -104,6 +104,7 @@ 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.TIMESTAMP;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.TINYINT;
@ -291,6 +292,12 @@ public class PostgreSQLDialect extends Dialect {
break;
}
break;
case TIMESTAMP:
// The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant
if ( "timestamptz".equals( columnTypeName ) ) {
jdbcTypeCode = TIMESTAMP_UTC;
}
break;
case ARRAY:
final JdbcType jdbcType = jdbcTypeRegistry.getDescriptor( jdbcTypeCode );
// PostgreSQL names array types by prepending an underscore to the base name

View File

@ -7,6 +7,7 @@
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;
@ -30,6 +31,7 @@ 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;
@ -161,6 +163,18 @@ public abstract class AbstractSchemaValidator implements SchemaValidator {
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 ) {
throw new SchemaManagementException(
String.format(

View File

@ -0,0 +1,186 @@
/*
* 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.orm.test.schemavalidation;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.JdbcMetadaAccessStrategy;
import org.hibernate.tool.schema.SourceType;
import org.hibernate.tool.schema.TargetType;
import org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl;
import org.hibernate.tool.schema.spi.ContributableMatcher;
import org.hibernate.tool.schema.spi.ExceptionHandler;
import org.hibernate.tool.schema.spi.ExecutionOptions;
import org.hibernate.tool.schema.spi.SchemaFilter;
import org.hibernate.tool.schema.spi.SchemaManagementTool;
import org.hibernate.tool.schema.spi.ScriptSourceInput;
import org.hibernate.tool.schema.spi.ScriptTargetOutput;
import org.hibernate.tool.schema.spi.SourceDescriptor;
import org.hibernate.tool.schema.spi.TargetDescriptor;
import org.hibernate.testing.TestForIssue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
/**
* Test that an existing timestamp with timezone column works for fields that use java.time.Instant.
*/
@TestForIssue(jiraKey = "HHH-15548")
@RunWith(Parameterized.class)
public class InstantValidationTest implements ExecutionOptions {
@Parameterized.Parameters
public static Collection<String> parameters() {
return Arrays.asList(
JdbcMetadaAccessStrategy.GROUPED.toString(),
JdbcMetadaAccessStrategy.INDIVIDUALLY.toString()
);
}
@Parameterized.Parameter
public String jdbcMetadataExtractorStrategy;
private StandardServiceRegistry ssr;
private MetadataImplementor metadata;
private MetadataImplementor oldMetadata;
@Before
public void beforeTest() {
ssr = new StandardServiceRegistryBuilder()
.applySetting(
AvailableSettings.HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY,
jdbcMetadataExtractorStrategy
)
.build();
oldMetadata = (MetadataImplementor) new MetadataSources( ssr )
.addAnnotatedClass( TestEntityOld.class )
.buildMetadata();
oldMetadata.validate();
metadata = (MetadataImplementor) new MetadataSources( ssr )
.addAnnotatedClass( TestEntity.class )
.buildMetadata();
metadata.validate();
try {
dropSchema();
// create the schema
createSchema();
}
catch (Exception e) {
tearDown();
throw e;
}
}
@After
public void tearDown() {
dropSchema();
if ( ssr != null ) {
StandardServiceRegistryBuilder.destroy( ssr );
}
}
@Test
public void testValidation() {
doValidation();
}
private void doValidation() {
ssr.getService( SchemaManagementTool.class ).getSchemaValidator( null )
.doValidation( metadata, this, ContributableMatcher.ALL );
}
private void createSchema() {
ssr.getService( SchemaManagementTool.class ).getSchemaCreator( null ).doCreation(
oldMetadata,
this,
ContributableMatcher.ALL,
new SourceDescriptor() {
@Override
public SourceType getSourceType() {
return SourceType.METADATA;
}
@Override
public ScriptSourceInput getScriptSourceInput() {
return null;
}
},
new TargetDescriptor() {
@Override
public EnumSet<TargetType> getTargetTypes() {
return EnumSet.of( TargetType.DATABASE );
}
@Override
public ScriptTargetOutput getScriptTargetOutput() {
return null;
}
}
);
}
private void dropSchema() {
new SchemaExport()
.drop( EnumSet.of( TargetType.DATABASE ), oldMetadata );
}
@Entity(name = "TestEntity")
public static class TestEntityOld {
@Id
public Integer id;
@Column(name = "instantVal")
Instant instantVal;
}
@Entity(name = "TestEntity")
public static class TestEntity {
@Id
public Integer id;
@Column(name = "instantVal")
Instant instantVal;
}
@Override
public Map getConfigurationValues() {
return ssr.getService( ConfigurationService.class ).getSettings();
}
@Override
public boolean shouldManageNamespaces() {
return false;
}
@Override
public ExceptionHandler getExceptionHandler() {
return ExceptionHandlerLoggedImpl.INSTANCE;
}
@Override
public SchemaFilter getSchemaFilter() {
return SchemaFilter.ALL;
}
}