Skip to content

Commit 3fabc9e

Browse files
HHH-18837 Oracle epoch extraction doesn't work with dates
1 parent 3cfeb8f commit 3fabc9e

File tree

4 files changed

+179
-3
lines changed

4 files changed

+179
-3
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,11 @@ public String extractPattern(TemporalUnit unit) {
14101410
return "extract(?1 from ?2)";
14111411
}
14121412

1413+
@SuppressWarnings("deprecation")
1414+
public String extractPattern(TemporalUnit unit, TemporalType temporalType) {
1415+
return extractPattern( unit );
1416+
}
1417+
14131418
/**
14141419
* Obtain a pattern for the SQL equivalent to a
14151420
* {@code cast()} function call. The resulting

hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,26 @@ public String extractPattern(TemporalUnit unit) {
624624
case HOUR -> "to_number(to_char(?2,'HH24'))";
625625
case MINUTE -> "to_number(to_char(?2,'MI'))";
626626
case SECOND -> "to_number(to_char(?2,'SS'))";
627-
case EPOCH -> "trunc((cast(?2 at time zone 'UTC' as date) - date '1970-1-1')*86400)";
628627
default -> super.extractPattern(unit);
629628
};
630629
}
631630

631+
@Override
632+
@SuppressWarnings("deprecation")
633+
public String extractPattern(TemporalUnit unit, TemporalType temporalType) {
634+
return switch ( unit ) {
635+
case EPOCH:
636+
if ( temporalType == TemporalType.DATE ) {
637+
yield "trunc((cast(from_tz(cast(?2 as timestamp),'UTC') as date) - date '1970-1-1')*86400)";
638+
}
639+
else {
640+
yield "trunc((cast(?2 at time zone 'UTC' as date) - date '1970-1-1')*86400)";
641+
}
642+
default:
643+
yield extractPattern( unit );
644+
};
645+
}
646+
632647
@Override @SuppressWarnings("deprecation")
633648
public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) {
634649
final StringBuilder pattern = new StringBuilder();

hibernate-core/src/main/java/org/hibernate/dialect/function/ExtractFunction.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55
package org.hibernate.dialect.function;
66

7+
import jakarta.persistence.TemporalType;
78
import org.hibernate.dialect.Dialect;
9+
import org.hibernate.dialect.OracleDialect;
810
import org.hibernate.metamodel.model.domain.ReturnableType;
911
import org.hibernate.query.SemanticException;
1012
import org.hibernate.query.common.TemporalUnit;
@@ -24,6 +26,7 @@
2426
import org.hibernate.sql.ast.SqlAstTranslator;
2527
import org.hibernate.sql.ast.spi.SqlAppender;
2628
import org.hibernate.sql.ast.tree.SqlAstNode;
29+
import org.hibernate.sql.ast.tree.expression.Expression;
2730
import org.hibernate.sql.ast.tree.expression.ExtractUnit;
2831
import org.hibernate.type.BasicType;
2932
import org.hibernate.type.spi.TypeConfiguration;
@@ -37,6 +40,7 @@
3740
import static org.hibernate.query.common.TemporalUnit.*;
3841
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
3942
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL_UNIT;
43+
import static org.hibernate.type.spi.TypeConfiguration.getSqlTemporalType;
4044
import static org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME;
4145

4246
/**
@@ -69,10 +73,22 @@ public void render(
6973
List<? extends SqlAstNode> sqlAstArguments,
7074
ReturnableType<?> returnType,
7175
SqlAstTranslator<?> walker) {
76+
new PatternRenderer( extractPattern( sqlAstArguments ) ).render( sqlAppender, sqlAstArguments, walker );
77+
}
78+
79+
@SuppressWarnings("deprecation")
80+
private String extractPattern(List<? extends SqlAstNode> sqlAstArguments) {
7281
final ExtractUnit field = (ExtractUnit) sqlAstArguments.get( 0 );
7382
final TemporalUnit unit = field.getUnit();
74-
final String pattern = dialect.extractPattern( unit );
75-
new PatternRenderer( pattern ).render( sqlAppender, sqlAstArguments, walker );
83+
final Expression expression = (Expression) sqlAstArguments.get( 1 );
84+
final TemporalType temporalType;
85+
if ( dialect instanceof OracleDialect && (temporalType = getSqlTemporalType(
86+
expression.getExpressionType() )) != null ) {
87+
return dialect.extractPattern( unit, temporalType );
88+
}
89+
else {
90+
return dialect.extractPattern( unit );
91+
}
7692
}
7793

7894
@Override
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.query;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.GeneratedValue;
9+
import jakarta.persistence.Id;
10+
import org.hibernate.dialect.OracleDialect;
11+
import org.hibernate.testing.orm.junit.DomainModel;
12+
import org.hibernate.testing.orm.junit.JiraKey;
13+
import org.hibernate.testing.orm.junit.RequiresDialect;
14+
import org.hibernate.testing.orm.junit.SessionFactory;
15+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
16+
import org.junit.jupiter.api.AfterAll;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.time.LocalDate;
20+
import java.time.LocalDateTime;
21+
import java.time.LocalTime;
22+
import java.time.ZoneId;
23+
import java.time.ZoneOffset;
24+
import java.time.ZonedDateTime;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
28+
29+
@SessionFactory
30+
@DomainModel(annotatedClasses = {
31+
OracleDateTypeTest.AuthorDate.class,
32+
OracleDateTypeTest.AuthorTimestamp.class,
33+
OracleDateTypeTest.AuthorTimestampTz.class
34+
})
35+
@RequiresDialect(OracleDialect.class)
36+
@JiraKey("HHH-18837")
37+
public class OracleDateTypeTest {
38+
39+
@Test
40+
void hhh18837test_withDate(SessionFactoryScope scope) {
41+
42+
// given
43+
LocalDate birth = LocalDate.of( 2013, 7, 5 );
44+
45+
// prepare
46+
scope.inTransaction( session -> {
47+
48+
AuthorDate authorDate = new AuthorDate();
49+
authorDate.birth = birth;
50+
session.persist( authorDate );
51+
} );
52+
53+
// assert
54+
scope.inTransaction( session -> {
55+
Long epoch = session.createSelectionQuery( "select epoch(a.birth) from AuthorDate a", Long.class )
56+
.getSingleResult();
57+
assertEquals( birth.atStartOfDay( ZoneOffset.UTC ).toInstant().toEpochMilli() / 1000, epoch );
58+
} );
59+
}
60+
61+
@Test
62+
void hhh18837test_withTimestamp(SessionFactoryScope scope) {
63+
64+
// given
65+
LocalDateTime birth = LocalDate.of( 2013, 7, 5 ).atTime( LocalTime.MIN );
66+
67+
// prepare
68+
scope.inTransaction( session -> {
69+
70+
AuthorTimestamp authorTimestamp = new AuthorTimestamp();
71+
authorTimestamp.birth = birth;
72+
session.persist( authorTimestamp );
73+
} );
74+
75+
// assert
76+
scope.inTransaction( session -> {
77+
Long epoch = session.createSelectionQuery( "select epoch(a.birth) from AuthorTimestamp a", Long.class )
78+
.getSingleResult();
79+
assertEquals( birth.toEpochSecond( ZoneOffset.UTC ), epoch );
80+
} );
81+
}
82+
83+
@Test
84+
void hhh18837test_withTimestampTz(SessionFactoryScope scope) {
85+
86+
// given
87+
ZonedDateTime birth = ZonedDateTime.of( LocalDate.of( 2013, 7, 5 ), LocalTime.MIN,
88+
ZoneId.of( "Europe/Vienna" ) );
89+
90+
// prepare
91+
scope.inTransaction( session -> {
92+
93+
AuthorTimestampTz authorTimestampTz = new AuthorTimestampTz();
94+
authorTimestampTz.birth = birth;
95+
session.persist( authorTimestampTz );
96+
} );
97+
98+
// assert
99+
scope.inTransaction( session -> {
100+
Long epoch = session.createSelectionQuery( "select epoch(a.birth) from AuthorTimestampTz a", Long.class )
101+
.getSingleResult();
102+
assertEquals( birth.toEpochSecond(), epoch );
103+
} );
104+
}
105+
106+
@AfterAll
107+
public void dropTestData(SessionFactoryScope scope) {
108+
scope.dropData();
109+
}
110+
111+
@Entity(name = "AuthorDate")
112+
public static class AuthorDate {
113+
114+
@Id
115+
@GeneratedValue
116+
long id;
117+
118+
LocalDate birth;
119+
}
120+
121+
@Entity(name = "AuthorTimestamp")
122+
public static class AuthorTimestamp {
123+
124+
@Id
125+
@GeneratedValue
126+
long id;
127+
128+
LocalDateTime birth;
129+
}
130+
131+
@Entity(name = "AuthorTimestampTz")
132+
public static class AuthorTimestampTz {
133+
134+
@Id
135+
@GeneratedValue
136+
long id;
137+
138+
ZonedDateTime birth;
139+
}
140+
}

0 commit comments

Comments
 (0)