diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Collate.java b/hibernate-core/src/main/java/org/hibernate/annotations/Collate.java new file mode 100644 index 0000000000..0ce0af0ebd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Collate.java @@ -0,0 +1,36 @@ +/* + * 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.annotations; + +import org.hibernate.Incubating; +import org.hibernate.binder.internal.CollateBinder; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies a collation to use when generating DDL for + * the column mapped by the annotated field or property. + * + * @author Gavin King + * + * @since 6.3 + */ +@Incubating +@AttributeBinderType(binder = CollateBinder.class) +@Target({METHOD, FIELD}) +@Retention(RUNTIME) +public @interface Collate { + /** + * The name of the collation. + */ + String value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/binder/internal/CollateBinder.java b/hibernate-core/src/main/java/org/hibernate/binder/internal/CollateBinder.java new file mode 100644 index 0000000000..b1a43d57ca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/binder/internal/CollateBinder.java @@ -0,0 +1,44 @@ +/* + * 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.binder.internal; + +import org.hibernate.AnnotationException; +import org.hibernate.annotations.Collate; +import org.hibernate.binder.AttributeBinder; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.OneToMany; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.Value; + +/** + * Handles {@link Collate} annotations. + * + * @author Gavin King + */ +public class CollateBinder implements AttributeBinder { + @Override + public void bind(Collate collate, MetadataBuildingContext context, PersistentClass entity, Property property) { + Value value = property.getValue(); + if ( value instanceof OneToMany ) { + throw new AnnotationException( "One to many association '" + property.getName() + + "' was annotated '@Collate'"); + } + else if ( value instanceof Collection ) { + throw new AnnotationException( "Collection '" + property.getName() + + "' was annotated '@Collate'"); + + } + else { + for ( Column column : value.getColumns() ) { + column.setCollation( collate.value() ); + } + } + } +} 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 89d97a5419..9b83bd7ab5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -3413,6 +3413,13 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun return getNullColumnString(); } + /** + * Quote the given collation name if necessary. + */ + public String quoteCollation(String collation) { + return collation; + } + /** * Does this dialect support commenting on tables and columns? * diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index 3cf02c7e26..60e8764f9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -660,4 +660,9 @@ public class HSQLDialect extends Dialect { public UniqueDelegate getUniqueDelegate() { return uniqueDelegate; } + + @Override + public String quoteCollation(String collation) { + return '\"' + collation + '\"'; + } } 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 eeece6d36e..43cffa420b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -861,6 +861,11 @@ public class PostgreSQLDialect extends Dialect { return "null::" + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName(); } + @Override + public String quoteCollation(String collation) { + return '\"' + collation + '\"'; + } + @Override public boolean supportsCommentOn() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/AggregateColumn.java b/hibernate-core/src/main/java/org/hibernate/mapping/AggregateColumn.java index c3c728b590..52c39f361c 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/AggregateColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/AggregateColumn.java @@ -36,6 +36,7 @@ public class AggregateColumn extends Column { addCheckConstraint( constraint ); } setComment( column.getComment() ); + setCollation( column.getCollation() ); setDefaultValue( column.getDefaultValue() ); setGeneratedAs( column.getGeneratedAs() ); setAssignmentExpression( column.getAssignmentExpression() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index 043bb759c3..20c5b7a3cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -65,7 +65,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn private String customWrite; private String customRead; private Size columnSize; -// private String specializedTypeDeclaration; + private String collation; private java.util.List checkConstraints = new ArrayList<>(); public Column() { @@ -614,6 +614,14 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn this.comment = comment; } + public String getCollation() { + return collation; + } + + public void setCollation(String collation) { + this.collation = collation; + } + public String getDefaultValue() { return defaultValue; } 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 index d3cf302015..45f9787838 100644 --- 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 @@ -183,6 +183,11 @@ class ColumnDefinitions { definition.append( ' ' ).append( columnType ); } + String collation = column.getCollation(); + if ( collation != null ) { + definition.append(" collate ").append( dialect.quoteCollation( collation ) ); + } + final String defaultValue = column.getDefaultValue(); if ( defaultValue != null ) { definition.append( " default " ).append( defaultValue ); @@ -205,7 +210,7 @@ class ColumnDefinitions { 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 ) ); + && column.getQuotedName( dialect ).equals( getPrimaryKeyColumnName( table, dialect ) ); } private static String getPrimaryKeyColumnName(Table table, Dialect dialect) { @@ -221,13 +226,13 @@ class ColumnDefinitions { // && table.getPrimaryKey().getColumn( 0 ).isIdentity(); MetadataImplementor metadataImplementor = (MetadataImplementor) metadata; return table.hasPrimaryKey() - && table.getIdentifierValue() != null - && table.getIdentifierValue() - .isIdentityColumn( - metadataImplementor.getMetadataBuildingOptions() - .getIdentifierGeneratorFactory(), - dialect - ); + && table.getIdentifierValue() != null + && table.getIdentifierValue() + .isIdentityColumn( + metadataImplementor.getMetadataBuildingOptions() + .getIdentifierGeneratorFactory(), + dialect + ); } private static String stripArgs(String string) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/MySQLCollateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/MySQLCollateTest.java new file mode 100644 index 0000000000..99999df55b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/MySQLCollateTest.java @@ -0,0 +1,38 @@ +package org.hibernate.orm.test.annotations.collate; + +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.Collate; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.testing.orm.junit.DomainModel; +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.Test; + +@SessionFactory +@DomainModel(annotatedClasses = MySQLCollateTest.Message.class) +@RequiresDialect(MySQLDialect.class) +public class MySQLCollateTest { + + @Test void test(SessionFactoryScope scope) { + scope.inTransaction(session -> session.persist(new Message("Hello, world!"))); + } + + @Entity(name = "msgs") + static class Message { + @Id @GeneratedValue + Long id; + @Basic(optional = false) + @Collate("utf8mb4_spanish2_ci") + @Column(length = 200) + String text; + + public Message(String text) { + this.text = text; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/PostgresCollateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/PostgresCollateTest.java new file mode 100644 index 0000000000..3493b485b1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collate/PostgresCollateTest.java @@ -0,0 +1,38 @@ +package org.hibernate.orm.test.annotations.collate; + +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.Collate; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.orm.junit.DomainModel; +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.Test; + +@SessionFactory +@DomainModel(annotatedClasses = PostgresCollateTest.Message.class) +@RequiresDialect(PostgreSQLDialect.class) +public class PostgresCollateTest { + + @Test void test(SessionFactoryScope scope) { + scope.inTransaction(session -> session.persist(new Message("Hello, world!"))); + } + + @Entity(name = "msgs") + static class Message { + @Id @GeneratedValue + Long id; + @Basic(optional = false) + @Collate("es_ES") + @Column(length = 200) + String text; + + public Message(String text) { + this.text = text; + } + } +}