diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java index 34f0469469..7ece118abb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java @@ -38,7 +38,6 @@ 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; @@ -53,6 +52,7 @@ import org.hibernate.type.StandardBasicTypes; import jakarta.persistence.TemporalType; import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION; +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useArgType; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; @@ -394,7 +394,7 @@ public class SpannerDialect extends Dialect { .setExactArgumentCount( 3 ) .register(); functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_trunc" ) - .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.useArgType( 2 ) ) + .setReturnTypeResolver( useArgType( 1 ) ) .setExactArgumentCount( 2 ) .register(); functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_from_unix_date" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseTruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseTruncFunction.java index 9ad6b3ea7a..30972a2c76 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseTruncFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseTruncFunction.java @@ -23,6 +23,7 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.type.spi.TypeConfiguration; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; /** * Custom {@link TruncFunction} for Sybase which uses a dialect-specific emulation function for datetimes @@ -94,10 +95,16 @@ public class SybaseTruncFunction extends TruncFunction { sqlAppender.append( '(' ); sqlAppender.append( "datetime,substring(convert(varchar," ); sqlAstArguments.get( 0 ).accept( walker ); - sqlAppender.append( ",21),1,17-len(" ); - sqlAstArguments.get( 1 ).accept( walker ); - sqlAppender.append( "))+" ); - sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.append( ",21),1,17" ); + if ( sqlAstArguments.size() > 1 ) { + sqlAppender.append( "-len(" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.append( "))+" ); + sqlAstArguments.get( 1 ).accept( walker ); + } + else { + sqlAppender.append( ')' ); + } sqlAppender.append( ",21)" ); } @@ -127,22 +134,25 @@ public class SybaseTruncFunction extends TruncFunction { literal = ":00"; break; case SECOND: - literal = ""; + literal = null; break; default: throw new UnsupportedOperationException( "Temporal unit not supported [" + temporalUnit + "]" ); } - final SqmTypedNode datetime = arguments.get( 0 ); - final SqmLiteral sqmLiteral = new SqmLiteral<>( - literal, - typeConfiguration.getBasicTypeForJavaType( String.class ), - nodeBuilder - ); return new SelfRenderingSqmFunction<>( this, this, - asList( datetime, sqmLiteral ), + literal == null ? + singletonList( arguments.get( 0 ) ) : + asList( + arguments.get( 0 ), + new SqmLiteral<>( + literal, + typeConfiguration.getBasicTypeForJavaType( String.class ), + nodeBuilder + ) + ), impliedResultType, null, getReturnTypeResolver(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index e12acebdae..8f9aae024f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -36,6 +36,7 @@ import org.hibernate.Incubating; import org.hibernate.query.sqm.FrameKind; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; +import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.tree.expression.SqmExpression; /** @@ -997,6 +998,9 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaFunction second(Expression datetime); + @Incubating + JpaFunction truncate(Expression datetime, TemporalUnit temporalUnit); + /** * @see #overlay(Expression, Expression, Expression, Expression) */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 4e8991fdbd..9b9c2e3ebf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -52,6 +52,7 @@ import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.criteria.JpaWindowFrame; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; +import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.tree.expression.SqmExpression; import jakarta.persistence.Tuple; @@ -1303,6 +1304,11 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.second( datetime ); } + @Override + public JpaFunction truncate(Expression datetime, TemporalUnit temporalUnit) { + return criteriaBuilder.truncate( datetime, temporalUnit ); + } + @Override public JpaFunction overlay(Expression string, String replacement, int start) { return criteriaBuilder.overlay( string, replacement, start ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index c52a93283a..0fc10d829c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -2650,6 +2650,19 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return extract( datetime, TemporalUnit.SECOND, Float.class ); } + @Override + public SqmFunction truncate(Expression datetime, TemporalUnit temporalUnit) { + return getFunctionDescriptor( "trunc" ).generateSqmExpression( + asList( + (SqmTypedNode) datetime, + new SqmExtractUnit<>( temporalUnit, getIntegerType(), this ) + ), + null, + queryEngine, + getJpaMetamodel().getTypeConfiguration() + ); + } + @Override public SqmFunction overlay(Expression string, String replacement, int start) { return overlay( string, replacement, value( start ), null ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java index 16f612916e..c7c569d38f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java @@ -12,12 +12,15 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; @@ -438,7 +441,7 @@ public class CriteriaBuilderNonStandardFunctionsTest { @Test @JiraKey("HHH-16185") - public void testCustomFunctionTrunc(SessionFactoryScope scope) { + public void testNumericTruncFunction(SessionFactoryScope scope) { scope.inTransaction( session -> { final CriteriaBuilder cb = session.getCriteriaBuilder(); final CriteriaQuery query = cb.createTupleQuery(); @@ -463,4 +466,41 @@ public class CriteriaBuilderNonStandardFunctionsTest { assertEquals( 32.923d, result.get( 7 ) ); } ); } + + @Test + @JiraKey("HHH-16130") + @SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support any form of date truncation") + public void testDateTruncFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createTupleQuery(); + Root from = query.from( EntityOfBasics.class ); + + Expression theLocalDateTime = from.get( "theLocalDateTime" ); + query.multiselect( + from.get( "id" ), + cb.truncate( theLocalDateTime, TemporalUnit.YEAR ), + cb.truncate( theLocalDateTime, TemporalUnit.MONTH ), + cb.truncate( theLocalDateTime, TemporalUnit.DAY ), + cb.truncate( theLocalDateTime, TemporalUnit.HOUR ), + cb.truncate( theLocalDateTime, TemporalUnit.MINUTE ), + cb.truncate( theLocalDateTime, TemporalUnit.SECOND ) + ).where( cb.isNotNull( theLocalDateTime ) ); + + Tuple result = session.createQuery( query ).getSingleResult(); + EntityOfBasics eob = session.find( EntityOfBasics.class, result.get( 0 ) ); + assertEquals( + eob.getTheLocalDateTime().withMonth( 1 ).withDayOfMonth( 1 ).truncatedTo( ChronoUnit.DAYS ), + result.get( 1 ) + ); + assertEquals( + eob.getTheLocalDateTime().withDayOfMonth( 1 ).truncatedTo( ChronoUnit.DAYS ), + result.get( 2 ) + ); + assertEquals( eob.getTheLocalDateTime().truncatedTo( ChronoUnit.DAYS ), result.get( 3 ) ); + assertEquals( eob.getTheLocalDateTime().truncatedTo( ChronoUnit.HOURS ), result.get( 4 ) ); + assertEquals( eob.getTheLocalDateTime().truncatedTo( ChronoUnit.MINUTES ), result.get( 5 ) ); + assertEquals( eob.getTheLocalDateTime().truncatedTo( ChronoUnit.SECONDS ), result.get( 6 ) ); + } ); + } }