diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 9c46b0f7d8..6b7eaa08c0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -75,6 +75,7 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2 import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; +import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; import org.hibernate.type.descriptor.jdbc.InstantJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -94,6 +95,7 @@ import static org.hibernate.type.SqlTypes.DOUBLE; import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; +import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -248,6 +250,9 @@ public class H2LegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) ); } + if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + } } } @@ -265,6 +270,9 @@ public class H2LegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); } + if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE ); + } } @Override @@ -392,6 +400,9 @@ public class H2LegacyDialect extends Dialect { if ( "GEOMETRY".equals( columnTypeName ) ) { return jdbcTypeRegistry.getDescriptor( GEOMETRY ); } + else if ( "JSON".equals( columnTypeName ) ) { + return jdbcTypeRegistry.getDescriptor( JSON ); + } break; } return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry ); 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 9ba5b556d0..3c3f969c3f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -68,6 +68,7 @@ import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2DatabaseImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; +import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; import org.hibernate.type.descriptor.jdbc.InstantJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -88,6 +89,7 @@ import static org.hibernate.type.SqlTypes.DOUBLE; import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; +import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -229,6 +231,9 @@ public class H2Dialect extends Dialect { if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) ); } + if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + } } @Override @@ -243,6 +248,9 @@ public class H2Dialect extends Dialect { if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); } + if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + jdbcTypeRegistry.addDescriptorIfAbsent( H2FormatJsonJdbcType.INSTANCE ); + } } @Override @@ -370,6 +378,9 @@ public class H2Dialect extends Dialect { if ( "GEOMETRY".equals( columnTypeName ) ) { return jdbcTypeRegistry.getDescriptor( GEOMETRY ); } + else if ( "JSON".equals( columnTypeName ) ) { + return jdbcTypeRegistry.getDescriptor( JSON ); + } break; } return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java new file mode 100644 index 0000000000..f6b383caf8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/H2FormatJsonJdbcType.java @@ -0,0 +1,48 @@ +/* + * 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.type.descriptor.jdbc; + +import org.hibernate.dialect.Dialect; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.sql.ast.spi.SqlAppender; + +/** + * Specialized type mapping for {@code JSON} that utilizes the custom + * '{@code ? format json}' write expression for H2. + * + * @author Marco Belladelli + */ +public class H2FormatJsonJdbcType extends JsonJdbcType { + /** + * Singleton access + */ + public static final H2FormatJsonJdbcType INSTANCE = new H2FormatJsonJdbcType( null ); + + protected H2FormatJsonJdbcType(EmbeddableMappingType embeddableMappingType) { + super( embeddableMappingType ); + } + + @Override + public String toString() { + return "FormatJsonJdbcType"; + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new H2FormatJsonJdbcType( mappingType ); + } + + @Override + public void appendWriteExpression(String writeExpression, SqlAppender appender, Dialect dialect) { + appender.append( writeExpression ); + appender.append( " format json" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/H2JsonListTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/H2JsonListTest.java new file mode 100644 index 0000000000..cf4ceee8a0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/H2JsonListTest.java @@ -0,0 +1,159 @@ +/* + * 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.type; + +import java.util.List; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +@SessionFactory +@DomainModel( annotatedClasses = { H2JsonListTest.Path.class, H2JsonListTest.PathClob.class } ) +@RequiresDialect( H2Dialect.class ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16320" ) +public class H2JsonListTest { + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Path( List.of( UUID.randomUUID(), UUID.randomUUID() ) ) ); + session.persist( new PathClob( List.of( UUID.randomUUID(), UUID.randomUUID() ) ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Path" ).executeUpdate(); + session.createMutationQuery( "delete from PathClob" ).executeUpdate(); + } ); + } + + @Test + public void testRetrievalJson(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Path path = session.find( Path.class, 1L ); + assertThat( path ).isNotNull(); + assertThat( path.getRelativePaths() ).hasSize( 2 ); + } ); + } + + @Test + public void testNativeSyntaxJson(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createNativeMutationQuery( "insert into paths (relativePaths,id) values (?1 FORMAT JSON, ?2)" ) + .setParameter( + 1, + "[\"2b099c92-95ff-42e0-9f8c-f08c2518792d\", \"8d2164db-86b4-460a-91d0-bf821a8ca3d7\"]" + ) + .setParameter( 2, 99L ) + .executeUpdate(); + } ); + scope.inTransaction( session -> { + final Path path = session.createNativeQuery( + "select * from paths_clob where id = 99", + Path.class + ).getSingleResult(); + assertThat( path ).isNotNull(); + assertThat( path.getRelativePaths() ).hasSize( 2 ); + } ); + } + + @Test + public void testRetrievalClob(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final PathClob path = session.find( PathClob.class, 1L ); + assertThat( path ).isNotNull(); + assertThat( path.getRelativePaths() ).hasSize( 2 ); + } ); + } + + @Test + public void testNativeSyntaxClob(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createNativeMutationQuery( "insert into paths_clob (relativePaths,id) values (?1 FORMAT JSON, ?2)" ) + .setParameter( + 1, + "[\"2b099c92-95ff-42e0-9f8c-f08c2518792d\", \"8d2164db-86b4-460a-91d0-bf821a8ca3d7\"]" + ) + .setParameter( 2, 99L ) + .executeUpdate(); + } ); + scope.inTransaction( session -> { + final PathClob path = session.createNativeQuery( + "select * from paths_clob where id = 99", + PathClob.class + ).getSingleResult(); + assertThat( path ).isNotNull(); + assertThat( path.getRelativePaths() ).hasSize( 2 ); + } ); + } + + @Entity( name = "Path" ) + @Table( name = "paths" ) + public static class Path { + @Id + @GeneratedValue + public Long id; + + @JdbcTypeCode( SqlTypes.JSON ) + public List relativePaths; + + public Path() { + } + + public Path(List relativePaths) { + this.relativePaths = relativePaths; + } + + public List getRelativePaths() { + return relativePaths; + } + } + + @Entity( name = "PathClob" ) + @Table( name = "paths_clob" ) + public static class PathClob { + @Id + @GeneratedValue + public Long id; + + @JdbcTypeCode( SqlTypes.JSON ) + @Column( columnDefinition = "clob" ) + public List relativePaths; + + public PathClob() { + } + + public PathClob(List relativePaths) { + this.relativePaths = relativePaths; + } + + public List getRelativePaths() { + return relativePaths; + } + } +}