HHH-15162 Add STRING_OR_CLOB FunctionParameterType and use that for various length functions

This commit is contained in:
Christian Beikov 2022-04-04 15:06:14 +02:00
parent 9d3726e39d
commit 6f0ec52e81
15 changed files with 446 additions and 19 deletions

View File

@ -366,6 +366,7 @@ public abstract class AbstractHANADialect extends Dialect {
functionFactory.format_toVarchar();
functionFactory.currentUtcdatetimetimestamp();
functionFactory.everyAny_minMaxCase();
functionFactory.octetLength_pattern( "length(to_binary(?1))" );
functionFactory.bitLength_pattern( "length(to_binary(?1))*8" );
functionFactory.windowFunctions();

View File

@ -284,6 +284,7 @@ public class DerbyDialect extends Dialect {
functionFactory.characterLength_length( SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER );
functionFactory.power_expLn();
functionFactory.round_floor();
functionFactory.octetLength_pattern( "length(?1)" );
functionFactory.bitLength_pattern( "length(?1)*8" );
queryEngine.getSqmFunctionRegistry().register(

View File

@ -61,7 +61,6 @@ import org.hibernate.dialect.temptable.TemporaryTableKind;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.spi.SqlAppender;
@ -163,7 +162,6 @@ public class OracleDialect extends Dialect {
functionFactory.rownumRowid();
functionFactory.sysdate();
functionFactory.systimestamp();
functionFactory.characterLength_length( SqlAstNodeRenderingMode.DEFAULT );
functionFactory.addMonths();
functionFactory.monthsBetween();
functionFactory.everyAny_minMaxCase();
@ -176,7 +174,10 @@ public class OracleDialect extends Dialect {
functionFactory.covarPopSamp();
functionFactory.corr();
functionFactory.regrLinearRegressionAggregates();
functionFactory.bitLength_pattern( "vsize(?1)*8" );
functionFactory.characterLength_length( "dbms_lob.getlength(?1)" );
// Approximate octet and bit length for clobs since exact size determination would require a custom function
functionFactory.octetLength_pattern( "lengthb(?1)", "dbms_lob.getlength(?1)*2" );
functionFactory.bitLength_pattern( "lengthb(?1)*8", "dbms_lob.getlength(?1)*16" );
if ( getVersion().isBefore( 9 ) ) {
queryEngine.getSqmFunctionRegistry().register( "coalesce", new NvlCoalesceEmulation() );

View File

@ -441,7 +441,6 @@ public class PostgreSQLDialect extends Dialect {
}
functionFactory.cbrt();
functionFactory.trim2();
functionFactory.octetLength();
functionFactory.repeat();
functionFactory.md5();
functionFactory.initcap();
@ -450,11 +449,12 @@ public class PostgreSQLDialect extends Dialect {
//also natively supports ANSI-style substring()
functionFactory.translate();
functionFactory.toCharNumberDateTimestamp();
functionFactory.concat_pipeOperator();
functionFactory.concat_pipeOperator( "convert_from(lo_get(?1),pg_client_encoding())" );
functionFactory.localtimeLocaltimestamp();
functionFactory.dateTrunc();
functionFactory.bitLength();
functionFactory.octetLength();
functionFactory.length_characterLength_pattern( "length(lo_get(?1),pg_client_encoding())" );
functionFactory.bitLength_pattern( "bit_length(?1)", "length(lo_get(?1))*8" );
functionFactory.octetLength_pattern( "octet_length(?1)", "length(lo_get(?1))" );
functionFactory.ascii();
functionFactory.char_chr();
functionFactory.position();

View File

@ -241,7 +241,8 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
functionFactory.truncate_round();
functionFactory.everyAny_minMaxIif();
functionFactory.bitLength_pattern( "datalength(?1) * 8" );
functionFactory.octetLength_pattern( "datalength(?1)" );
functionFactory.bitLength_pattern( "datalength(?1)*8" );
if ( getVersion().isSameOrAfter( 10 ) ) {
functionFactory.locate_charindex();

View File

@ -223,7 +223,8 @@ public class SybaseDialect extends AbstractTransactSQLDialect {
functionFactory.replace_strReplace();
functionFactory.everyAny_minMaxCase();
functionFactory.bitLength_pattern( "datalength(?1) * 8" );
functionFactory.octetLength_pattern( "datalength(?1)" );
functionFactory.bitLength_pattern( "datalength(?1)*8" );
queryEngine.getSqmFunctionRegistry().register( "timestampadd",
new IntegralTimestampaddFunction( this, queryEngine.getTypeConfiguration() ) );

View File

@ -13,9 +13,6 @@ import org.hibernate.dialect.Dialect;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.type.BasicType;
@ -1114,6 +1111,10 @@ public class CommonFunctionFactory {
.register();
}
public void concat_pipeOperator(String clobPattern) {
functionRegistry.register( "concat", new ConcatPipeFunction( clobPattern, typeConfiguration ) );
}
/**
* Oracle-style
*/
@ -1350,11 +1351,19 @@ public class CommonFunctionFactory {
functionRegistry.namedDescriptorBuilder( "character_length" )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.register();
functionRegistry.registerAlternateKey( "length", "character_length" );
}
public void length_characterLength_pattern(String clobPattern) {
functionRegistry.register(
"character_length",
new LengthFunction( "character_length", "character_length(?1)", clobPattern, typeConfiguration )
);
functionRegistry.registerAlternateKey( "character_length", "length" );
}
/**
* Transact SQL-style
*/
@ -1362,7 +1371,7 @@ public class CommonFunctionFactory {
functionRegistry.namedDescriptorBuilder( "len" )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.register();
functionRegistry.registerAlternateKey( "character_length", "len" );
functionRegistry.registerAlternateKey( "length", "len" );
@ -1375,25 +1384,48 @@ public class CommonFunctionFactory {
functionRegistry.namedDescriptorBuilder( "length" )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.setArgumentRenderingMode( argumentRenderingMode )
.register();
functionRegistry.registerAlternateKey( "character_length", "length" );
}
public void characterLength_length(String clobPattern) {
functionRegistry.register(
"length",
new LengthFunction( "length", "length(?1)", clobPattern, typeConfiguration )
);
functionRegistry.registerAlternateKey( "character_length", "length" );
}
public void octetLength() {
functionRegistry.namedDescriptorBuilder( "octet_length" )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.register();
}
public void octetLength_pattern(String pattern) {
functionRegistry.patternDescriptorBuilder( "octet_length", pattern )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING_OR_CLOB)
.register();
}
public void octetLength_pattern(String pattern, String clobPattern) {
functionRegistry.register(
"octet_length",
new LengthFunction( "octet_length", pattern, clobPattern, typeConfiguration )
);
}
public void bitLength() {
functionRegistry.namedDescriptorBuilder( "bit_length" )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.register();
}
@ -1401,10 +1433,17 @@ public class CommonFunctionFactory {
functionRegistry.patternDescriptorBuilder( "bit_length", pattern )
.setInvariantType(integerType)
.setExactArgumentCount( 1 )
.setParameterTypes(STRING)
.setParameterTypes(STRING_OR_CLOB)
.register();
}
public void bitLength_pattern(String pattern, String clobPattern) {
functionRegistry.register(
"bit_length",
new LengthFunction( "bit_length", pattern, clobPattern, typeConfiguration )
);
}
/**
* ANSI-style
*/

View File

@ -0,0 +1,75 @@
/*
* 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.function;
import java.util.Collections;
import java.util.List;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.produce.function.internal.PatternRenderer;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
/**
* A concat function with a pattern for clob arguments.
*/
public class ConcatPipeFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
private final PatternRenderer clobPatternRenderer;
public ConcatPipeFunction(String clobPattern, TypeConfiguration typeConfiguration) {
super(
"concat",
StandardArgumentsValidators.min( 1 ),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
),
StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, STRING )
);
this.clobPatternRenderer = new PatternRenderer( clobPattern );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
String separator = "(";
for ( int i = 0; i < sqlAstArguments.size(); i++ ) {
final Expression expression = (Expression) sqlAstArguments.get( i );
final JdbcType jdbcType = expression.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType();
sqlAppender.appendSql( separator );
switch ( jdbcType.getJdbcTypeCode() ) {
case SqlTypes.CLOB:
case SqlTypes.NCLOB:
clobPatternRenderer.render( sqlAppender, Collections.singletonList( expression ), walker );
break;
default:
expression.accept( walker );
break;
}
separator = "||";
}
sqlAppender.appendSql( ')' );
}
@Override
public String getSignature(String name) {
return "(STRING string0[, STRING string1[, ...]])";
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.function;
import java.util.List;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.produce.function.internal.PatternRenderer;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
/**
* A length function with separate patterns for string and clob argument.
*/
public class LengthFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
private final PatternRenderer stringPatternRenderer;
private final PatternRenderer clobPatternRenderer;
public LengthFunction(String functionName, String stringPattern, String clobPattern, TypeConfiguration typeConfiguration) {
super(
functionName,
new ArgumentTypesValidator(
StandardArgumentsValidators.exactly( 1 ),
FunctionParameterType.STRING_OR_CLOB
),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER )
),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING )
);
this.stringPatternRenderer = new PatternRenderer( stringPattern );
this.clobPatternRenderer = new PatternRenderer( clobPattern );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final Expression expression = (Expression) sqlAstArguments.get( 0 );
final JdbcType jdbcType = expression.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType();
switch ( jdbcType.getJdbcTypeCode() ) {
case SqlTypes.CLOB:
case SqlTypes.NCLOB:
clobPatternRenderer.render( sqlAppender, sqlAstArguments, walker );
break;
default:
stringPatternRenderer.render( sqlAppender, sqlAstArguments, walker );
break;
}
}
}

View File

@ -163,6 +163,11 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
throwError(type, javaType, functionName, count);
}
break;
case STRING_OR_CLOB:
if ( !isCharacterOrClobType(code) ) {
throwError(type, javaType, functionName, count);
}
break;
case NUMERIC:
if ( !isNumericType(code) ) {
throwError(type, javaType, functionName, count);

View File

@ -78,5 +78,9 @@ public enum FunctionParameterType {
/**
* Any type with an order (numeric, string, and temporal types)
*/
COMPARABLE
COMPARABLE,
/**
* @see org.hibernate.type.SqlTypes#isCharacterOrClobType(int)
*/
STRING_OR_CLOB
}

View File

@ -142,6 +142,7 @@ public final class StandardFunctionArgumentTypeResolvers {
FunctionParameterType type) {
switch ( type ) {
case STRING:
case STRING_OR_CLOB:
return typeConfiguration.getBasicTypeForJavaType( String.class );
case NUMERIC:
return typeConfiguration.getBasicTypeForJavaType( BigDecimal.class );

View File

@ -483,6 +483,27 @@ public class SqlTypes {
}
}
/**
* Does the given JDBC type code represent some sort of
* character string type?
* @param sqlType a JDBC type code from {@link Types}
*/
public static boolean isCharacterOrClobType(int sqlType) {
switch (sqlType) {
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
case Types.NCHAR:
case Types.NVARCHAR:
case Types.LONGNVARCHAR:
case Types.CLOB:
case Types.NCLOB:
return true;
default:
return false;
}
}
/**
* Does the given JDBC type code represent some sort of
* character string type?

View File

@ -0,0 +1,206 @@
/*
* 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.lob;
import java.sql.Clob;
import java.util.List;
import org.hibernate.query.Query;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.Lob;
import jakarta.persistence.Table;
import jakarta.persistence.Tuple;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.isOneOf;
import static org.hamcrest.core.Is.is;
@TestForIssue(jiraKey = "HHH-15162")
@DomainModel(
annotatedClasses = LobStringFunctionsTest.TestEntity.class
)
@SessionFactory
public class LobStringFunctionsTest {
private static final int LONG_STRING_SIZE = 3999;
private final String value1 = buildRecursively( LONG_STRING_SIZE, 'x' );
private final String value2 = buildRecursively( LONG_STRING_SIZE, 'y' );
@BeforeEach
protected void prepareTest(SessionFactoryScope scope) throws Exception {
TestEntity entity = new TestEntity();
scope.inTransaction( session -> {
entity.setFirstLobField( value1 );
entity.setSecondLobField( value2 );
entity.setClobField( session.getLobHelper().createClob( value2 ) );
session.save( entity );
} );
scope.inTransaction( session -> {
final TestEntity testEntity = session.find( TestEntity.class, entity.getId() );
assertThat( testEntity.getFirstLobField(), is( value1 ) );
} );
}
@AfterEach
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction(
session ->
session.createQuery( "delete from TestEntity" ).executeUpdate()
);
}
@Test
public void testLengthFunction(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Query<Tuple> query = session.createQuery(
"select length(e.firstLobField),length(e.secondLobField),length(e.clobField) from TestEntity e",
Tuple.class
);
final List<Tuple> results = query.getResultList();
assertThat( results.size(), is( 1 ) );
final Tuple testEntity = results.get( 0 );
assertThat( testEntity.get( 0, Integer.class ), is( value1.length() ) );
assertThat( testEntity.get( 1, Integer.class ), is( value2.length() ) );
assertThat( testEntity.get( 2, Integer.class ), is( value2.length() ) );
} );
}
@Test
public void testOctetLengthFunction(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Query<Tuple> query = session.createQuery(
"select octet_length(e.firstLobField),octet_length(e.secondLobField),octet_length(e.clobField) from TestEntity e",
Tuple.class
);
final List<Tuple> results = query.getResultList();
assertThat( results.size(), is( 1 ) );
final Tuple testEntity = results.get( 0 );
// Some databases report 2 octets per character
assertThat( testEntity.get( 0, Integer.class ), isOneOf( value1.length(), value1.length() * 2 ) );
assertThat( testEntity.get( 1, Integer.class ), isOneOf( value2.length(), value2.length() * 2 ) );
assertThat( testEntity.get( 2, Integer.class ), isOneOf( value2.length(), value2.length() * 2 ) );
} );
}
@Test
public void testBitLengthFunction(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Query<Tuple> query = session.createQuery(
"select bit_length(e.firstLobField),bit_length(e.secondLobField),bit_length(e.clobField) from TestEntity e",
Tuple.class
);
final List<Tuple> results = query.getResultList();
assertThat( results.size(), is( 1 ) );
final Tuple testEntity = results.get( 0 );
// Some databases report 16 bit per character
assertThat( testEntity.get( 0, Integer.class ), isOneOf( value1.length() * 8, value1.length() * 16 ) );
assertThat( testEntity.get( 1, Integer.class ), isOneOf( value2.length() * 8, value2.length() * 16 ) );
assertThat( testEntity.get( 2, Integer.class ), isOneOf( value2.length() * 8, value2.length() * 16 ) );
} );
}
@Test
public void testConcatFunction(SessionFactoryScope scope) {
scope.inTransaction( session -> {
// Use trim('') instead of '' since Sybase interprets that as single space string...
final Query<Tuple> query = session.createQuery(
"select concat(e.firstLobField, trim('')),concat(e.secondLobField, trim('')),concat(e.clobField, trim('')) from TestEntity e",
Tuple.class
);
final List<Tuple> results = query.getResultList();
assertThat( results.size(), is( 1 ) );
final Tuple testEntity = results.get( 0 );
assertThat( testEntity.get( 0, String.class ), is( value1 ) );
assertThat( testEntity.get( 1, String.class ), is( value2 ) );
assertThat( testEntity.get( 2, String.class ), is( value2 ) );
} );
}
@Entity(name = "TestEntity")
@Table(name = "TEST_ENTITY")
public static class TestEntity {
@Id
@GeneratedValue
private long id;
@Lob
@Column(length = LONG_STRING_SIZE) //needed by HSQLDialect
String firstLobField;
@Lob
@Column(length = LONG_STRING_SIZE) //needed by HSQLDialect
String secondLobField;
@Lob
@Column(length = LONG_STRING_SIZE) //needed by HSQLDialect
Clob clobField;
public long getId() {
return id;
}
public String getFirstLobField() {
return firstLobField;
}
public void setFirstLobField(String firstLobField) {
this.firstLobField = firstLobField;
}
public String getSecondLobField() {
return secondLobField;
}
public void setSecondLobField(String secondLobField) {
this.secondLobField = secondLobField;
}
public Clob getClobField() {
return clobField;
}
public void setClobField(Clob clobField) {
this.clobField = clobField;
}
}
private String buildRecursively(int size, char baseChar) {
StringBuilder buff = new StringBuilder();
for ( int i = 0; i < size; i++ ) {
buff.append( baseChar );
}
return buff.toString();
}
}

View File

@ -31,6 +31,7 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import jakarta.persistence.Tuple;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;