HHH-15762 work around weird semantics of null in unique index on DB2/T-SQL

This commit is contained in:
Gavin 2022-11-26 17:44:47 +01:00 committed by Gavin King
parent 0253e1fe7a
commit 5172d8798f
30 changed files with 412 additions and 146 deletions

View File

@ -11,6 +11,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import org.hibernate.LockOptions;
import org.hibernate.boot.model.TypeContributions;
@ -32,7 +33,9 @@
import org.hibernate.dialect.sequence.DB2SequenceSupport;
import org.hibernate.dialect.sequence.LegacyDB2SequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.DB2UniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -41,6 +44,7 @@
import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.mapping.Column;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.query.spi.QueryEngine;
@ -207,7 +211,9 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR
}
protected UniqueDelegate createUniqueDelegate() {
return new DB2UniqueDelegate( this );
return getDB2Version().isSameOrAfter(10,5)
? new AlterTableUniqueIndexDelegate( this )
: new SkipNullableUniqueDelegate( this );
}
@Override
@ -487,6 +493,11 @@ public boolean dropConstraints() {
return false;
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return unique ? " exclude null keys" : "";
}
@Override
public SequenceSupport getSequenceSupport() {
return getDB2Version().isBefore( 9, 7 )

View File

@ -17,10 +17,12 @@
import org.hibernate.dialect.sequence.DB2iSequenceSupport;
import org.hibernate.dialect.sequence.NoSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.AlterTableUniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.mapping.Column;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -28,6 +30,8 @@
import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.exec.spi.JdbcOperation;
import java.util.List;
/**
* An SQL dialect for DB2 for iSeries previously known as DB2/400.
*
@ -69,9 +73,21 @@ public DatabaseVersion getDB2Version() {
@Override
protected UniqueDelegate createUniqueDelegate() {
return getVersion().isSameOrAfter(7, 3)
? new AlterTableUniqueDelegate(this)
: super.createUniqueDelegate();
return getVersion().isSameOrAfter(7, 1)
? new AlterTableUniqueIndexDelegate(this)
: new SkipNullableUniqueDelegate(this);
}
@Override
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique where not null index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return "";
}
@Override

View File

@ -18,8 +18,12 @@
import org.hibernate.dialect.sequence.DB2zSequenceSupport;
import org.hibernate.dialect.sequence.NoSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.mapping.Column;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.IntervalType;
import org.hibernate.query.sqm.TemporalUnit;
@ -31,6 +35,8 @@
import jakarta.persistence.TemporalType;
import java.util.List;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
/**
@ -80,6 +86,25 @@ public DatabaseVersion getDB2Version() {
return DB2_LUW_VERSION9;
}
@Override
protected UniqueDelegate createUniqueDelegate() {
return getVersion().isSameOrAfter(11)
? new AlterTableUniqueIndexDelegate(this)
: new SkipNullableUniqueDelegate(this);
}
@Override
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique where not null index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return "";
}
@Override
public boolean supportsDistinctFromPredicate() {
// Supported at least since DB2 z/OS 9.0

View File

@ -30,6 +30,9 @@
import org.hibernate.dialect.sequence.SQLServer16SequenceSupport;
import org.hibernate.dialect.sequence.SQLServerSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -39,6 +42,7 @@
import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.mapping.Column;
import org.hibernate.query.sqm.CastType;
import org.hibernate.query.sqm.FetchClauseType;
import org.hibernate.query.sqm.IntervalType;
@ -71,6 +75,7 @@
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import jakarta.persistence.TemporalType;
@ -91,6 +96,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
private static final int PARAM_LIST_SIZE_LIMIT = 2100;
private final StandardSequenceExporter exporter;
private final UniqueDelegate uniqueDelegate;
public SQLServerLegacyDialect() {
this( DatabaseVersion.make( 8, 0 ) );
@ -99,17 +105,25 @@ public SQLServerLegacyDialect() {
public SQLServerLegacyDialect(DatabaseVersion version) {
super(version);
exporter = createSequenceExporter(version);
uniqueDelegate = createUniqueDelgate(version);
}
public SQLServerLegacyDialect(DialectResolutionInfo info) {
super(info);
exporter = createSequenceExporter(info);
uniqueDelegate = createUniqueDelgate(info);
}
private StandardSequenceExporter createSequenceExporter(DatabaseVersion version) {
return version.isSameOrAfter(11) ? new SqlServerSequenceExporter(this) : null;
}
private UniqueDelegate createUniqueDelgate(DatabaseVersion version) {
return version.isSameOrAfter(10)
? new AlterTableUniqueIndexDelegate(this)
: new SkipNullableUniqueDelegate(this);
}
@Override
protected void registerDefaultKeywords() {
super.registerDefaultKeywords();
@ -952,6 +966,36 @@ public Exporter<Sequence> getSequenceExporter() {
return exporter;
}
@Override
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
@Override
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique nonclustered index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
if (unique) {
StringBuilder tail = new StringBuilder();
for ( Column column : columns ) {
if ( column.isNullable() ) {
tail.append( tail.length() == 0 ? " where " : " and " )
.append( column.getQuotedName( this ) )
.append( " is not null" );
}
}
return tail.toString();
}
else {
return "";
}
}
private static class SqlServerSequenceExporter extends StandardSequenceExporter {
public SqlServerSequenceExporter(Dialect dialect) {

View File

@ -17,6 +17,8 @@
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.CountFunction;
import org.hibernate.dialect.function.IntegralTimestampaddFunction;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -68,6 +70,7 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect {
//All Sybase dialects share an IN list size limit.
private static final int PARAM_LIST_SIZE_LIMIT = 250000;
private final UniqueDelegate uniqueDelegate = new SkipNullableUniqueDelegate(this);
public SybaseLegacyDialect() {
this( DatabaseVersion.make( 11, 0 ) );
@ -338,4 +341,8 @@ public NameQualifierSupport getNameQualifierSupport() {
return NameQualifierSupport.CATALOG;
}
@Override
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
}

View File

@ -2020,9 +2020,15 @@ private void buildUniqueKeyFromColumnNames(
for ( int index = 0; index < size; index++ ) {
final String logicalColumnName = columnNames[index];
try {
final String physicalColumnName = getPhysicalColumnName( table, logicalColumnName );
columns[index] = new Column( physicalColumnName );
unbound.add( columns[index] );
Column column = table.getColumn( buildingContext.getMetadataCollector(), logicalColumnName );
if ( column == null ) {
throw new AnnotationException(
"Table '" + table.getName() + "' has no column named '" + logicalColumnName
+ "' matching the column specified in '@UniqueConstraint'"
);
}
columns[index] = column;
unbound.add( column );
//column equals and hashcode is based on column name
}
catch ( MappingException e ) {

View File

@ -91,9 +91,7 @@ public void doSecondPass(Map<String, PersistentClass> persistentClasses) throws
}
private void addConstraintToColumn(final String columnName ) {
Column column = table.getColumn(
new Column( buildingContext.getMetadataCollector().getPhysicalColumnName( table, columnName ) )
);
Column column = table.getColumn( buildingContext.getMetadataCollector(), columnName );
if ( column == null ) {
throw new AnnotationException(
"Table '" + table.getName() + "' has no column named '" + columnName

View File

@ -10,8 +10,6 @@
import org.hibernate.LockOptions;
import org.hibernate.dialect.function.CastingConcatFunction;
import org.hibernate.dialect.function.TransactSQLStrFunction;
import org.hibernate.dialect.unique.CreateTableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.query.sqm.NullOrdering;
import org.hibernate.dialect.function.CommonFunctionFactory;
@ -51,8 +49,6 @@
*/
public abstract class AbstractTransactSQLDialect extends Dialect {
private final UniqueDelegate uniqueDelegate = new CreateTableUniqueDelegate(this);
public AbstractTransactSQLDialect(DatabaseVersion version) {
super(version);
}
@ -392,9 +388,4 @@ public void appendBinaryLiteral(SqlAppender appender, byte[] bytes) {
appender.appendSql( "0x" );
PrimitiveByteArrayJavaType.INSTANCE.appendString( appender, bytes );
}
@Override
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
}

View File

@ -21,7 +21,7 @@
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.sequence.DB2SequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.DB2UniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -30,6 +30,7 @@
import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.mapping.Column;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.query.sqm.IntervalType;
@ -66,6 +67,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import jakarta.persistence.TemporalType;
@ -181,7 +183,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR
}
protected UniqueDelegate createUniqueDelegate() {
return new DB2UniqueDelegate( this );
return new AlterTableUniqueIndexDelegate( this );
}
@Override
@ -495,6 +497,11 @@ public boolean dropConstraints() {
return false;
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return unique ? " exclude null keys" : "";
}
@Override
public SequenceSupport getSequenceSupport() {
return DB2SequenceSupport.INSTANCE;

View File

@ -16,10 +16,9 @@
import org.hibernate.dialect.sequence.DB2iSequenceSupport;
import org.hibernate.dialect.sequence.NoSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.AlterTableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.mapping.Column;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -27,6 +26,8 @@
import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.exec.spi.JdbcOperation;
import java.util.List;
/**
* An SQL dialect for DB2 for iSeries previously known as DB2/400.
*
@ -73,10 +74,15 @@ public DatabaseVersion getDB2Version() {
}
@Override
protected UniqueDelegate createUniqueDelegate() {
return getVersion().isSameOrAfter(7, 3)
? new AlterTableUniqueDelegate(this)
: super.createUniqueDelegate();
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique where not null index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return "";
}
@Override

View File

@ -18,6 +18,7 @@
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.mapping.Column;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.IntervalType;
import org.hibernate.query.sqm.TemporalUnit;
@ -27,6 +28,8 @@
import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.exec.spi.JdbcOperation;
import java.util.List;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
/**
@ -82,6 +85,18 @@ public DatabaseVersion getDB2Version() {
return DB2_LUW_VERSION;
}
@Override
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique where not null index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return "";
}
@Override
public boolean supportsDistinctFromPredicate() {
// Supported at least since DB2 z/OS 9.0

View File

@ -102,6 +102,7 @@
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.io.StreamCopier;
import org.hibernate.loader.BatchLoadSizingStrategy;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Constraint;
import org.hibernate.mapping.ForeignKey;
import org.hibernate.mapping.Index;
@ -1983,6 +1984,14 @@ public String getCreateTableString() {
return "create table";
}
public String getCreateIndexString(boolean unique) {
return unique ? "create unique index" : "create index";
}
public String getCreateIndexTail(boolean unique, List<Column> columns) {
return "";
}
/**
* Command used to alter a table.
*

View File

@ -24,6 +24,8 @@
import org.hibernate.dialect.sequence.SQLServer16SequenceSupport;
import org.hibernate.dialect.sequence.SQLServerSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -33,6 +35,7 @@
import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.mapping.Column;
import org.hibernate.query.sqm.CastType;
import org.hibernate.query.sqm.FetchClauseType;
import org.hibernate.query.sqm.IntervalType;
@ -68,6 +71,7 @@
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import jakarta.persistence.TemporalType;
@ -80,7 +84,7 @@
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros;
/**
* A dialect for Microsoft SQL Server 2000 and above
* A dialect for Microsoft SQL Server 2008 and above
*
* @author Gavin King
*/
@ -89,6 +93,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
private static final int PARAM_LIST_SIZE_LIMIT = 2100;
private final StandardSequenceExporter exporter;
private final UniqueDelegate uniqueDelegate = new AlterTableUniqueIndexDelegate(this);
public SQLServerDialect() {
this( MINIMUM_VERSION );
@ -937,12 +942,42 @@ public String[] getDropSchemaCommand(String schemaName) {
return super.getDropSchemaCommand( schemaName );
}
@Override
public String getCreateIndexString(boolean unique) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? "create unique nonclustered index" : "create index";
}
@Override
public String getCreateIndexTail(boolean unique, List<Column> columns) {
if (unique) {
StringBuilder tail = new StringBuilder();
for ( Column column : columns ) {
if ( column.isNullable() ) {
tail.append( tail.length() == 0 ? " where " : " and " )
.append( column.getQuotedName( this ) )
.append( " is not null" );
}
}
return tail.toString();
}
else {
return "";
}
}
@Override
public NameQualifierSupport getNameQualifierSupport() {
return NameQualifierSupport.BOTH;
}
@Override
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
@Override
public Exporter<Sequence> getSequenceExporter() {
if ( exporter == null ) {
return super.getSequenceExporter();

View File

@ -10,6 +10,8 @@
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.CountFunction;
import org.hibernate.dialect.function.IntegralTimestampaddFunction;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.dialect.unique.UniqueDelegate;
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
@ -50,6 +52,7 @@
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Types;
import jakarta.persistence.TemporalType;
@ -66,6 +69,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect {
//All Sybase dialects share an IN list size limit.
private static final int PARAM_LIST_SIZE_LIMIT = 250000;
private final UniqueDelegate uniqueDelegate = new SkipNullableUniqueDelegate(this);
public SybaseDialect() {
this( MINIMUM_VERSION );
@ -350,4 +354,8 @@ public NameQualifierSupport getNameQualifierSupport() {
}
}
@Override
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
}

View File

@ -9,32 +9,31 @@
import org.hibernate.boot.Metadata;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.dialect.Dialect;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.UniqueKey;
import static org.hibernate.mapping.Index.buildSqlCreateIndexString;
/**
* DB2 does not allow unique constraints on nullable columns, but it
* does allow the creation of unique <em>indexes</em> instead, using
* a different syntax.
* A {@link UniqueDelegate} which uses {@code create unique index} commands when necessary.
* <ul>
* <li>DB2 does not allow unique constraints on nullable columns, but it does allow the creation
* of unique indexes instead, using {@code create unique index ... exclude null keys}.
* <li>SQL Server <em>does</em> allow unique constraints on nullable columns, but the semantics
* are that two null values are non-unique. So here we need to jump through hoops with the
* {@code create unique nonclustered index} command.
* </ul>
*
* @author Brett Meyer
*/
public class DB2UniqueDelegate extends AlterTableUniqueDelegate {
/**
* Constructs a DB2UniqueDelegate
*
* @param dialect The dialect
*/
public DB2UniqueDelegate( Dialect dialect ) {
public class AlterTableUniqueIndexDelegate extends AlterTableUniqueDelegate {
public AlterTableUniqueIndexDelegate(Dialect dialect ) {
super( dialect );
}
@Override
public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata,
SqlStringGenerationContext context) {
if ( hasNullable( uniqueKey ) ) {
if ( uniqueKey.hasNullableColumn() ) {
return buildSqlCreateIndexString(
context,
uniqueKey.getName(),
@ -53,7 +52,7 @@ public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata m
@Override
public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata,
SqlStringGenerationContext context) {
if ( hasNullable( uniqueKey ) ) {
if ( uniqueKey.hasNullableColumn() ) {
return org.hibernate.mapping.Index.buildSqlDropIndexString(
uniqueKey.getName(),
context.format( uniqueKey.getTable().getQualifiedTableName() )
@ -63,13 +62,4 @@ public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata
return super.getAlterTableToDropUniqueKeyCommand( uniqueKey, metadata, context );
}
}
private boolean hasNullable(UniqueKey uniqueKey) {
for ( Column column : uniqueKey.getColumns() ) {
if ( column.isNullable() ) {
return true;
}
}
return false;
}
}

View File

@ -16,6 +16,14 @@
/**
* A {@link UniqueDelegate} which includes the unique constraint in the {@code create table}
* statement, except when called during schema migration.
* <ul>
* <li>For columns marked {@linkplain jakarta.persistence.Column#unique() unique}, this results
* in a {@code unique} column definition.
* <li>For {@linkplain jakarta.persistence.UniqueConstraint#name named unique keys}, it results
* in {@code constraint abc unique(a,b,c)} after the column list in {@code create table}.
* <li>For unique keys with no explicit name, it results in {@code unique(x, y)} after the
* column list.
* </ul>
*
* @author Gavin King
*/
@ -52,17 +60,21 @@ public String getTableCreationUniqueConstraintsFragment(Table table, SqlStringGe
// signature of getColumnDefinitionUniquenessFragment() doesn't let me
// detect this case. (But that would be easy to fix!)
if ( !isSingleColumnUnique( uniqueKey ) ) {
fragment.append( ", " );
if ( uniqueKey.isNameExplicit() ) {
fragment.append( "constraint " ).append( uniqueKey.getName() ).append( " " );
}
fragment.append( uniqueConstraintSql( uniqueKey ) );
appendUniqueConstraint( fragment, uniqueKey );
}
}
return fragment.toString();
}
}
protected void appendUniqueConstraint(StringBuilder fragment, UniqueKey uniqueKey) {
fragment.append( ", " );
if ( uniqueKey.isNameExplicit() ) {
fragment.append( "constraint " ).append( uniqueKey.getName() ).append( " " );
}
fragment.append( uniqueConstraintSql(uniqueKey) );
}
private static boolean isSingleColumnUnique(UniqueKey uniqueKey) {
return uniqueKey.getColumns().size() == 1
&& uniqueKey.getColumn(0).isUnique();

View File

@ -0,0 +1,55 @@
/*
* 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.dialect.unique;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.dialect.Dialect;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.UniqueKey;
/**
* A {@link UniqueDelegate} that only creates unique constraints on not-null columns, and ignores requests for
* uniqueness for nullable columns.
* <p>
* Needed because unique constraints on nullable columns in Sybase always consider null values to be non-unique.
* There is simply no way to create a unique constraint with the semantics we want on a nullable column in Sybase >:-(
* <p>
* You might argue that this was a bad decision because if the programmer explicitly specifies an {@code @UniqueKey},
* then we should damn well respect their wishes. But the simple answer is that the user should have also specified
* {@code @Column(nullable=false)} if that is what they wanted. A unique key on a nullable column just really doesn't
* make sense in Sybase, except, perhaps, in some incredibly corner cases.
*
* @author Gavin King
*/
public class SkipNullableUniqueDelegate extends CreateTableUniqueDelegate {
public SkipNullableUniqueDelegate(Dialect dialect) {
super( dialect );
}
@Override
public String getColumnDefinitionUniquenessFragment(Column column, SqlStringGenerationContext context) {
return column.isNullable() ? "" : super.getColumnDefinitionUniquenessFragment(column, context);
}
@Override
protected void appendUniqueConstraint(StringBuilder fragment, UniqueKey uniqueKey) {
if ( !uniqueKey.hasNullableColumn() ) {
super.appendUniqueConstraint( fragment, uniqueKey );
}
}
@Override
public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) {
return uniqueKey.hasNullableColumn() ? "" : super.getAlterTableToAddUniqueKeyCommand( uniqueKey, metadata, context );
}
@Override
public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) {
return uniqueKey.hasNullableColumn() ? "" : super.getAlterTableToDropUniqueKeyCommand( uniqueKey, metadata, context );
}
}

View File

@ -32,16 +32,21 @@ public String format(String sql) {
return sql;
}
if ( sql.toLowerCase(Locale.ROOT).startsWith( "create table" ) ) {
String lowerCaseSql = sql.toLowerCase(Locale.ROOT);
if ( lowerCaseSql.startsWith( "create table" ) ) {
return formatCreateTable( sql );
}
else if ( sql.toLowerCase(Locale.ROOT).startsWith( "create" ) ) {
return sql;
}
else if ( sql.toLowerCase(Locale.ROOT).startsWith( "alter table" ) ) {
else if ( lowerCaseSql.startsWith( "create index" )
|| lowerCaseSql.startsWith("create unique") ) {
return formatAlterTable( sql );
}
else if ( sql.toLowerCase(Locale.ROOT).startsWith( "comment on" ) ) {
else if ( lowerCaseSql.startsWith( "create" ) ) {
return sql;
}
else if ( lowerCaseSql.startsWith( "alter table" ) ) {
return formatAlterTable( sql );
}
else if ( lowerCaseSql.startsWith( "comment on" ) ) {
return formatCommentOn( sql );
}
else {

View File

@ -11,13 +11,11 @@
import java.util.HashMap;
import java.util.Iterator;
import org.hibernate.HibernateException;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.relational.Exportable;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.Mapping;
import org.hibernate.internal.util.StringHelper;
import static java.util.Collections.unmodifiableList;
@ -27,6 +25,9 @@
/**
* A mapping model object representing an {@linkplain jakarta.persistence.Index index} on a relational database table.
* <p>
* We regularize the semantics of unique constraints on nullable columns: two null values are not considered to be
* "equal" for the purpose of determining uniqueness, just as specified by ANSI SQL and common sense.
*
* @author Gavin King
*/
@ -49,9 +50,8 @@ public static String buildSqlCreateIndexString(
java.util.List<Column> columns,
java.util.Map<Column, String> columnOrderMap,
boolean unique) {
StringBuilder buf = new StringBuilder( "create" )
.append( unique ? " unique" : "" )
.append( " index " )
StringBuilder statement = new StringBuilder( dialect.getCreateIndexString( unique ) )
.append( " " )
.append( dialect.qualifyIndexName() ? name : StringHelper.unqualify( name ) )
.append( " on " )
.append( tableName )
@ -62,15 +62,17 @@ public static String buildSqlCreateIndexString(
first = false;
}
else {
buf.append(", ");
statement.append(", ");
}
buf.append( column.getQuotedName( dialect ) );
statement.append( column.getQuotedName( dialect ) );
if ( columnOrderMap.containsKey( column ) ) {
buf.append( " " ).append( columnOrderMap.get( column ) );
statement.append( " " ).append( columnOrderMap.get( column ) );
}
}
buf.append( ")" );
return buf.toString();
statement.append( ")" );
statement.append( dialect.getCreateIndexTail( unique, columns ) );
return statement.toString();
}
public static String buildSqlCreateIndexString(

View File

@ -19,6 +19,7 @@
import java.util.function.Function;
import org.hibernate.HibernateException;
import org.hibernate.Internal;
import org.hibernate.MappingException;
import org.hibernate.Remove;
import org.hibernate.boot.Metadata;
@ -28,6 +29,7 @@
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.QualifiedTableName;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.dialect.Dialect;
import org.hibernate.tool.schema.extract.spi.ColumnInformation;
import org.hibernate.tool.schema.extract.spi.TableInformation;
@ -246,6 +248,14 @@ public Column getColumn(Identifier name) {
return columns.get( name.getCanonicalName() );
}
@Internal
public Column getColumn(InFlightMetadataCollector collector, String logicalName) {
if ( name == null ) {
return null;
}
return getColumn( new Column( collector.getPhysicalColumnName( this, logicalName ) ) );
}
public Column getColumn(int n) {
final Iterator<Column> iter = columns.values().iterator();
for ( int i = 0; i < n - 1; i++ ) {

View File

@ -60,4 +60,13 @@ public boolean isNameExplicit() {
public void setNameExplicit(boolean nameExplicit) {
this.nameExplicit = nameExplicit;
}
public boolean hasNullableColumn() {
for ( Column column : getColumns() ) {
if ( column.isNullable() ) {
return true;
}
}
return false;
}
}

View File

@ -11,6 +11,8 @@
import org.hibernate.JDBCException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.dialect.SybaseDialect;
import org.hibernate.testing.SkipForDialect;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
@ -21,6 +23,8 @@
* @author <a href="mailto:bernhardt.manuel@gmail.com">Manuel Bernhardt</a>
* @author Brett Meyer
*/
@SkipForDialect(value = SybaseDialect.class,
comment = "Sybase does not properly support unique constraints on nullable columns")
public class UniqueConstraintTest extends BaseCoreFunctionalTestCase {
protected Class[] getAnnotatedClasses() {
@ -32,7 +36,7 @@ protected Class[] getAnnotatedClasses() {
}
@Test
public void testUniquenessConstraintWithSuperclassProperty() throws Exception {
public void testUniquenessConstraintWithSuperclassProperty() {
Session s = openSession();
Transaction tx = s.beginTransaction();
Room livingRoom = new Room();
@ -55,7 +59,7 @@ public void testUniquenessConstraintWithSuperclassProperty() throws Exception {
s.persist(house2);
try {
s.flush();
fail( "Database constraint non-existant" );
fail( "Database constraint non-existent" );
}
catch (PersistenceException e) {
assertTyping( JDBCException.class, e.getCause() );

View File

@ -15,8 +15,6 @@
import jakarta.persistence.PersistenceException;
import jakarta.persistence.Table;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.testing.TestForIssue;
@ -75,7 +73,7 @@ public static class Customer {
@Column(name = "CUSTOMER_ACCOUNT_NUMBER")
public Long customerAccountNumber;
@Basic
@Basic(optional = false)
@Column(name = "CUSTOMER_ID", unique = true)
public String customerId;

View File

@ -149,7 +149,7 @@ public static class ParentData {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "parentId", nullable = false)
@OrderColumn(name = "listOrder")
@OrderColumn(name = "listOrder", nullable = false)
private List<ChildData> children = new ArrayList<>();
public List<ChildData> getChildren() {

View File

@ -54,7 +54,7 @@ public void testGeneratedSql() {
metadata,
false
);
String expectedMappingTableSql = "create table personAddress (address_id bigint unique, " +
String expectedMappingTableSql = "create table personAddress (address_id bigint, " +
"person_id bigint not null, primary key (person_id))";
assertEquals( "Wrong SQL", expectedMappingTableSql, commands.get( 2 ) );

View File

@ -25,7 +25,7 @@ public abstract class Element {
@GeneratedValue
private Long id;
@Column(unique = true)
@Column(unique = true, nullable = false)
@NaturalId
private String code;
}

View File

@ -85,12 +85,14 @@ public void testUniqueConstraintIsCorrectlyGenerated() throws Exception {
isUniqueConstraintCreated = isUniqueConstraintCreated
|| statement.startsWith("create unique index")
&& statement.contains("category (code)")
|| statement.startsWith("create unique nonclustered index")
&& statement.contains("category (code)")
|| statement.startsWith("alter table if exists category add constraint")
&& statement.contains("unique (code)")
|| statement.startsWith("alter table category add constraint")
&& statement.contains("unique (code)")
|| statement.startsWith("create table category")
&& statement.contains("code " + varchar255 + dialect.getNullColumnString() + " unique")
&& statement.contains("code " + varchar255 + " not null unique")
|| statement.startsWith("create table category")
&& statement.contains("unique(code)");
}

View File

@ -20,10 +20,10 @@
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.DB2Dialect;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.unique.AlterTableUniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.tool.schema.TargetType;
@ -104,7 +104,7 @@ public void tearDown() {
@Test
@TestForIssue(jiraKey = "HHH-11236")
public void testUniqueConstraintIsGenerated() throws Exception {
public void testUniqueConstraintIsDropped() throws Exception {
new IndividuallySchemaMigratorImpl( tool, DefaultSchemaFilter.INSTANCE )
.doMigration(
@ -114,18 +114,11 @@ public void testUniqueConstraintIsGenerated() throws Exception {
new TargetDescriptorImpl()
);
if ( getDialect().getUniqueDelegate() instanceof AlterTableUniqueDelegate ) {
if ( getDialect() instanceof MySQLDialect ) {
assertThat(
"The test_entity_item table unique constraint has not been dropped",
checkDropIndex( "test_entity_item", "item" ),
is( true )
);
if ( !(getDialect().getUniqueDelegate() instanceof SkipNullableUniqueDelegate) ) {
if ( getDialect().getUniqueDelegate() instanceof AlterTableUniqueIndexDelegate) {
checkDropIndex( "test_entity_item", "item" );
}
else if ( getDialect() instanceof DB2Dialect ) {
checkDB2DropIndex( "test_entity_item", "item" );
}
else {
else if ( getDialect().getUniqueDelegate() instanceof AlterTableUniqueDelegate ) {
assertThat(
"The test_entity_item table unique constraint has not been dropped",
checkDropConstraint( "test_entity_item", "item" ),
@ -133,6 +126,11 @@ else if ( getDialect() instanceof DB2Dialect ) {
);
}
}
assertThat(
checkDropConstraint( "test_entity_children", "child" ),
is( true )
);
}
protected Dialect getDialect() {
@ -140,43 +138,42 @@ protected Dialect getDialect() {
}
private boolean checkDropConstraint(String tableName, String columnName) throws IOException {
String regex = getDialect().getAlterTableString( tableName ) + " drop constraint";
String regex = getDialect().getAlterTableString( tableName ) + getDialect().getDropUniqueKeyString();
if ( getDialect().supportsIfExistsBeforeConstraintName() ) {
regex += " if exists";
}
regex += " uk_(.)*";
if ( getDialect().supportsIfExistsAfterConstraintName() ) {
regex += " if exists";
}
return isMatching( regex );
}
private boolean checkDropIndex(String tableName, String columnName) throws IOException {
String regex = "alter table ";
if ( getDialect().supportsIfExistsAfterAlterTable() ) {
regex += "if exists ";
}
regex += tableName;
if ( getDialect().supportsIfExistsAfterTableName() ) {
regex += " if exists";
}
regex += " drop index";
if ( getDialect().supportsIfExistsBeforeConstraintName() ) {
regex += " if exists";
}
regex += " uk.*";
regex += "uk_.*";
if ( getDialect().supportsIfExistsAfterConstraintName() ) {
regex += " if exists";
}
regex += ";";
return isMatching( regex );
}
private boolean checkDB2DropIndex(String tableName, String columnName) throws IOException {
// private boolean checkAlterTableDropIndex(String tableName, String columnName) throws IOException {
// String regex = "alter table ";
//
// if ( getDialect().supportsIfExistsAfterAlterTable() ) {
// regex += "if exists ";
// }
// regex += tableName;
// if ( getDialect().supportsIfExistsAfterTableName() ) {
// regex += " if exists";
// }
// regex += " drop index";
//
// if ( getDialect().supportsIfExistsBeforeConstraintName() ) {
// regex += " if exists";
// }
// regex += " uk.*";
// if ( getDialect().supportsIfExistsAfterConstraintName() ) {
// regex += " if exists";
// }
//
// return isMatching( regex );
// }
private boolean checkDropIndex(String tableName, String columnName) throws IOException {
String regex = "drop index " + tableName + ".uk.*";
return isMatching( regex );
}

View File

@ -18,10 +18,11 @@
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.DB2Dialect;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.unique.AlterTableUniqueDelegate;
import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate;
import org.hibernate.dialect.unique.CreateTableUniqueDelegate;
import org.hibernate.dialect.unique.SkipNullableUniqueDelegate;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.TargetType;
@ -67,26 +68,28 @@ public void testUniqueConstraintIsGenerated() throws Exception {
.setOutputFile( output.getAbsolutePath() )
.create( EnumSet.of( TargetType.SCRIPT ), metadata );
if ( getDialect() instanceof DB2Dialect) {
assertThat(
"The test_entity_item table unique constraint has not been generated",
isCreateUniqueIndexGenerated("test_entity_item", "item"),
is(true)
);
}
else {
assertThat(
"The test_entity_item table unique constraint has not been generated",
isUniqueConstraintGenerated("test_entity_item", "item"),
is(true)
);
}
if ( !(getDialect().getUniqueDelegate() instanceof SkipNullableUniqueDelegate) ) {
if ( getDialect().getUniqueDelegate() instanceof AlterTableUniqueIndexDelegate ) {
assertThat(
"The test_entity_item table unique constraint has not been generated",
isCreateUniqueIndexGenerated("test_entity_item", "item"),
is(true)
);
}
else {
assertThat(
"The test_entity_item table unique constraint has not been generated",
isUniqueConstraintGenerated("test_entity_item", "item"),
is(true)
);
}
assertThat(
"The test_entity_children table unique constraint has not been generated",
isUniqueConstraintGenerated( "test_entity_children", "child" ),
is( true )
);
assertThat(
"The test_entity_children table unique constraint has not been generated",
isUniqueConstraintGenerated( "test_entity_children", "child" ),
is( true )
);
}
}
private Dialect getDialect() {
@ -118,7 +121,8 @@ else if ( dialect.getUniqueDelegate() instanceof AlterTableUniqueDelegate) {
}
private boolean isCreateUniqueIndexGenerated(String tableName, String columnName) throws IOException {
String regex = "create unique index uk.* on " + tableName + " \\(" + columnName + "\\);";
String regex = "create unique (nonclustered )?index uk.* on " + tableName
+ " \\(" + columnName + "\\)( where .*| exclude null keys)?;";
final String fileContent = new String( Files.readAllBytes( output.toPath() ) ).toLowerCase();
final String[] split = fileContent.split( System.lineSeparator() );
Pattern p = Pattern.compile( regex );

View File

@ -108,7 +108,7 @@ private SimpleEntity newEntity(int id) {
public static class SimpleEntity {
@Id
public Integer id;
@Column(unique = true, name = "entity_key")
@Column(unique = true, nullable = false, name = "entity_key")
public String key;
public String name;