Skip to content

Commit

Permalink
HHH-15548 Fix schema validation issues on PostgreSQL with Instant type
Browse files Browse the repository at this point in the history
  • Loading branch information
beikov committed Oct 4, 2022
1 parent 8a8de55 commit f679ce7
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 19 deletions.
Expand Up @@ -87,6 +87,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.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;
Expand Down Expand Up @@ -250,25 +251,33 @@ public JdbcType resolveSqlTypeDescriptor(
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 );
}
Expand Down
Expand Up @@ -107,6 +107,7 @@
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;
Expand Down Expand Up @@ -291,6 +292,12 @@ public JdbcType resolveSqlTypeDescriptor(
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
Expand Down
Expand Up @@ -30,6 +30,7 @@
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;

Expand Down Expand Up @@ -161,6 +162,18 @@ protected void validateColumnType(
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(
Expand Down
@@ -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;
}
}

0 comments on commit f679ce7

Please sign in to comment.