HHH-16185 Implement portable date_trunc function emulation and tests

This commit is contained in:
Marco Belladelli 2023-02-14 22:11:45 +01:00 committed by Christian Beikov
parent d52a44c8eb
commit 789c131c2d
14 changed files with 367 additions and 14 deletions

View File

@ -201,6 +201,7 @@ public class HSQLLegacyDialect extends Dialect {
functionFactory.rand();
functionFactory.trunc();
// functionFactory.truncate();
functionFactory.dateTrunc_trunc();
functionFactory.pi();
functionFactory.soundex();
functionFactory.reverse();

View File

@ -562,6 +562,7 @@ public class MySQLLegacyDialect extends Dialect {
functionFactory.position();
functionFactory.nowCurdateCurtime();
functionFactory.trunc_truncate();
functionFactory.dateTrunc_format( "str_to_date", false );
functionFactory.insert();
functionFactory.bitandorxornot_operator();
functionFactory.bitAndOr();

View File

@ -366,6 +366,10 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
}
if ( getVersion().isSameOrAfter( 16 ) ) {
functionFactory.leastGreatest();
functionFactory.dateTrunc_datetrunc();
}
else {
functionFactory.dateTrunc_format( "convert", false );
}
}

View File

@ -218,6 +218,7 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect {
functionFactory.varPopSamp();
functionFactory.trunc_floorPower();
functionFactory.round_round();
functionFactory.dateTrunc_format( "convert", true );
// For SQL-Server we need to cast certain arguments to varchar(16384) to be able to concat them
functionContributions.getFunctionRegistry().register(

View File

@ -398,6 +398,7 @@ public abstract class AbstractHANADialect extends Dialect {
functionFactory.sinh();
functionFactory.tanh();
functionFactory.trunc_roundMode();
functionFactory.dateTrunc_format( "to_date", false );
functionFactory.log10_log();
functionFactory.log();
functionFactory.bitand();

View File

@ -142,6 +142,7 @@ public class HSQLDialect extends Dialect {
functionFactory.rand();
functionFactory.trunc();
// functionFactory.truncate();
functionFactory.dateTrunc_trunc();
functionFactory.pi();
functionFactory.soundex();
functionFactory.reverse();

View File

@ -561,6 +561,7 @@ public class MySQLDialect extends Dialect {
functionFactory.position();
functionFactory.nowCurdateCurtime();
functionFactory.trunc_truncate();
functionFactory.dateTrunc_format( "str_to_date", false );
functionFactory.insert();
functionFactory.bitandorxornot_operator();
functionFactory.bitAndOr();

View File

@ -370,6 +370,10 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
}
if ( getVersion().isSameOrAfter( 16 ) ) {
functionFactory.leastGreatest();
functionFactory.dateTrunc_datetrunc();
}
else {
functionFactory.dateTrunc_format( "convert", false );
}
}

View File

@ -38,6 +38,7 @@ import org.hibernate.persister.entity.Lockable;
import org.hibernate.query.SemanticException;
import org.hibernate.query.sqm.IntervalType;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.spi.SqlAppender;
@ -393,7 +394,7 @@ public class SpannerDialect extends Dialect {
.setExactArgumentCount( 3 )
.register();
functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_trunc" )
.setInvariantType( dateType )
.setReturnTypeResolver( StandardFunctionReturnTypeResolvers.useArgType( 2 ) )
.setExactArgumentCount( 2 )
.register();
functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_from_unix_date" )

View File

@ -222,6 +222,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect {
functionFactory.varPopSamp();
functionFactory.trunc_floorPower();
functionFactory.round_round();
functionFactory.dateTrunc_format( "convert", true );
// For SQL-Server we need to cast certain arguments to varchar(16384) to be able to concat them
functionContributions.getFunctionRegistry().register(

View File

@ -2560,20 +2560,30 @@ public class CommonFunctionFactory {
public void dateTrunc() {
functionRegistry.patternDescriptorBuilder( "date_trunc", "date_trunc('?1',?2)" )
.setInvariantType(timestampType)
.setReturnTypeResolver( useArgType( 2 ) )
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL_UNIT, TEMPORAL)
.setParameterTypes( TEMPORAL_UNIT, TEMPORAL )
.setArgumentListSignature( "(TEMPORAL_UNIT field, TEMPORAL datetime)" )
.register();
}
public void dateTrunc_datetrunc() {
functionRegistry.patternDescriptorBuilder( "date_trunc", "datetrunc(?1,?2)" )
.setReturnTypeResolver( useArgType( 2 ) )
.setExactArgumentCount( 2 )
.setParameterTypes( TEMPORAL_UNIT, TEMPORAL )
.setArgumentListSignature( "(TEMPORAL_UNIT field, TEMPORAL datetime)" )
.register();
}
public void dateTrunc_trunc() {
functionRegistry.patternDescriptorBuilder( "date_trunc", "trunc(?2,'?1')" )
.setInvariantType(timestampType)
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL_UNIT, TEMPORAL)
.setArgumentListSignature( "(TEMPORAL_UNIT field, TEMPORAL datetime)" )
.register();
functionRegistry.register( "date_trunc", new DateTruncTrunc( typeConfiguration ) );
}
public void dateTrunc_format(String toDateFunction, boolean useConvertToFormat) {
functionRegistry.register(
"date_trunc",
new DateTruncEmulation( toDateFunction, useConvertToFormat, typeConfiguration )
);
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.ArrayList;
import java.util.List;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionRenderingSupport;
import org.hibernate.query.sqm.function.SelfRenderingSqmFunction;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
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.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmDurationUnit;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmFormat;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.spi.TypeConfiguration;
import static java.util.Arrays.asList;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL_UNIT;
/**
* Emulation of {@code datetrunc} function that leverages
* formatting the datetime to string and back to truncate it
*
* @author Marco Belladelli
*/
public class DateTruncEmulation extends AbstractSqmFunctionDescriptor implements FunctionRenderingSupport {
private final String toDateFunction;
private final boolean useConvertToFormat;
public DateTruncEmulation(String toDateFunction, boolean useConvertToFormat, TypeConfiguration typeConfiguration) {
super(
"date_trunc",
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL_UNIT, TEMPORAL ),
StandardFunctionReturnTypeResolvers.useArgType( 2 ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TEMPORAL_UNIT, TEMPORAL )
);
this.toDateFunction = toDateFunction;
this.useConvertToFormat = useConvertToFormat;
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( toDateFunction );
sqlAppender.append( '(' );
if ( !useConvertToFormat ) {
if ( toDateFunction.equalsIgnoreCase( "convert" ) ) {
sqlAppender.append( "datetime," );
sqlAstArguments.get( 0 ).accept( walker );
}
else {
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 1 ).accept( walker );
}
}
else {
// custom implementation that uses convert instead of format for Sybase
// see: https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc36271.1600/doc/html/san1393050437990.html
sqlAppender.append( "datetime,substring(convert(varchar," );
sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.append( ",21),1,17-len(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( "))+" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ",21" );
}
sqlAppender.append( ')' );
}
@Override
protected <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine,
TypeConfiguration typeConfiguration) {
final NodeBuilder nodeBuilder = queryEngine.getCriteriaBuilder();
final TemporalUnit temporalUnit = ( (SqmDurationUnit<?>) arguments.get( 0 ) ).getUnit();
final String pattern;
final String literal;
switch ( temporalUnit ) {
case YEAR:
pattern = "yyyy";
literal = "-01-01 00:00:00";
break;
case MONTH:
pattern = "yyyy-MM";
literal = "-01 00:00:00";
break;
case DAY:
pattern = "yyyy-MM-dd";
literal = " 00:00:00";
break;
case HOUR:
pattern = "yyyy-MM-dd HH";
literal = ":00:00";
break;
case MINUTE:
pattern = "yyyy-MM-dd HH:mm";
literal = ":00";
break;
case SECOND:
pattern = "yyyy-MM-dd HH:mm:ss";
literal = null;
break;
default:
throw new UnsupportedOperationException( "Temporal unit not supported [" + temporalUnit + "]" );
}
final SqmTypedNode<?> datetime = arguments.get( 1 );
final List<SqmTypedNode<?>> args = new ArrayList<>( 2 );
if ( !useConvertToFormat ) {
// use standard format function
final SqmExpression<?> formatExpression = queryEngine.getSqmFunctionRegistry()
.findFunctionDescriptor( "format" )
.generateSqmExpression(
asList(
datetime,
new SqmFormat(
pattern,
typeConfiguration.getBasicTypeForJavaType( String.class ),
nodeBuilder
)
),
null,
queryEngine,
typeConfiguration
);
final SqmExpression<?> formattedDatetime;
if ( literal != null ) {
formattedDatetime = queryEngine.getSqmFunctionRegistry()
.findFunctionDescriptor( "concat" )
.generateSqmExpression(
asList(
formatExpression,
new SqmLiteral<>(
literal,
typeConfiguration.getBasicTypeForJavaType( String.class ),
nodeBuilder
)
),
null,
queryEngine,
typeConfiguration
);
}
else {
formattedDatetime = formatExpression;
}
args.add( formattedDatetime );
args.add( new SqmFormat(
"yyyy-MM-dd HH:mm:ss",
typeConfiguration.getBasicTypeForJavaType( String.class ),
nodeBuilder
) );
}
else {
args.add( new SqmLiteral<>(
literal != null ? literal.replace( "-", "/" ) : "",
typeConfiguration.getBasicTypeForJavaType( String.class ),
nodeBuilder
) );
args.add( datetime );
}
return new SelfRenderingSqmFunction<>(
this,
this,
args,
impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
nodeBuilder,
"date_trunc"
);
}
}

View File

@ -0,0 +1,127 @@
/*
* 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.ArrayList;
import java.util.List;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionRenderingSupport;
import org.hibernate.query.sqm.function.SelfRenderingSqmFunction;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
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.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmDurationUnit;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL_UNIT;
/**
* Trunc function which converts the {@link TemporalUnit}
* to the format required for {@code trunc()}
*
* @author Marco Belladelli
*/
public class DateTruncTrunc extends AbstractSqmFunctionDescriptor implements FunctionRenderingSupport {
public DateTruncTrunc(TypeConfiguration typeConfiguration) {
super(
"date_trunc",
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL_UNIT, TEMPORAL ),
StandardFunctionReturnTypeResolvers.useArgType( 2 ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TEMPORAL_UNIT, TEMPORAL )
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "trunc(" );
sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ')' );
}
@Override
protected <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine,
TypeConfiguration typeConfiguration) {
final NodeBuilder nodeBuilder = queryEngine.getCriteriaBuilder();
final TemporalUnit temporalUnit = ( (SqmDurationUnit<?>) arguments.get( 0 ) ).getUnit();
final String pattern;
switch ( temporalUnit ) {
case YEAR:
pattern = "YYYY";
break;
case MONTH:
pattern = "MM";
break;
case WEEK:
pattern = "IW";
break;
case DAY:
pattern = "DD";
break;
case HOUR:
pattern = "HH";
break;
case MINUTE:
pattern = "MI";
break;
case SECOND:
if ( nodeBuilder.getSessionFactory().getJdbcServices().getDialect() instanceof OracleDialect ) {
// Oracle does not support truncating to seconds with the native function, use emulation
return new DateTruncEmulation( "to_date", false, typeConfiguration )
.generateSqmFunctionExpression(
arguments,
impliedResultType,
queryEngine,
typeConfiguration
);
}
pattern = "SS";
break;
default:
throw new UnsupportedOperationException( "Temporal unit not supported [" + temporalUnit + "]" );
}
final List<SqmTypedNode<?>> args = new ArrayList<>( 2 );
args.add( new SqmLiteral<>(
pattern,
typeConfiguration.getBasicTypeForJavaType( String.class ),
nodeBuilder
) );
args.add( arguments.get( 1 ) );
return new SelfRenderingSqmFunction<>(
this,
this,
args,
impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
nodeBuilder,
"date_trunc"
);
}
}

View File

@ -513,13 +513,17 @@ public class FunctionTests {
}
@Test
@RequiresDialect(H2Dialect.class)
@RequiresDialect(DB2Dialect.class)
@RequiresDialect(OracleDialect.class)
@RequiresDialect(PostgreSQLDialect.class)
@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support any form of date truncation")
public void testDateTruncFunction(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.createQuery("select date_trunc(year,current_timestamp)", Timestamp.class).getSingleResult()
session -> {
session.createQuery( "select date_trunc(year,current_timestamp)", Timestamp.class ).getSingleResult();
session.createQuery( "select date_trunc(month,current_timestamp)", Timestamp.class ).getSingleResult();
session.createQuery( "select date_trunc(day,current_timestamp)", Timestamp.class ).getSingleResult();
session.createQuery( "select date_trunc(hour,current_timestamp)", Timestamp.class ).getSingleResult();
session.createQuery( "select date_trunc(minute,current_timestamp)", Timestamp.class ).getSingleResult();
session.createQuery( "select date_trunc(second,current_timestamp)", Timestamp.class ).getSingleResult();
}
);
}