HHH-16989 fix 'cast(string as String(10))', 'cast(string as Binary(10))' on MySQL

This commit is contained in:
Gavin King 2023-08-18 20:39:02 +02:00
parent 8461ba2078
commit 72e092f67f
5 changed files with 164 additions and 109 deletions

View File

@ -299,12 +299,9 @@ public class MySQLLegacyDialect extends Dialect {
final int maxLobLen = 65_535;
final int maxMediumLobLen = 16_777_215;
final CapacityDependentDdlType.Builder varcharBuilder = CapacityDependentDdlType.builder(
VARCHAR,
columnType( CLOB ),
"char",
this
)
final CapacityDependentDdlType.Builder varcharBuilder =
CapacityDependentDdlType.builder( VARCHAR,
columnType( CLOB ), columnType( CHAR ), castType( CHAR ), this )
.withTypeCapacity( getMaxVarcharLength(), "varchar($l)" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" );
if ( getMaxVarcharLength() < maxLobLen ) {
@ -312,12 +309,9 @@ public class MySQLLegacyDialect extends Dialect {
}
ddlTypeRegistry.addDescriptor( varcharBuilder.build() );
final CapacityDependentDdlType.Builder nvarcharBuilder = CapacityDependentDdlType.builder(
NVARCHAR,
columnType( NCLOB ),
"char",
this
)
final CapacityDependentDdlType.Builder nvarcharBuilder =
CapacityDependentDdlType.builder( NVARCHAR,
columnType( NCLOB ), columnType( NCHAR ), castType( NCHAR ), this )
.withTypeCapacity( getMaxVarcharLength(), "varchar($l)" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" );
if ( getMaxVarcharLength() < maxLobLen ) {
@ -325,12 +319,9 @@ public class MySQLLegacyDialect extends Dialect {
}
ddlTypeRegistry.addDescriptor( nvarcharBuilder.build() );
final CapacityDependentDdlType.Builder varbinaryBuilder = CapacityDependentDdlType.builder(
VARBINARY,
columnType( BLOB ),
"binary",
this
)
final CapacityDependentDdlType.Builder varbinaryBuilder =
CapacityDependentDdlType.builder( VARBINARY,
columnType( BLOB ), columnType( BINARY ), castType( BINARY ), this )
.withTypeCapacity( getMaxVarbinaryLength(), "varbinary($l)" )
.withTypeCapacity( maxMediumLobLen, "mediumblob" );
if ( getMaxVarbinaryLength() < maxLobLen ) {
@ -338,12 +329,15 @@ public class MySQLLegacyDialect extends Dialect {
}
ddlTypeRegistry.addDescriptor( varbinaryBuilder.build() );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARBINARY, columnType( BLOB ), "binary", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARCHAR, columnType( CLOB ), "char", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32NVARCHAR, columnType( CLOB ), "char", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARBINARY,
columnType( BLOB ), castType( BINARY ), this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARCHAR,
columnType( CLOB ), castType( CHAR ), this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32NVARCHAR,
columnType( CLOB ), castType( NCHAR ), this ) );
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( BLOB, columnType( BLOB ), "binary", this )
CapacityDependentDdlType.builder( BLOB, columnType( BLOB ), castType( BINARY ), this )
.withTypeCapacity( maxTinyLobLen, "tinyblob" )
.withTypeCapacity( maxMediumLobLen, "mediumblob" )
.withTypeCapacity( maxLobLen, "blob" )
@ -351,7 +345,7 @@ public class MySQLLegacyDialect extends Dialect {
);
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( CLOB, columnType( CLOB ), "char", this )
CapacityDependentDdlType.builder( CLOB, columnType( CLOB ), castType( CHAR ), this )
.withTypeCapacity( maxTinyLobLen, "tinytext" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" )
.withTypeCapacity( maxLobLen, "text" )
@ -359,7 +353,7 @@ public class MySQLLegacyDialect extends Dialect {
);
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( NCLOB, columnType( NCLOB ), "char", this )
CapacityDependentDdlType.builder( NCLOB, columnType( NCLOB ), castType( NCHAR ), this )
.withTypeCapacity( maxTinyLobLen, "tinytext" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" )
.withTypeCapacity( maxLobLen, "text" )

View File

@ -297,13 +297,14 @@ public class MySQLDialect extends Dialect {
//the default scale is 0 (no decimal places)
return "decimal($p,$s)";
case CHAR:
case NCHAR:
case VARCHAR:
case NVARCHAR:
case LONG32VARCHAR:
case LONG32NVARCHAR:
//MySQL doesn't let you cast to TEXT/LONGTEXT
return "char";
case NCHAR:
case NVARCHAR:
case LONG32NVARCHAR:
return "char character set utf8";
case BINARY:
case VARBINARY:
case LONG32VARBINARY:
@ -332,12 +333,9 @@ public class MySQLDialect extends Dialect {
final int maxLobLen = 65_535;
final int maxMediumLobLen = 16_777_215;
final CapacityDependentDdlType.Builder varcharBuilder = CapacityDependentDdlType.builder(
VARCHAR,
columnType( CLOB ),
"char",
this
)
final CapacityDependentDdlType.Builder varcharBuilder =
CapacityDependentDdlType.builder( VARCHAR,
columnType( CLOB ), columnType( CHAR ), castType( CHAR ), this )
.withTypeCapacity( getMaxVarcharLength(), "varchar($l)" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" );
if ( getMaxVarcharLength() < maxLobLen ) {
@ -347,12 +345,9 @@ public class MySQLDialect extends Dialect {
// do not use nchar/nvarchar/ntext because these
// types use a deprecated character set on MySQL 8
final CapacityDependentDdlType.Builder nvarcharBuilder = CapacityDependentDdlType.builder(
NVARCHAR,
columnType( NCLOB ),
"char character set utf8",
this
)
final CapacityDependentDdlType.Builder nvarcharBuilder =
CapacityDependentDdlType.builder( NVARCHAR,
columnType( NCLOB ), columnType( NCHAR ), castType( NCHAR ), this )
.withTypeCapacity( getMaxVarcharLength(), "varchar($l) character set utf8" )
.withTypeCapacity( maxMediumLobLen, "mediumtext character set utf8" );
if ( getMaxVarcharLength() < maxLobLen ) {
@ -360,12 +355,9 @@ public class MySQLDialect extends Dialect {
}
ddlTypeRegistry.addDescriptor( nvarcharBuilder.build() );
final CapacityDependentDdlType.Builder varbinaryBuilder = CapacityDependentDdlType.builder(
VARBINARY,
columnType( BLOB ),
"binary",
this
)
final CapacityDependentDdlType.Builder varbinaryBuilder =
CapacityDependentDdlType.builder( VARBINARY,
columnType( BLOB ), columnType( BINARY ), castType( BINARY ), this )
.withTypeCapacity( getMaxVarbinaryLength(), "varbinary($l)" )
.withTypeCapacity( maxMediumLobLen, "mediumblob" );
if ( getMaxVarbinaryLength() < maxLobLen ) {
@ -373,12 +365,16 @@ public class MySQLDialect extends Dialect {
}
ddlTypeRegistry.addDescriptor( varbinaryBuilder.build() );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARBINARY, columnType( BLOB ), "binary", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARCHAR, columnType( CLOB ), "char", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32NVARCHAR, columnType( CLOB ), "char", this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARBINARY,
columnType( BLOB ), castType( BINARY ), this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARCHAR,
columnType( CLOB ), castType( CHAR ), this ) );
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32NVARCHAR,
columnType( CLOB ), castType( CHAR ), this ) );
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( BLOB, columnType( BLOB ), "binary", this )
CapacityDependentDdlType.builder( BLOB,
columnType( BLOB ), castType( BINARY ), this )
.withTypeCapacity( maxTinyLobLen, "tinyblob" )
.withTypeCapacity( maxMediumLobLen, "mediumblob" )
.withTypeCapacity( maxLobLen, "blob" )
@ -386,7 +382,8 @@ public class MySQLDialect extends Dialect {
);
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( CLOB, columnType( CLOB ), "char", this )
CapacityDependentDdlType.builder( CLOB,
columnType( CLOB ), castType( CHAR ), this )
.withTypeCapacity( maxTinyLobLen, "tinytext" )
.withTypeCapacity( maxMediumLobLen, "mediumtext" )
.withTypeCapacity( maxLobLen, "text" )
@ -394,7 +391,8 @@ public class MySQLDialect extends Dialect {
);
ddlTypeRegistry.addDescriptor(
CapacityDependentDdlType.builder( NCLOB, columnType( NCLOB ), "char character set utf8", this )
CapacityDependentDdlType.builder( NCLOB,
columnType( NCLOB ), castType( NCHAR ), this )
.withTypeCapacity( maxTinyLobLen, "tinytext character set utf8" )
.withTypeCapacity( maxMediumLobLen, "mediumtext character set utf8" )
.withTypeCapacity( maxLobLen, "text character set utf8" )

View File

@ -26,12 +26,14 @@ public class CapacityDependentDdlType extends DdlTypeImpl {
builder.sqlTypeCode,
builder.typeNamePattern,
builder.castTypeNamePattern,
builder.castTypeName,
builder.dialect
);
builder.typeEntries.sort( Comparator.naturalOrder() );
this.typeEntries = builder.typeEntries.toArray(new TypeEntry[0]);
}
@Override @Deprecated
public String[] getRawTypeNames() {
final String[] rawTypeNames = new String[typeEntries.length + 1];
for ( int i = 0; i < typeEntries.length; i++ ) {
@ -78,15 +80,25 @@ public class CapacityDependentDdlType extends DdlTypeImpl {
public static Builder builder(
int sqlTypeCode,
String typeNamePattern,
String castTypeNamePattern,
String castTypeName,
Dialect dialect) {
return new Builder( sqlTypeCode, typeNamePattern, castTypeNamePattern, dialect );
return new Builder( sqlTypeCode, typeNamePattern, null, castTypeName, dialect );
}
public static Builder builder(
int sqlTypeCode,
String typeNamePattern,
String castTypeNamePattern,
String castTypeName,
Dialect dialect) {
return new Builder( sqlTypeCode, typeNamePattern, castTypeNamePattern, castTypeName, dialect );
}
public static class Builder {
private final int sqlTypeCode;
private final String typeNamePattern;
private final String castTypeNamePattern;
private final String castTypeName;
private final Dialect dialect;
private final List<TypeEntry> typeEntries;
@ -94,10 +106,12 @@ public class CapacityDependentDdlType extends DdlTypeImpl {
int sqlTypeCode,
String typeNamePattern,
String castTypeNamePattern,
String castTypeName,
Dialect dialect) {
this.sqlTypeCode = sqlTypeCode;
this.typeNamePattern = typeNamePattern;
this.castTypeNamePattern = castTypeNamePattern;
this.castTypeName = castTypeName;
this.dialect = dialect;
this.typeEntries = new ArrayList<>();
}

View File

@ -10,6 +10,7 @@ import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.java.CharacterJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.sql.DdlType;
@ -24,24 +25,36 @@ public class DdlTypeImpl implements DdlType {
private final int sqlTypeCode;
private final String typeNamePattern;
private final String castTypeNamePattern;
private final String castTypeName;
private final boolean castTypeNameIsStatic;
final Dialect dialect;
public DdlTypeImpl(int sqlTypeCode, String typeNamePattern, Dialect dialect) {
this( sqlTypeCode, typeNamePattern, typeNamePattern, dialect );
this( sqlTypeCode, typeNamePattern, typeNamePattern, typeNamePattern, dialect );
}
public DdlTypeImpl(
int sqlTypeCode,
String typeNamePattern,
String castTypeNamePattern,
String castTypeName,
Dialect dialect) {
this( sqlTypeCode, typeNamePattern, null, castTypeName, dialect );
}
public DdlTypeImpl(
int sqlTypeCode,
String typeNamePattern,
String castTypeNamePattern, //optional, usually null
String castTypeName,
Dialect dialect) {
this.sqlTypeCode = sqlTypeCode;
this.typeNamePattern = typeNamePattern;
this.castTypeNamePattern = castTypeNamePattern;
this.castTypeNameIsStatic = !castTypeNamePattern.contains( "$s" )
&& !castTypeNamePattern.contains( "$l" )
&& !castTypeNamePattern.contains( "$p" );
this.castTypeName = castTypeName;
this.castTypeNameIsStatic =
!castTypeName.contains( "$s" )
&& !castTypeName.contains( "$p" )
&& !castTypeName.contains( "$l" );
this.dialect = dialect;
}
@ -58,7 +71,7 @@ public class DdlTypeImpl implements DdlType {
final int parenEnd = typeNamePattern.lastIndexOf( ')' );
return parenEnd + 1 == typeNamePattern.length()
? typeNamePattern.substring( 0, paren )
: ( typeNamePattern.substring( 0, paren ) + typeNamePattern.substring( parenEnd + 1 ) );
: typeNamePattern.substring( 0, paren ) + typeNamePattern.substring( parenEnd + 1 );
}
return typeNamePattern;
}
@ -79,38 +92,40 @@ public class DdlTypeImpl implements DdlType {
//needed for cast(x as BigInteger(p))
scale = javaType.getDefaultSqlScale( dialect, jdbcType );
}
return castTypeNamePattern == null
? getTypeName( length, precision, scale )
: replace( castTypeNamePattern, length, precision, scale );
}
return getTypeName( length, precision, scale );
}
@Override
public String getCastTypeName(JdbcType jdbcType, JavaType<?> javaType) {
if ( castTypeNameIsStatic ) {
return castTypeNamePattern;
if ( javaType instanceof CharacterJavaType && jdbcType.isString() ) {
// nasty special case for casting to Character
return getCastTypeName( jdbcType, javaType, 1L, null, null );
}
Long length = null;
Integer precision = null;
Integer scale = null;
else if ( castTypeNameIsStatic ) {
return castTypeName;
}
else {
final Size size = dialect.getSizeStrategy()
.resolveSize( jdbcType, javaType, null, null, defaultLength( jdbcType ) );
return replace( castTypeName, size.getLength(), size.getPrecision(), size.getScale() );
}
}
//TODO: move this to JdbcType??
private Long defaultLength(JdbcType jdbcType) {
switch ( jdbcType.getDdlTypeCode() ) {
case SqlTypes.VARCHAR:
length = (long) dialect.getMaxVarcharLength();
break;
return (long) dialect.getMaxVarcharLength();
case SqlTypes.NVARCHAR:
length = (long) dialect.getMaxNVarcharLength();
break;
return (long) dialect.getMaxNVarcharLength();
case SqlTypes.VARBINARY:
length = (long) dialect.getMaxVarbinaryLength();
break;
return (long) dialect.getMaxVarbinaryLength();
default:
return null;
}
final Size size = dialect.getSizeStrategy().resolveSize(
jdbcType,
javaType,
precision,
scale,
length
);
return replace( castTypeNamePattern, size.getLength(), size.getPrecision(), size.getScale() );
}
/**

View File

@ -14,6 +14,7 @@ import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.MariaDBDialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.SybaseDialect;
import org.hibernate.dialect.TiDBDialect;
import org.hibernate.testing.TestForIssue;
@ -57,6 +58,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.isOneOf;
import static org.hibernate.testing.orm.domain.gambit.EntityOfBasics.Gender.FEMALE;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -997,21 +999,6 @@ public class FunctionTests {
);
}
@Test
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTruncateThroughCast.class)
@SkipForDialect(dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "We need to fix this, see HHH-16989")
public void testCastFunction_withTruncation(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select cast(e.theString as String(15)), cast(e.theDouble as String(8)) from EntityOfBasics e", Object[].class)
.list();
session.createQuery("select cast('ABCDEF' as Character) from EntityOfBasics", Character.class)
.list();
}
);
}
@Test
@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support casting to the binary types")
public void testCastFunctionBinary(SessionFactoryScope scope) {
@ -1024,15 +1011,62 @@ public class FunctionTests {
}
@Test
@SkipForDialect(dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "We need to fix this, see HHH-16989")
@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support casting to the binary types")
public void testCastFunctionWithLength(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select cast(e.theString as String(15)), cast(e.theDouble as String(17)) from EntityOfBasics e", Object[].class)
.list();
assertEquals( 'A',
session.createQuery("select cast('ABCDEF' as Character)", Character.class)
.getSingleResult() );
assertEquals( "ABC",
session.createQuery("select cast('ABCDEF' as String(3))", String.class)
.getSingleResult() );
}
);
}
@Test
@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support casting to binary types")
@SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "PostgreSQL bytea doesn't have a length")
@SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle cast to raw does not do truncatation")
public void testCastBinaryWithLength(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select cast(e.theString as Binary(10)) from EntityOfBasics e", byte[].class)
.list();
assertArrayEquals( new byte[]{(byte)0xFF,(byte)0},
session.createQuery("select cast(X'FF00EE11' as Binary(2))", byte[].class)
.getSingleResult() );
}
);
}
@Test
@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support casting varchar to binary")
@SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "PostgreSQL bytea doesn't have a length")
public void testCastBinaryWithLengthForOracle(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select cast(e.theString as Binary(10)) from EntityOfBasics e", byte[].class)
.list();
assertArrayEquals( new byte[]{(byte)0xFF,(byte)0},
session.createQuery("select cast(X'FF00' as Binary(2))", byte[].class)
.getSingleResult() );
}
);
}
@Test
@SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "PostgreSQL bytea doesn't have a length")
public void testCastBinaryWithLengthForDerby(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select cast(X'22FF00EE11' as Binary(10))", byte[].class)
.list();
assertArrayEquals( new byte[]{(byte)0xFF,(byte)0},
session.createQuery("select cast(X'FF00' as Binary(2))", byte[].class)
.getSingleResult() );
}
);
}