Implement support for TimeZoneStorageType.COLUMN

This commit is contained in:
Christian Beikov 2022-03-03 15:12:48 +01:00
parent fa750a9c26
commit 964e72f536
40 changed files with 2072 additions and 178 deletions

View File

@ -273,9 +273,10 @@ Specifies whether to automatically quote any names that are deemed keywords.
`*hibernate.timezone.default_storage*` (e.g. `COLUMN`, `NATIVE`, `AUTO` or `NORMALIZE` (default value))::
Global setting for configuring the default storage for the time zone information for time zone based types.
+
`NORMALIZE`::: Does not store the time zone, and instead normalizes timestamps to UTC
`NATIVE`::: Stores the time zone by using the `with time zone` type. Error if `Dialect#getTimeZoneSupport()` is not `NATIVE`
`AUTO`::: Stores the time zone either with `NATIVE` if `Dialect#getTimeZoneSupport()` is `NATIVE`, otherwise uses the `COLUMN` strategy.
`NORMALIZE`::: Does not store the time zone information, and instead normalizes timestamps to UTC
`COLUMN`::: Stores the time zone information in a separate column; works in conjunction with `@TimeZoneColumn`
`NATIVE`::: Stores the time zone information by using the `with time zone` type. Error if `Dialect#getTimeZoneSupport()` is not `NATIVE`
`AUTO`::: Stores the time zone information either with `NATIVE` if `Dialect#getTimeZoneSupport()` is `NATIVE`, otherwise uses the `COLUMN` strategy.
+
The default value is given by the {@link org.hibernate.annotations.TimeZoneStorageType#NORMALIZE},
meaning that time zone information is not stored by default, but timestamps are normalized instead.

View File

@ -1509,7 +1509,7 @@ The `JavaType` resolved earlier is then inspected for a number of special cases.
still uses any explicit `JdbcType` indicators
. For temporal values, we check for `@Temporal` and create an enumeration mapping. Note that this resolution
still uses any explicit `JdbcType` indicators; this includes `@JdbcType` and `@JdbcTypeCode`, as well as
`@TimeZoneStorage` if appropriate.
`@TimeZoneStorage` and `@TimeZoneColumn` if appropriate.
The fallback at this point is to use the `JavaType` and `JdbcType` determined in earlier steps to create a
JDBC-mapping (which encapsulates the `JavaType` and `JdbcType`) and combines it with the resolved `MutabilityPlan`
@ -1668,12 +1668,14 @@ boil down to the 3 main Date/Time types defined by the SQL specification:
DATE:: Represents a calendar date by storing years, months and days.
TIME:: Represents the time of a day by storing hours, minutes and seconds.
TIMESTAMP:: Represents both a DATE and a TIME plus nanoseconds.
TIMESTAMP WITH TIME ZONE:: Represents both a DATE and a TIME plus nanoseconds and zone id or offset.
The mapping of `java.time` temporal types to the specific SQL Date/Time types is implied as follows:
DATE:: `java.time.LocalDate`
TIME:: `java.time.LocalTime`, `java.time.OffsetTime`
TIMESTAMP:: `java.time.Instant`, `java.time.LocalDateTime`, `java.time.OffsetDateTime` and `java.time.ZonedDateTime`
TIMESTAMP WITH TIME ZONE:: `java.time.OffsetDateTime`, `java.time.ZonedDateTime`
Although Hibernate recommends the use of the `java.time` package for representing temporal values,
it does support using `java.sql.Date`, `java.sql.Time`, `java.sql.Timestamp`, `java.util.Date` and
@ -1759,6 +1761,50 @@ Session session = sessionFactory()
With this configuration property in place, Hibernate is going to call the https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTimestamp-int-java.sql.Timestamp-java.util.Calendar-[`PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp, Calendar cal)`] or
https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTime-int-java.sql.Time-java.util.Calendar-[`PreparedStatement.setTime(int parameterIndex, java.sql.Time x, Calendar cal)`], where the `java.util.Calendar` references the time zone provided via the `hibernate.jdbc.time_zone` property.
[[basic-timestamp-with-time-zone]]
===== Handling time zoned temporal data
By default, Hibernate will convert and normalize `OffsetDateTime` and `ZonedDateTime` to `java.sql.Timestamp` in UTC.
This behavior can be altered by configuring the `hibernate.timezone.default_storage` property
[source,java]
----
settings.put(
AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
TimeZoneStorageType.AUTO
);
----
Other possible storage types are `AUTO`, `COLUMN`, `NATIVE` and `NORMALIZE` (the default).
With `COLUMN`, Hibernate will save the time zone information into a dedicated column,
whereas `NATIVE` will require the support of database for a `TIMESTAMP WITH TIME ZONE` data type
that retains the time zone information.
`NORMALIZE` doesn't store time zone information and will simply convert the timestamp to UTC.
Hibernate understands what a database/dialect supports through `Dialect#getTimeZoneSupport`
and will abort with a boot error if the `NATIVE` is used in conjunction with a database that doesn't support this.
For `AUTO`, Hibernate tries to use `NATIVE` if possible and falls back to `COLUMN` otherwise.
==== `@TimeZoneStorage`
Hibernate supports defining the storage to use for time zone information for individual properties
via the `@TimeZoneStorage` and `@TimeZoneColumn` annotations.
The storage type can be specified via the `@TimeZoneStorage` by specifying a `org.hibernate.annotations.TimeZoneStorageType`.
The default storage type is `AUTO` which will ensure that the time zone information is retained.
The `@TimeZoneColumn` annotation can be used in conjunction with `AUTO` or `COLUMN` and allows to define
the column details for the time zone information storage.
[NOTE]
====
Storing the zone offset might be problematic for future timestamps as zone rules can change.
Due to this, storing the offset is only safe for past timestamps, and we advise sticking to the `NORMALIZE` strategy by default.
.`@TimeZoneColumn` usage
====
[source, JAVA, indent=0]
----
include::{sourcedir}/basic/TimeZoneStorageMappingTests.java[tags=time-zone-column-examples-mapping-example]
----
====

View File

@ -6,7 +6,6 @@
*/
package org.hibernate.userguide.mapping.basic;
import java.sql.Types;
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

View File

@ -0,0 +1,216 @@
/*
* 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.userguide.mapping.basic;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.hibernate.annotations.TimeZoneColumn;
import org.hibernate.annotations.TimeZoneStorage;
import org.hibernate.annotations.TimeZoneStorageType;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
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.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Tuple;
import org.hamcrest.Matchers;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Christian Beikov
*/
@DomainModel(annotatedClasses = TimeZoneStorageMappingTests.TimeZoneStorageEntity.class)
@SessionFactory
@ServiceRegistry(settings = @Setting( name = AvailableSettings.TIMEZONE_DEFAULT_STORAGE, value = "AUTO"))
public class TimeZoneStorageMappingTests {
private static final OffsetDateTime OFFSET_DATE_TIME = OffsetDateTime.of(
LocalDateTime.of(
2022,
3,
1,
12,
0,
0
),
ZoneOffset.ofHoursMinutes( 5, 45 )
);
private static final ZonedDateTime ZONED_DATE_TIME = ZonedDateTime.of(
LocalDateTime.of(
2022,
3,
1,
12,
0,
0
),
ZoneOffset.ofHoursMinutes( 5, 45 )
);
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern( "dd/MM/yyyy 'at' HH:mm:ssxxx" );
@BeforeEach
public void setup(SessionFactoryScope scope) {
scope.inTransaction( s -> s.persist( new TimeZoneStorageEntity( 1, OFFSET_DATE_TIME, ZONED_DATE_TIME ) ) );
}
@AfterEach
public void destroy(SessionFactoryScope scope) {
scope.inTransaction( s -> s.createMutationQuery( "delete from java.lang.Object" ).executeUpdate() );
}
@Test
public void testOffsetRetainedAuto(SessionFactoryScope scope) {
testOffsetRetained( scope, "Auto" );
}
@Test
public void testOffsetRetainedColumn(SessionFactoryScope scope) {
testOffsetRetained( scope, "Column" );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class)
public void testOffsetRetainedFormatAuto(SessionFactoryScope scope) {
testOffsetRetainedFormat( scope, "Auto" );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class)
public void testOffsetRetainedFormatColumn(SessionFactoryScope scope) {
testOffsetRetainedFormat( scope, "Column" );
}
public void testOffsetRetained(SessionFactoryScope scope, String suffix) {
scope.inSession(
session -> {
List<Tuple> resultList = session.createQuery(
"select " +
"e.offsetDateTime" + suffix + ", " +
"e.zonedDateTime" + suffix + ", " +
"extract(offset from e.offsetDateTime" + suffix + "), " +
"extract(offset from e.zonedDateTime" + suffix + "), " +
"e.offsetDateTime" + suffix + " + 1 hour, " +
"e.zonedDateTime" + suffix + " + 1 hour, " +
"e.offsetDateTime" + suffix + " + 1 hour - e.offsetDateTime" + suffix + ", " +
"e.zonedDateTime" + suffix + " + 1 hour - e.zonedDateTime" + suffix + ", " +
"1 from TimeZoneStorageEntity e " +
"where e.offsetDateTime" + suffix + " = e.offsetDateTime" + suffix,
Tuple.class
).getResultList();
assertThat( resultList.get( 0 ).get( 0, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME ) );
assertThat( resultList.get( 0 ).get( 1, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME ) );
assertThat( resultList.get( 0 ).get( 2, ZoneOffset.class ), Matchers.is( OFFSET_DATE_TIME.getOffset() ) );
assertThat( resultList.get( 0 ).get( 3, ZoneOffset.class ), Matchers.is( ZONED_DATE_TIME.getOffset() ) );
assertThat( resultList.get( 0 ).get( 4, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME.plusHours( 1L ) ) );
assertThat( resultList.get( 0 ).get( 5, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME.plusHours( 1L ) ) );
assertThat( resultList.get( 0 ).get( 6, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) );
assertThat( resultList.get( 0 ).get( 7, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) );
}
);
}
public void testOffsetRetainedFormat(SessionFactoryScope scope, String suffix) {
scope.inSession(
session -> {
List<Tuple> resultList = session.createQuery(
"select " +
"format(e.offsetDateTime" + suffix + " as 'dd/MM/yyyy ''at'' HH:mm:ssxxx'), " +
"format(e.zonedDateTime" + suffix + " as 'dd/MM/yyyy ''at'' HH:mm:ssxxx'), " +
"1 from TimeZoneStorageEntity e " +
"where e.offsetDateTime" + suffix + " = e.offsetDateTime" + suffix,
Tuple.class
).getResultList();
assertThat( resultList.get( 0 ).get( 0, String.class ), Matchers.is( FORMATTER.format( OFFSET_DATE_TIME ) ) );
assertThat( resultList.get( 0 ).get( 1, String.class ), Matchers.is( FORMATTER.format( ZONED_DATE_TIME ) ) );
}
);
}
// @Test
// @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class)
// public void testNormalize(SessionFactoryScope scope) {
// scope.inSession(
// session -> {
// List<Tuple> resultList = session.createQuery(
// "select e.offsetDateTimeNormalized, extract(offset from e.offsetDateTimeNormalized), e.zonedDateTimeNormalized, extract(offset from e.zonedDateTimeNormalized) from TimeZoneStorageEntity e",
// Tuple.class
// ).getResultList();
// assertThat( resultList.get( 0 ).get( 0, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME ) );
// assertThat( resultList.get( 0 ).get( 1, ZoneOffset.class ), Matchers.is( OFFSET_DATE_TIME.getOffset() ) );
// assertThat( resultList.get( 0 ).get( 2, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME ) );
// assertThat( resultList.get( 0 ).get( 3, ZoneOffset.class ), Matchers.is( ZONED_DATE_TIME.getOffset() ) );
// }
// );
// }
@Entity(name = "TimeZoneStorageEntity")
@Table(name = "TimeZoneStorageEntity")
public static class TimeZoneStorageEntity {
@Id
private Integer id;
//end::time-zone-column-examples-mapping-example[]
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_offset_offset")
@Column(name = "birthday_offset")
private OffsetDateTime offsetDateTimeColumn;
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_zoned_offset")
@Column(name = "birthday_zoned")
private ZonedDateTime zonedDateTimeColumn;
//end::time-zone-column-examples-mapping-example[]
@TimeZoneStorage
@Column(name = "birthday_offset_auto")
private OffsetDateTime offsetDateTimeAuto;
@TimeZoneStorage
@Column(name = "birthday_zoned_auto")
private ZonedDateTime zonedDateTimeAuto;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
@Column(name = "birthday_offset_normalized")
private OffsetDateTime offsetDateTimeNormalized;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
@Column(name = "birthday_zoned_normalized")
private ZonedDateTime zonedDateTimeNormalized;
public TimeZoneStorageEntity() {
}
public TimeZoneStorageEntity(Integer id, OffsetDateTime offsetDateTime, ZonedDateTime zonedDateTime) {
this.id = id;
this.offsetDateTimeColumn = offsetDateTime;
this.zonedDateTimeColumn = zonedDateTime;
this.offsetDateTimeAuto = offsetDateTime;
this.zonedDateTimeAuto = zonedDateTime;
this.offsetDateTimeNormalized = offsetDateTime;
this.zonedDateTimeNormalized = zonedDateTime;
}
}
}

View File

@ -19,6 +19,10 @@ public enum TimeZoneStorageStrategy {
* Stores the time zone through the "with time zone" types which retain the information.
*/
NATIVE,
/**
* Stores the time zone in a separate column.
*/
COLUMN,
/**
* Doesn't store the time zone, but instead normalizes to UTC.
*/

View File

@ -0,0 +1,68 @@
/*
* 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.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hibernate.Incubating;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
/**
* Specifies the mapped column for storing the time zone information.
* The annotation can be used in conjunction with the <code>TimeZoneStorageType.AUTO</code> and
* <code>TimeZoneStorageType.COLUMN</code>. The column is simply ignored if <code>TimeZoneStorageType.AUTO</code>
* is used and the database supports native time zone storage.
*
* @author Christian Beikov
* @author Steve Ebersole
* @author Andrea Boriero
* @see TimeZoneStorage
* @see TimeZoneStorageType#COLUMN
* @see TimeZoneStorageType#AUTO
*/
@Incubating
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, METHOD })
public @interface TimeZoneColumn {
/**
* (Optional) The name of the column. Defaults to
* the property or field name, suffixed by <code>_tz</code>.
*/
String name() default "";
/**
* (Optional) Whether the column is included in SQL INSERT
* statements generated by the persistence provider.
*/
boolean insertable() default true;
/**
* (Optional) Whether the column is included in SQL UPDATE
* statements generated by the persistence provider.
*/
boolean updatable() default true;
/**
* (Optional) The SQL fragment that is used when
* generating the DDL for the column.
* <p> Defaults to the generated SQL to create a
* column of the inferred type.
*/
String columnDefinition() default "";
/**
* (Optional) The name of the table that contains the column.
* If absent the column is assumed to be in the primary table.
*/
String table() default "";
}

View File

@ -38,6 +38,7 @@ import static java.lang.annotation.ElementType.METHOD;
* @author Christian Beikov
* @author Steve Ebersole
* @author Andrea Boriero
* @see TimeZoneColumn
*/
@Incubating
@Retention(RetentionPolicy.RUNTIME)
@ -46,5 +47,5 @@ public @interface TimeZoneStorage {
/**
* The storage strategy for the time zone information.
*/
TimeZoneStorageType value();
TimeZoneStorageType value() default TimeZoneStorageType.AUTO;
}

View File

@ -30,5 +30,18 @@ public enum TimeZoneStorageType {
* Does not store the time zone, and instead normalizes
* timestamps to UTC.
*/
NORMALIZE
NORMALIZE,
/**
* Stores the time zone in a separate column; works in
* conjunction with {@link TimeZoneColumn}.
*/
COLUMN,
/**
* Stores the time zone either with {@link #NATIVE} if
* {@link Dialect#getTimeZoneSupport()} is
* {@link org.hibernate.dialect.TimeZoneSupport#NATIVE},
* otherwise uses the {@link #COLUMN} strategy.
*/
AUTO
}

View File

@ -1,30 +0,0 @@
/*
* 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.annotations;
import org.hibernate.Incubating;
/**
* The type of storage to use for the time zone information.
*
* @author Christian Beikov
* @author Steve Ebersole
* @author Andrea Boriero
*/
@Incubating
public enum TimeZoneType {
/**
* Stores the time zone id as String.
*/
ZONE_ID,
/**
* Stores the offset seconds of a timestamp as Integer.
*/
OFFSET;
}

View File

@ -875,7 +875,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
ConfigurationService configService) {
final TimeZoneStorageType configuredTimeZoneStorageType = configService.getSetting(
AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
TimeZoneStorageType.class,
value -> TimeZoneStorageType.valueOf( value.toString() ),
null
);
final TimeZoneStorageStrategy resolvedTimezoneStorage;
@ -894,9 +894,25 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
}
resolvedTimezoneStorage = TimeZoneStorageStrategy.NATIVE;
break;
case COLUMN:
resolvedTimezoneStorage = TimeZoneStorageStrategy.COLUMN;
break;
case NORMALIZE:
resolvedTimezoneStorage = TimeZoneStorageStrategy.NORMALIZE;
break;
case AUTO:
switch ( timeZoneSupport ) {
case NATIVE:
resolvedTimezoneStorage = TimeZoneStorageStrategy.NATIVE;
break;
case NORMALIZE:
case NONE:
resolvedTimezoneStorage = TimeZoneStorageStrategy.COLUMN;
break;
default:
throw new HibernateException( "Unsupported time zone support: " + timeZoneSupport );
}
break;
default:
throw new HibernateException( "Unsupported time zone storage type: " + configuredTimeZoneStorageType );
}

View File

@ -65,6 +65,8 @@ public class VersionResolution<E> implements BasicValue.Resolution<E> {
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
if ( timeZoneStorageType != null ) {
switch ( timeZoneStorageType ) {
case COLUMN:
return TimeZoneStorageStrategy.COLUMN;
case NATIVE:
return TimeZoneStorageStrategy.NATIVE;
case NORMALIZE:

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.cfg;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -25,16 +26,23 @@ import jakarta.persistence.MappedSuperclass;
import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.TimeZoneStorageStrategy;
import org.hibernate.annotations.ColumnTransformer;
import org.hibernate.annotations.ColumnTransformers;
import org.hibernate.annotations.TimeZoneColumn;
import org.hibernate.annotations.TimeZoneStorage;
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.ImplicitBasicColumnNameSource;
import org.hibernate.boot.model.source.spi.AttributePath;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType;
import org.jboss.logging.Logger;
@ -180,7 +188,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder {
this.currentPropertyForeignKeyOverride = null;
}
else {
this.currentPropertyColumnOverride = buildColumnOverride( property, getPath() );
this.currentPropertyColumnOverride = buildColumnOverride( property, getPath(), context );
if ( this.currentPropertyColumnOverride.size() == 0 ) {
this.currentPropertyColumnOverride = null;
}
@ -408,7 +416,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder {
if ( current.isAnnotationPresent( Entity.class ) || current.isAnnotationPresent( MappedSuperclass.class )
|| current.isAnnotationPresent( Embeddable.class ) ) {
//FIXME is embeddable override?
Map<String, Column[]> currentOverride = buildColumnOverride( current, getPath() );
Map<String, Column[]> currentOverride = buildColumnOverride( current, getPath(), context );
Map<String, ColumnTransformer> currentTransformerOverride = buildColumnTransformerOverride( current, getPath() );
Map<String, JoinColumn[]> currentJoinOverride = buildJoinColumnOverride( current, getPath() );
Map<String, JoinTable> currentJoinTableOverride = buildJoinTableOverride( current, getPath() );
@ -434,7 +442,10 @@ public abstract class AbstractPropertyHolder implements PropertyHolder {
holderForeignKeyOverride = foreignKeyOverride.size() > 0 ? foreignKeyOverride : null;
}
private static Map<String, Column[]> buildColumnOverride(XAnnotatedElement element, String path) {
private static Map<String, Column[]> buildColumnOverride(
XAnnotatedElement element,
String path,
MetadataBuildingContext context) {
Map<String, Column[]> columnOverride = new HashMap<>();
if ( element != null ) {
AttributeOverride singleOverride = element.getAnnotation( AttributeOverride.class );
@ -474,6 +485,93 @@ public abstract class AbstractPropertyHolder implements PropertyHolder {
);
}
}
else {
final TimeZoneStorage timeZoneStorage = element.getAnnotation( TimeZoneStorage.class );
if ( timeZoneStorage != null ) {
switch ( timeZoneStorage.value() ) {
case AUTO:
if ( context.getBuildingOptions().getDefaultTimeZoneStorage() != TimeZoneStorageStrategy.COLUMN ) {
break;
}
case COLUMN:
final Column column;
final Column annotatedColumn = element.getAnnotation( Column.class );
if ( annotatedColumn != null ) {
column = annotatedColumn;
}
else {
// Base the name of the synthetic dateTime field on the name of the original attribute
final Identifier implicitName = context.getObjectNameNormalizer().normalizeIdentifierQuoting(
context.getBuildingOptions().getImplicitNamingStrategy().determineBasicColumnName(
new ImplicitBasicColumnNameSource() {
final AttributePath attributePath = AttributePath.parse( path );
@Override
public AttributePath getAttributePath() {
return attributePath;
}
@Override
public boolean isCollectionElement() {
return false;
}
@Override
public MetadataBuildingContext getBuildingContext() {
return context;
}
}
)
);
column = new ColumnImpl(
implicitName.getText(),
false,
true,
true,
true,
"",
"",
0
);
}
columnOverride.put(
path + "." + AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME,
new Column[] { column }
);
final Column offsetColumn;
final TimeZoneColumn timeZoneColumn = element.getAnnotation( TimeZoneColumn.class );
if ( timeZoneColumn != null ) {
offsetColumn = new ColumnImpl(
timeZoneColumn.name(),
false,
column.nullable(),
timeZoneColumn.insertable(),
timeZoneColumn.updatable(),
timeZoneColumn.columnDefinition(),
timeZoneColumn.table(),
0
);
}
else {
offsetColumn = new ColumnImpl(
column.name() + "_tz",
false,
column.nullable(),
column.insertable(),
column.updatable(),
"",
column.table(),
0
);
}
columnOverride.put(
path + "." + AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME,
new Column[] { offsetColumn }
);
break;
}
}
}
}
return columnOverride;
}
@ -574,4 +672,90 @@ public abstract class AbstractPropertyHolder implements PropertyHolder {
public void setParentProperty(String parentProperty) {
throw new AssertionFailure( "Setting the parent property to a non component" );
}
private static class ColumnImpl implements Column {
private final String name;
private final boolean unique;
private final boolean nullable;
private final boolean insertable;
private final boolean updatable;
private final String columnDefinition;
private final String table;
private final int precision;
private ColumnImpl(
String name,
boolean unique,
boolean nullable,
boolean insertable,
boolean updatable,
String columnDefinition,
String table,
int precision) {
this.name = name;
this.unique = unique;
this.nullable = nullable;
this.insertable = insertable;
this.updatable = updatable;
this.columnDefinition = columnDefinition;
this.table = table;
this.precision = precision;
}
@Override
public String name() {
return name;
}
@Override
public boolean unique() {
return unique;
}
@Override
public boolean nullable() {
return nullable;
}
@Override
public boolean insertable() {
return insertable;
}
@Override
public boolean updatable() {
return updatable;
}
@Override
public String columnDefinition() {
return columnDefinition;
}
@Override
public String table() {
return table;
}
@Override
public int length() {
return 255;
}
@Override
public int precision() {
return precision;
}
@Override
public int scale() {
return 0;
}
@Override
public Class<? extends Annotation> annotationType() {
return Column.class;
}
}
}

View File

@ -12,6 +12,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -30,6 +31,7 @@ import org.hibernate.AssertionFailure;
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.TimeZoneStorageStrategy;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.Cascade;
@ -89,6 +91,7 @@ import org.hibernate.annotations.Proxy;
import org.hibernate.annotations.SortComparator;
import org.hibernate.annotations.SortNatural;
import org.hibernate.annotations.Source;
import org.hibernate.annotations.TimeZoneStorage;
import org.hibernate.annotations.Where;
import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
@ -156,6 +159,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.CompositeUserType;
import org.hibernate.usertype.UserType;
import org.hibernate.usertype.internal.OffsetDateTimeCompositeUserType;
import org.hibernate.usertype.internal.ZonedDateTimeCompositeUserType;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.AttributeOverride;
@ -2896,6 +2901,14 @@ public final class AnnotationBinder {
if ( compositeType != null ) {
return compositeType.value();
}
Class<? extends CompositeUserType<?>> compositeUserType = resolveTimeZoneStorageCompositeUserType(
property,
returnedClass,
context
);
if ( compositeUserType != null ) {
return compositeUserType;
}
}
if ( returnedClass != null ) {
@ -2910,6 +2923,31 @@ public final class AnnotationBinder {
return null;
}
protected static Class<? extends CompositeUserType<?>> resolveTimeZoneStorageCompositeUserType(
XProperty property,
XClass returnedClass,
MetadataBuildingContext context) {
if ( property != null ) {
final TimeZoneStorage timeZoneStorage = property.getAnnotation( TimeZoneStorage.class );
if ( timeZoneStorage != null ) {
switch ( timeZoneStorage.value() ) {
case AUTO:
if ( context.getBuildingOptions().getDefaultTimeZoneStorage() != TimeZoneStorageStrategy.COLUMN ) {
return null;
}
case COLUMN:
switch ( returnedClass.getName() ) {
case "java.time.OffsetDateTime":
return OffsetDateTimeCompositeUserType.class;
case "java.time.ZonedDateTime":
return ZonedDateTimeCompositeUserType.class;
}
}
}
}
return null;
}
private static boolean isGlobalGeneratorNameGlobal(MetadataBuildingContext context) {
return context.getBootstrapContext().getJpaCompliance().isGlobalGeneratorScopeEnabled();
}

View File

@ -44,6 +44,7 @@ import org.hibernate.annotations.Mutability;
import org.hibernate.annotations.Nationalized;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Target;
import org.hibernate.annotations.TimeZoneColumn;
import org.hibernate.annotations.TimeZoneStorage;
import org.hibernate.annotations.TimeZoneStorageType;
import org.hibernate.annotations.common.reflection.XClass;
@ -178,6 +179,8 @@ public class BasicValueBinder implements JdbcTypeIndicators {
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
if ( timeZoneStorageType != null ) {
switch ( timeZoneStorageType ) {
case COLUMN:
return TimeZoneStorageStrategy.COLUMN;
case NATIVE:
return TimeZoneStorageStrategy.NATIVE;
case NORMALIZE:
@ -656,7 +659,22 @@ public class BasicValueBinder implements JdbcTypeIndicators {
}
final TimeZoneStorage timeZoneStorageAnn = attributeXProperty.getAnnotation( TimeZoneStorage.class );
timeZoneStorageType = timeZoneStorageAnn != null ? timeZoneStorageAnn.value() : null;
if ( timeZoneStorageAnn != null ) {
timeZoneStorageType = timeZoneStorageAnn.value();
final TimeZoneColumn timeZoneColumnAnn = attributeXProperty.getAnnotation( TimeZoneColumn.class );
if ( timeZoneColumnAnn != null ) {
if ( timeZoneStorageType != TimeZoneStorageType.AUTO && timeZoneStorageType != TimeZoneStorageType.COLUMN ) {
throw new IllegalStateException(
"@TimeZoneColumn can not be used in conjunction with @TimeZoneStorage( " + timeZoneStorageType +
" ) with attribute " + attributeXProperty.getDeclaringClass().getName() +
'.' + attributeXProperty.getName()
);
}
}
}
else {
timeZoneStorageType = null;
}
normalSupplementalDetails( attributeXProperty);

View File

@ -19,6 +19,7 @@ import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.FormatFunction;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.OffsetFetchLimitHandler;
import org.hibernate.dialect.sequence.PostgreSQLSequenceSupport;
@ -230,11 +231,10 @@ public class CockroachDialect extends Dialect {
functionFactory.pi();
functionFactory.trunc(); //TODO: emulate second arg
queryEngine.getSqmFunctionRegistry().namedDescriptorBuilder("format", "experimental_strftime")
.setInvariantType( stringType )
.setArgumentsValidator( CommonFunctionFactory.formatValidator() )
.setArgumentListSignature("(TEMPORAL datetime as STRING pattern)")
.register();
queryEngine.getSqmFunctionRegistry().register(
"format",
new FormatFunction( "experimental_strftime", queryEngine.getTypeConfiguration() )
);
functionFactory.windowFunctions();
functionFactory.listagg_stringAgg( "string" );
functionFactory.inverseDistributionOrderedSetAggregates();

View File

@ -287,8 +287,10 @@ public class DB2Dialect extends Dialect {
)
);
queryEngine.getSqmFunctionRegistry().register( "format",
new DB2FormatEmulation( queryEngine.getTypeConfiguration() ) );
queryEngine.getSqmFunctionRegistry().register(
"format",
new DB2FormatEmulation( queryEngine.getTypeConfiguration() )
);
queryEngine.getSqmFunctionRegistry().namedDescriptorBuilder( "posstr" )
.setInvariantType(

View File

@ -63,13 +63,14 @@ public class HSQLSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAs
@Override
protected void visitAnsiCaseSearchedExpression(
CaseSearchedExpression caseSearchedExpression,
CaseSearchedExpression expression,
Consumer<Expression> resultRenderer) {
if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSearchedExpression ) ) {
final List<CaseSearchedExpression.WhenFragment> whenFragments = caseSearchedExpression.getWhenFragments();
if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression )
|| areAllResultsPlainParametersOrLiterals( expression ) ) {
final List<CaseSearchedExpression.WhenFragment> whenFragments = expression.getWhenFragments();
final Expression firstResult = whenFragments.get( 0 ).getResult();
super.visitAnsiCaseSearchedExpression(
caseSearchedExpression,
expression,
e -> {
if ( e == firstResult ) {
renderCasted( e );
@ -81,19 +82,20 @@ public class HSQLSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAs
);
}
else {
super.visitAnsiCaseSearchedExpression( caseSearchedExpression, resultRenderer );
super.visitAnsiCaseSearchedExpression( expression, resultRenderer );
}
}
@Override
protected void visitAnsiCaseSimpleExpression(
CaseSimpleExpression caseSimpleExpression,
CaseSimpleExpression expression,
Consumer<Expression> resultRenderer) {
if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSimpleExpression ) ) {
final List<CaseSimpleExpression.WhenFragment> whenFragments = caseSimpleExpression.getWhenFragments();
if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression )
|| areAllResultsPlainParametersOrLiterals( expression ) ) {
final List<CaseSimpleExpression.WhenFragment> whenFragments = expression.getWhenFragments();
final Expression firstResult = whenFragments.get( 0 ).getResult();
super.visitAnsiCaseSimpleExpression(
caseSimpleExpression,
expression,
e -> {
if ( e == firstResult ) {
renderCasted( e );
@ -105,10 +107,52 @@ public class HSQLSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAs
);
}
else {
super.visitAnsiCaseSimpleExpression( caseSimpleExpression, resultRenderer );
super.visitAnsiCaseSimpleExpression( expression, resultRenderer );
}
}
protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) {
final List<CaseSearchedExpression.WhenFragment> whenFragments = caseSearchedExpression.getWhenFragments();
final Expression firstResult = whenFragments.get( 0 ).getResult();
if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT
|| isLiteral( firstResult ) ) {
for ( int i = 1; i < whenFragments.size(); i++ ) {
final Expression result = whenFragments.get( i ).getResult();
if ( isParameter( result ) ) {
if ( getParameterRenderingMode() != SqlAstNodeRenderingMode.DEFAULT ) {
return false;
}
}
else if ( !isLiteral( result ) ) {
return false;
}
}
return true;
}
return false;
}
protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) {
final List<CaseSimpleExpression.WhenFragment> whenFragments = caseSimpleExpression.getWhenFragments();
final Expression firstResult = whenFragments.get( 0 ).getResult();
if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT
|| isLiteral( firstResult ) ) {
for ( int i = 1; i < whenFragments.size(); i++ ) {
final Expression result = whenFragments.get( i ).getResult();
if ( isParameter( result ) ) {
if ( getParameterRenderingMode() != SqlAstNodeRenderingMode.DEFAULT ) {
return false;
}
}
else if ( !isLiteral( result ) ) {
return false;
}
}
return true;
}
return false;
}
@Override
public boolean supportsFilterClause() {
return true;

View File

@ -1014,6 +1014,12 @@ public class MySQLDialect extends Dialect {
return getMySQLVersion().isBefore( 5, 5 ) ? MyISAMStorageEngine.INSTANCE : InnoDBStorageEngine.INSTANCE;
}
@Override
public TimeZoneSupport getTimeZoneSupport() {
// In MySQL and MariaDB, the TIMESTAMP type normalize to UTC just like PostgreSQL
return TimeZoneSupport.NORMALIZE;
}
@Override
public void appendLiteral(SqlAppender appender, String literal) {
appender.appendSql( '\'' );

View File

@ -8,6 +8,7 @@ package org.hibernate.dialect;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@ -15,6 +16,7 @@ import java.sql.SQLException;
import java.sql.Types;
import java.time.Duration;
import org.hibernate.HibernateException;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.ValueBinder;
@ -34,12 +36,27 @@ import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
public class PostgreSQLIntervalSecondJdbcType implements AdjustableJdbcType {
public static final PostgreSQLIntervalSecondJdbcType INSTANCE = new PostgreSQLIntervalSecondJdbcType();
private static final Class<?> PG_INTERVAL_CLASS;
private static final Constructor<Object> PG_INTERVAL_CONSTRUCTOR;
private static final Method PG_INTERVAL_GET_DAYS;
private static final Method PG_INTERVAL_GET_HOURS;
private static final Method PG_INTERVAL_GET_MINUTES;
private static final Method PG_INTERVAL_GET_SECONDS;
private static final Method PG_INTERVAL_GET_MICRO_SECONDS;
private static final long SECONDS_PER_DAY = 86400;
private static final long SECONDS_PER_HOUR = 3600;
private static final long SECONDS_PER_MINUTE = 60;
static {
Constructor<Object> constructor;
Class<?> pgIntervalClass;
Method pgIntervalGetDays;
Method pgIntervalGetHours;
Method pgIntervalGetMinutes;
Method pgIntervalGetSeconds;
Method pgIntervalGetMicroSeconds;
try {
final Class<?> pgIntervalClass = ReflectHelper.classForName(
pgIntervalClass = ReflectHelper.classForName(
"org.postgresql.util.PGInterval",
PostgreSQLIntervalSecondJdbcType.class
);
@ -51,11 +68,22 @@ public class PostgreSQLIntervalSecondJdbcType implements AdjustableJdbcType {
int.class,
double.class
);
pgIntervalGetDays = pgIntervalClass.getDeclaredMethod( "getDays" );
pgIntervalGetHours = pgIntervalClass.getDeclaredMethod( "getHours" );
pgIntervalGetMinutes = pgIntervalClass.getDeclaredMethod( "getMinutes" );
pgIntervalGetSeconds = pgIntervalClass.getDeclaredMethod( "getWholeSeconds" );
pgIntervalGetMicroSeconds = pgIntervalClass.getDeclaredMethod( "getMicroSeconds" );
}
catch (Exception e) {
throw new RuntimeException( "Could not initialize PostgreSQLPGObjectJdbcType", e );
}
PG_INTERVAL_CLASS = pgIntervalClass;
PG_INTERVAL_CONSTRUCTOR = constructor;
PG_INTERVAL_GET_DAYS = pgIntervalGetDays;
PG_INTERVAL_GET_HOURS = pgIntervalGetHours;
PG_INTERVAL_GET_MINUTES = pgIntervalGetMinutes;
PG_INTERVAL_GET_SECONDS = pgIntervalGetSeconds;
PG_INTERVAL_GET_MICRO_SECONDS = pgIntervalGetMicroSeconds;
}
@Override
@ -139,18 +167,36 @@ public class PostgreSQLIntervalSecondJdbcType implements AdjustableJdbcType {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
return getJavaType().wrap( rs.getString( paramIndex ), options );
return getJavaType().wrap( getValue( rs.getObject( paramIndex ) ), options );
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getString( index ), options );
return getJavaType().wrap( getValue( statement.getObject( index ) ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
throws SQLException {
return getJavaType().wrap( statement.getString( name ), options );
return getJavaType().wrap( getValue( statement.getObject( name ) ), options );
}
private Object getValue(Object value) {
if ( PG_INTERVAL_CLASS.isInstance( value ) ) {
try {
final long seconds = (int) PG_INTERVAL_GET_SECONDS.invoke( value )
+ SECONDS_PER_DAY * (int) PG_INTERVAL_GET_DAYS.invoke( value )
+ SECONDS_PER_HOUR * (int) PG_INTERVAL_GET_HOURS.invoke( value )
+ SECONDS_PER_MINUTE * (int) PG_INTERVAL_GET_MINUTES.invoke( value );
final long nanos = 1000L * (int) PG_INTERVAL_GET_MICRO_SECONDS.invoke( value );
return Duration.ofSeconds( seconds, nanos );
}
catch (Exception e) {
throw new HibernateException( "Couldn't create Duration from interval", e );
}
}
return value;
}
};
}

View File

@ -9,6 +9,8 @@ package org.hibernate.dialect;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.internal.util.StringHelper;
/**
* @author Gavin King
*/
@ -47,7 +49,7 @@ public class Replacer {
public Replacer(String format, String quote, String delimiter) {
this.delimiter = delimiter;
this.chunks = format.split( quote );
this.chunks = StringHelper.splitFull( quote, format );
this.quote = quote;
}

View File

@ -250,7 +250,10 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
}
if ( getVersion().isSameOrAfter( 11 ) ) {
queryEngine.getSqmFunctionRegistry().register( "format", new SQLServerFormatEmulation( this, queryEngine.getTypeConfiguration() ) );
queryEngine.getSqmFunctionRegistry().register(
"format",
new SQLServerFormatEmulation( this, queryEngine.getTypeConfiguration() )
);
//actually translate() was added in 2017 but
//it's not worth adding a new dialect for that!

View File

@ -17,6 +17,7 @@ import org.hibernate.boot.model.relational.Exportable;
import org.hibernate.boot.model.relational.Sequence;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.FormatFunction;
import org.hibernate.dialect.lock.LockingStrategy;
import org.hibernate.dialect.lock.LockingStrategyException;
import org.hibernate.dialect.pagination.LimitHandler;
@ -434,11 +435,10 @@ public class SpannerDialect extends Dialect {
.setExactArgumentCount( 1 )
.register();
queryEngine.getSqmFunctionRegistry().patternDescriptorBuilder("format", "format_timestamp(?2,?1)")
.setInvariantType( stringType )
.setArgumentsValidator( CommonFunctionFactory.formatValidator() )
.setArgumentListSignature("(TIMESTAMP datetime as STRING pattern)")
.register();
queryEngine.getSqmFunctionRegistry().register(
"format",
new FormatFunction( "format_timestamp", true, queryEngine.getTypeConfiguration() )
);
functionFactory.listagg_stringAgg( "string" );
functionFactory.inverseDistributionOrderedSetAggregates();
functionFactory.hypotheticalOrderedSetAggregates();

View File

@ -2212,12 +2212,7 @@ public class CommonFunctionFactory {
* H2-style (uses Java's SimpleDateFormat directly so no need to translate format)
*/
public void format_formatdatetime() {
functionRegistry.namedDescriptorBuilder( "format", "formatdatetime" )
.setInvariantType(stringType)
.setParameterTypes( TEMPORAL, STRING )
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
functionRegistry.register( "format", new FormatFunction( "formatdatetime", typeConfiguration ) );
}
/**
@ -2226,12 +2221,7 @@ public class CommonFunctionFactory {
* @see org.hibernate.dialect.OracleDialect#datetimeFormat
*/
public void format_toChar() {
functionRegistry.namedDescriptorBuilder( "format", "to_char" )
.setInvariantType(stringType)
.setParameterTypes( TEMPORAL, STRING )
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
functionRegistry.register( "format", new FormatFunction( "to_char", typeConfiguration ) );
}
/**
@ -2240,12 +2230,7 @@ public class CommonFunctionFactory {
* @see org.hibernate.dialect.MySQLDialect#datetimeFormat
*/
public void format_dateFormat() {
functionRegistry.namedDescriptorBuilder( "format", "date_format" )
.setInvariantType(stringType)
.setParameterTypes( TEMPORAL, STRING )
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
functionRegistry.register( "format", new FormatFunction( "date_format", typeConfiguration ) );
}
/**
@ -2254,16 +2239,7 @@ public class CommonFunctionFactory {
* @see org.hibernate.dialect.OracleDialect#datetimeFormat
*/
public void format_toVarchar() {
functionRegistry.namedDescriptorBuilder( "format", "to_varchar" )
.setInvariantType(stringType)
.setParameterTypes( TEMPORAL, STRING )
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
}
public static ArgumentsValidator formatValidator() {
return new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING );
functionRegistry.register( "format", new FormatFunction( "to_varchar", typeConfiguration ) );
}
/**

View File

@ -6,24 +6,18 @@
*/
package org.hibernate.dialect.function;
import java.util.List;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
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.sql.ast.tree.expression.Format;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.List;
import jakarta.persistence.TemporalType;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
/**
* DB2's varchar_format() can't handle quoted literal strings in
* the format pattern. So just split the pattern into bits, call
@ -33,16 +27,12 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEM
* @author Gavin King
*/
public class DB2FormatEmulation
extends AbstractSqmSelfRenderingFunctionDescriptor {
extends FormatFunction {
public DB2FormatEmulation(TypeConfiguration typeConfiguration) {
super(
"format",
CommonFunctionFactory.formatValidator(),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TEMPORAL, STRING )
typeConfiguration
);
}
@ -94,9 +84,4 @@ public class DB2FormatEmulation
}
sqlAppender.appendSql(")");
}
@Override
public String getArgumentListSignature() {
return "(TEMPORAL datetime as STRING pattern)";
}
}

View File

@ -13,17 +13,26 @@ import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.NodeBuilder;
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.produce.function.internal.PatternRenderer;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.query.sqm.tree.expression.*;
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.ExtractUnit;
import org.hibernate.type.BasicType;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import static java.util.Arrays.asList;
@ -31,7 +40,6 @@ import static org.hibernate.query.sqm.BinaryArithmeticOperator.*;
import static org.hibernate.query.sqm.TemporalUnit.*;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL_UNIT;
import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useArgType;
/**
* ANSI SQL-inspired {@code extract()} function, where the date/time fields
@ -41,7 +49,7 @@ import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTyp
* @author Gavin King
*/
public class ExtractFunction
extends AbstractSqmFunctionDescriptor {
extends AbstractSqmFunctionDescriptor implements FunctionRenderingSupport {
private final Dialect dialect;
@ -58,14 +66,27 @@ public class ExtractFunction
this.dialect = dialect;
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final ExtractUnit field = (ExtractUnit) sqlAstArguments.get( 0 );
final TemporalUnit unit = field.getUnit();
final String pattern = dialect.extractPattern( unit );
new PatternRenderer( pattern ).render( sqlAppender, sqlAstArguments, walker );
}
@Override
protected <T> SelfRenderingSqmFunction generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine,
TypeConfiguration typeConfiguration) {
SqmExtractUnit<?> field = (SqmExtractUnit<?>) arguments.get(0);
SqmExpression<?> expression = (SqmExpression<?>) arguments.get(1);
final SqmExtractUnit<?> field = (SqmExtractUnit<?>) arguments.get( 0 );
final SqmExpression<?> originalExpression = (SqmExpression<?>) arguments.get( 1 );
final boolean compositeTemporal = SqmExpressionHelper.isCompositeTemporal( originalExpression );
final SqmExpression<?> expression = SqmExpressionHelper.getOffsetAdjustedExpression( originalExpression );
TemporalUnit unit = field.getUnit();
switch ( unit ) {
@ -74,8 +95,27 @@ public class ExtractFunction
case NATIVE:
throw new SemanticException("can't extract() the field TemporalUnit.NATIVE");
case OFFSET:
// use format(arg, 'xxx') to get the offset
return extractOffsetUsingFormat( expression, queryEngine, typeConfiguration );
if ( compositeTemporal ) {
final SqmPath<Object> offsetPath = ( (SqmPath<?>) originalExpression ).get(
AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME
);
return new SelfRenderingSqmFunction<>(
this,
(sqlAppender, sqlAstArguments, walker) -> {
sqlAstArguments.get( 0 ).accept( walker );
},
Collections.singletonList( offsetPath ),
null,
null,
StandardFunctionReturnTypeResolvers.useArgType( 1 ),
expression.nodeBuilder(),
"extract"
);
}
else {
// use format(arg, 'xxx') to get the offset
return extractOffsetUsingFormat( expression, queryEngine, typeConfiguration );
}
case DATE:
case TIME:
// use cast(arg as Type) to get the date or time part
@ -93,18 +133,16 @@ public class ExtractFunction
// otherwise it's something we expect the SQL dialect
// itself to understand, either natively, or via the
// method Dialect.extract()
String pattern = dialect.extractPattern( unit );
return queryEngine.getSqmFunctionRegistry()
.patternDescriptorBuilder( "extract", pattern )
.setExactArgumentCount( 2 )
.setReturnTypeResolver( useArgType( 1 ) )
.descriptor()
.generateSqmExpression(
arguments,
impliedResultType,
queryEngine,
typeConfiguration
);
return new SelfRenderingSqmFunction(
this,
this,
expression == originalExpression ? arguments : List.of( arguments.get( 0 ), expression ),
impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
expression.nodeBuilder(),
"extract"
);
}
}

View File

@ -0,0 +1,763 @@
/*
* 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.Dialect;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.BinaryArithmeticOperator;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionRenderingSupport;
import org.hibernate.query.sqm.function.MultipatternSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression;
import org.hibernate.query.sqm.function.SelfRenderingSqmFunction;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
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.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmTypedNode;
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.BinaryArithmeticExpression;
import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.DurationUnit;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.Format;
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.predicate.BetweenPredicate;
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
import org.hibernate.type.BasicType;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
/**
* A format function with support for composite temporal expressions.
*
* @author Christian Beikov
*/
public class FormatFunction extends AbstractSqmFunctionDescriptor implements FunctionRenderingSupport {
private final String nativeFunctionName;
private final boolean reversedArguments;
public FormatFunction(String nativeFunctionName, TypeConfiguration typeConfiguration) {
this( nativeFunctionName, false, typeConfiguration );
}
public FormatFunction(String nativeFunctionName, boolean reversedArguments, TypeConfiguration typeConfiguration) {
super(
"format",
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING ),
StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.getBasicTypeRegistry().resolve(
StandardBasicTypes.STRING ) ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TEMPORAL, STRING )
);
this.nativeFunctionName = nativeFunctionName;
this.reversedArguments = reversedArguments;
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( nativeFunctionName );
sqlAppender.append( '(' );
final SqlAstNode expression = sqlAstArguments.get( 0 );
final SqlAstNode format = sqlAstArguments.get( 1 );
if ( reversedArguments ) {
format.accept( walker );
sqlAppender.append( ',' );
expression.accept( walker );
}
else {
expression.accept( walker );
sqlAppender.append( ',' );
format.accept( walker );
}
sqlAppender.append( ')' );
}
@Override
protected <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine,
TypeConfiguration typeConfiguration) {
return new SelfRenderingSqmFunction<>(
this,
this,
arguments,
impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
queryEngine.getCriteriaBuilder(),
"format"
) {
@Override
public Expression convertToSqlAst(SqmToSqlAstConverter walker) {
final ReturnableType<?> resultType = resolveResultType(
walker.getCreationContext().getMappingMetamodel().getTypeConfiguration()
);
final List<SqlAstNode> arguments = resolveSqlAstArguments( getArguments(), walker );
final SqlAstNode expression = arguments.get( 0 );
if ( expression instanceof SqlTupleContainer ) {
// SqlTupleContainer means this is a composite temporal type i.e. uses `@TimeZoneStorage(COLUMN)`
// The support for this kind of type requires that we inject the offset from the second column
// as literal into the pattern, and apply the formatting on the date time part
final SqlTuple sqlTuple = ( (SqlTupleContainer) expression ).getSqlTuple();
final AbstractSqmSelfRenderingFunctionDescriptor timestampaddFunction = getFunction(
walker,
"timestampadd"
);
final BasicType<Integer> integerType = typeConfiguration.getBasicTypeRegistry()
.resolve( StandardBasicTypes.INTEGER );
arguments.set( 0, getOffsetAdjusted( sqlTuple, timestampaddFunction, integerType ) );
final Format format = (Format) arguments.get( 1 );
// If the format contains a time zone or offset, we must replace that with the offset column
if ( format.getFormat().contains( "x" ) ) {
final AbstractSqmSelfRenderingFunctionDescriptor concatFunction = getFunction(
walker,
"concat"
);
final AbstractSqmSelfRenderingFunctionDescriptor substringFunction = getFunction(
walker,
"substring",
3
);
final AbstractSqmSelfRenderingFunctionDescriptor floorFunction = getFunction( walker, "floor" );
final AbstractSqmSelfRenderingFunctionDescriptor castFunction = getFunction( walker, "cast" );
final BasicType<String> stringType = typeConfiguration.getBasicTypeRegistry()
.resolve( StandardBasicTypes.STRING );
final Dialect dialect = walker.getCreationContext()
.getSessionFactory()
.getJdbcServices()
.getDialect();
Expression formatExpression = null;
final StringBuilder sb = new StringBuilder();
dialect.appendDatetimeFormat( sb::append, "'a'" );
final String delimiter = sb.substring( 0, sb.indexOf( "a" ) ).replace( "''", "'" );
final String[] chunks = StringHelper.splitFull( "'", format.getFormat() );
final Expression offsetExpression = sqlTuple.getExpressions().get( 1 );
// Splitting by `'` will put actual format pattern parts to even indices and literal pattern parts
// to uneven indices. We will only replace the time zone and offset pattern in the format pattern parts
for ( int i = 0; i < chunks.length; i += 2 ) {
// The general idea is to replace the various patterns `xxx`, `xx` and `x` by concatenating
// the offset column as literal i.e. `HH:mmxxx` is translated to `HH:mm'''||offset||'''`
// xxx stands for the full offset i.e. `+01:00`
// xx stands for the medium offset i.e. `+0100`
// x stands for the small offset i.e. `+01`
final String[] fullParts = StringHelper.splitFull( "xxx", chunks[i] );
for ( int j = 0; j < fullParts.length; j++ ) {
if ( fullParts[j].isEmpty() ) {
continue;
}
final String[] mediumParts = StringHelper.splitFull( "xx", fullParts[j] );
for ( int k = 0; k < mediumParts.length; k++ ) {
if ( mediumParts[k].isEmpty() ) {
continue;
}
final String[] smallParts = StringHelper.splitFull( "x", mediumParts[k] );
for ( int l = 0; l < smallParts.length; l++ ) {
if ( smallParts[l].isEmpty() ) {
continue;
}
sb.setLength( 0 );
dialect.appendDatetimeFormat( sb::append, smallParts[l] );
final String formatPart = sb.toString();
formatExpression = concat(
concatFunction,
stringType,
formatExpression,
new QueryLiteral<>( formatPart, stringType )
);
if ( l + 1 < smallParts.length ) {
// This is for `x` patterns, which require `+01`
// so we concat `substring(offset, 1, 4)`
// Since the offset is always in the full format
formatExpression = concatAsLiteral(
concatFunction,
stringType,
delimiter,
formatExpression,
createSmallOffset(
concatFunction,
substringFunction,
floorFunction,
castFunction,
stringType,
integerType,
offsetExpression
)
);
}
}
if ( k + 1 < mediumParts.length ) {
// This is for `xx` patterns, which require `+0100`
// so we concat `substring(offset, 1, 4)||substring(offset, 4, 6)`
// Since the offset is always in the full format
formatExpression = concatAsLiteral(
concatFunction,
stringType,
delimiter, formatExpression,
createMediumOffset(
concatFunction,
substringFunction,
floorFunction,
castFunction,
stringType,
integerType,
offsetExpression
)
);
}
}
if ( j + 1 < fullParts.length ) {
formatExpression = concatAsLiteral(
concatFunction,
stringType,
delimiter, formatExpression,
createFullOffset(
concatFunction,
floorFunction,
castFunction,
stringType,
integerType,
offsetExpression
)
);
}
}
if ( i + 1 < chunks.length ) {
// Handle the pattern literal content
sb.setLength( 0 );
dialect.appendDatetimeFormat( sb::append, "'" + chunks[i + 1] + "'" );
final String formatLiteralPart = sb.toString().replace( "''", "'" );
formatExpression = concat(
concatFunction,
stringType,
formatExpression,
new QueryLiteral<>(
formatLiteralPart,
stringType
)
);
}
}
arguments.set( 1, formatExpression );
}
}
if ( getArgumentsValidator() != null ) {
getArgumentsValidator().validateSqlTypes( arguments, getFunctionName() );
}
return new SelfRenderingFunctionSqlAstExpression(
getFunctionName(),
getRenderingSupport(),
arguments,
resultType,
resultType == null ? null : getMappingModelExpressible( walker, resultType )
);
}
private AbstractSqmSelfRenderingFunctionDescriptor getFunction(SqmToSqlAstConverter walker, String name) {
return (AbstractSqmSelfRenderingFunctionDescriptor) walker.getCreationContext()
.getSessionFactory()
.getQueryEngine()
.getSqmFunctionRegistry()
.findFunctionDescriptor( name );
}
private AbstractSqmSelfRenderingFunctionDescriptor getFunction(SqmToSqlAstConverter walker, String name, int argumentCount) {
final SqmFunctionDescriptor functionDescriptor = walker.getCreationContext()
.getSessionFactory()
.getQueryEngine()
.getSqmFunctionRegistry()
.findFunctionDescriptor( name );
if ( functionDescriptor instanceof MultipatternSqmFunctionDescriptor ) {
return (AbstractSqmSelfRenderingFunctionDescriptor) ( (MultipatternSqmFunctionDescriptor) functionDescriptor )
.getFunction( argumentCount );
}
return (AbstractSqmSelfRenderingFunctionDescriptor) functionDescriptor;
}
private SqlAstNode getOffsetAdjusted(
SqlTuple sqlTuple,
AbstractSqmSelfRenderingFunctionDescriptor timestampaddFunction,
BasicType<Integer> integerType) {
final Expression instantExpression = sqlTuple.getExpressions().get( 0 );
final Expression offsetExpression = sqlTuple.getExpressions().get( 1 );
return new SelfRenderingFunctionSqlAstExpression(
"timestampadd",
timestampaddFunction,
List.of(
new DurationUnit( TemporalUnit.SECOND, integerType ),
offsetExpression,
instantExpression
),
(ReturnableType<?>) instantExpression.getExpressionType(),
instantExpression.getExpressionType()
);
}
private Expression createFullOffset(
AbstractSqmSelfRenderingFunctionDescriptor concatFunction,
AbstractSqmSelfRenderingFunctionDescriptor floorFunction,
AbstractSqmSelfRenderingFunctionDescriptor castFunction,
BasicType<String> stringType,
BasicType<Integer> integerType,
Expression offsetExpression) {
if ( offsetExpression.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType().isString() ) {
return offsetExpression;
}
else {
// ZoneOffset as seconds
final CaseSearchedExpression caseSearchedExpression = new CaseSearchedExpression( stringType );
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN_OR_EQUAL,
new QueryLiteral<>(
-36000,
integerType
)
),
new QueryLiteral<>( "-", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN,
new QueryLiteral<>(
0,
integerType
)
),
new QueryLiteral<>( "-0", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.GREATER_THAN_OR_EQUAL,
new QueryLiteral<>(
36000,
integerType
)
),
new QueryLiteral<>( "+", stringType )
)
);
caseSearchedExpression.otherwise( new QueryLiteral<>( "+0", stringType ) );
final Expression hours = getHours( floorFunction, castFunction, integerType, offsetExpression );
final Expression minutes = getMinutes( floorFunction, castFunction, integerType, offsetExpression );
final CaseSearchedExpression minuteStart = new CaseSearchedExpression( stringType );
minuteStart.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new BetweenPredicate(
minutes,
new QueryLiteral<>(
-9,
integerType
),
new QueryLiteral<>(
9,
integerType
),
false,
null
),
new QueryLiteral<>( ":0", stringType )
)
);
minuteStart.otherwise( new QueryLiteral<>( ":", stringType ) );
return concat(
concatFunction,
stringType,
concat(
concatFunction,
stringType,
concat( concatFunction, stringType, caseSearchedExpression, hours ),
minuteStart
),
minutes
);
}
}
private Expression createMediumOffset(
AbstractSqmSelfRenderingFunctionDescriptor concatFunction,
AbstractSqmSelfRenderingFunctionDescriptor substringFunction,
AbstractSqmSelfRenderingFunctionDescriptor floorFunction,
AbstractSqmSelfRenderingFunctionDescriptor castFunction,
BasicType<String> stringType,
BasicType<Integer> integerType,
Expression offsetExpression) {
if ( offsetExpression.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType().isString() ) {
return concat(
concatFunction,
stringType,
createSmallOffset(
concatFunction,
substringFunction,
floorFunction,
castFunction,
stringType,
integerType,
offsetExpression
),
new SelfRenderingFunctionSqlAstExpression(
"substring",
substringFunction,
List.of(
offsetExpression,
new QueryLiteral<>( 4, integerType ),
new QueryLiteral<>( 6, integerType )
),
stringType,
stringType
)
);
}
else {
// ZoneOffset as seconds
final CaseSearchedExpression caseSearchedExpression = new CaseSearchedExpression( stringType );
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN_OR_EQUAL,
new QueryLiteral<>(
-36000,
integerType
)
),
new QueryLiteral<>( "-", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN,
new QueryLiteral<>(
0,
integerType
)
),
new QueryLiteral<>( "-0", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.GREATER_THAN_OR_EQUAL,
new QueryLiteral<>(
36000,
integerType
)
),
new QueryLiteral<>( "+", stringType )
)
);
caseSearchedExpression.otherwise( new QueryLiteral<>( "+0", stringType ) );
final Expression hours = getHours( floorFunction, castFunction, integerType, offsetExpression );
final Expression minutes = getMinutes( floorFunction, castFunction, integerType, offsetExpression );
final CaseSearchedExpression minuteStart = new CaseSearchedExpression( stringType );
minuteStart.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new BetweenPredicate(
minutes,
new QueryLiteral<>(
-9,
integerType
),
new QueryLiteral<>(
9,
integerType
),
false,
null
),
new QueryLiteral<>( "0", stringType )
)
);
minuteStart.otherwise( new QueryLiteral<>( "", stringType ) );
return concat(
concatFunction,
stringType,
concat(
concatFunction,
stringType,
concat( concatFunction, stringType, caseSearchedExpression, hours ),
minuteStart
),
minutes
);
}
}
private Expression createSmallOffset(
AbstractSqmSelfRenderingFunctionDescriptor concatFunction,
AbstractSqmSelfRenderingFunctionDescriptor substringFunction,
AbstractSqmSelfRenderingFunctionDescriptor floorFunction,
AbstractSqmSelfRenderingFunctionDescriptor castFunction,
BasicType<String> stringType,
BasicType<Integer> integerType,
Expression offsetExpression) {
if ( offsetExpression.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType().isString() ) {
return new SelfRenderingFunctionSqlAstExpression(
"substring",
substringFunction,
List.of(
offsetExpression,
new QueryLiteral<>( 1, integerType ),
new QueryLiteral<>( 4, integerType )
),
stringType,
stringType
);
}
else {
// ZoneOffset as seconds
final CaseSearchedExpression caseSearchedExpression = new CaseSearchedExpression( stringType );
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN_OR_EQUAL,
new QueryLiteral<>(
-36000,
integerType
)
),
new QueryLiteral<>( "-", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.LESS_THAN,
new QueryLiteral<>(
0,
integerType
)
),
new QueryLiteral<>( "-0", stringType )
)
);
caseSearchedExpression.getWhenFragments().add(
new CaseSearchedExpression.WhenFragment(
new ComparisonPredicate(
offsetExpression,
ComparisonOperator.GREATER_THAN_OR_EQUAL,
new QueryLiteral<>(
36000,
integerType
)
),
new QueryLiteral<>( "+", stringType )
)
);
caseSearchedExpression.otherwise( new QueryLiteral<>( "+0", stringType ) );
final Expression hours = getHours( floorFunction, castFunction, integerType, offsetExpression );
return concat( concatFunction, stringType, caseSearchedExpression, hours );
}
}
private Expression concatAsLiteral(
AbstractSqmSelfRenderingFunctionDescriptor concatFunction,
BasicType<String> stringType,
String delimiter,
Expression expression,
Expression expression2) {
return concat(
concatFunction,
stringType,
concat(
concatFunction,
stringType,
concat(
concatFunction,
stringType,
expression,
new QueryLiteral<>( delimiter, stringType )
),
expression2
),
new QueryLiteral<>( delimiter, stringType )
);
}
private Expression concat(
AbstractSqmSelfRenderingFunctionDescriptor concatFunction,
BasicType<String> stringType,
Expression expression,
Expression expression2) {
if ( expression == null ) {
return expression2;
}
else if ( expression instanceof SelfRenderingFunctionSqlAstExpression
&& "concat".equals( ( (SelfRenderingFunctionSqlAstExpression) expression ).getFunctionName() ) ) {
List<SqlAstNode> list = (List<SqlAstNode>) ( (SelfRenderingFunctionSqlAstExpression) expression ).getArguments();
final SqlAstNode lastOperand = list.get( list.size() - 1 );
if ( expression2 instanceof QueryLiteral<?> && lastOperand instanceof QueryLiteral<?> ) {
list.set(
list.size() - 1,
new QueryLiteral<>(
( (QueryLiteral<?>) lastOperand ).getLiteralValue().toString() +
( (QueryLiteral<?>) expression2 ).getLiteralValue().toString(),
stringType
)
);
}
else {
list.add( expression2 );
}
return expression;
}
else if ( expression2 instanceof SelfRenderingFunctionSqlAstExpression
&& "concat".equals( ( (SelfRenderingFunctionSqlAstExpression) expression2 ).getFunctionName() ) ) {
List<SqlAstNode> list = (List<SqlAstNode>) ( (SelfRenderingFunctionSqlAstExpression) expression2 ).getArguments();
final SqlAstNode firstOperand = list.get( 0 );
if ( expression instanceof QueryLiteral<?> && firstOperand instanceof QueryLiteral<?> ) {
list.set(
list.size() - 1,
new QueryLiteral<>(
( (QueryLiteral<?>) expression ).getLiteralValue().toString() +
( (QueryLiteral<?>) firstOperand ).getLiteralValue().toString(),
stringType
)
);
}
else {
list.add( 0, expression );
}
return expression2;
}
else if ( expression instanceof QueryLiteral<?> && expression2 instanceof QueryLiteral<?> ) {
return new QueryLiteral<>(
( (QueryLiteral<?>) expression ).getLiteralValue().toString() +
( (QueryLiteral<?>) expression2 ).getLiteralValue().toString(),
stringType
);
}
else {
final List<Expression> list = new ArrayList<>( 2 );
list.add( expression );
list.add( expression2 );
return new SelfRenderingFunctionSqlAstExpression(
"concat",
concatFunction,
list,
stringType,
stringType
);
}
}
private Expression getHours(
AbstractSqmSelfRenderingFunctionDescriptor floorFunction,
AbstractSqmSelfRenderingFunctionDescriptor castFunction,
BasicType<Integer> integerType,
Expression offsetExpression) {
return new SelfRenderingFunctionSqlAstExpression(
"cast",
castFunction,
List.of(
new SelfRenderingFunctionSqlAstExpression(
"floor",
floorFunction,
List.of(
new BinaryArithmeticExpression(
offsetExpression,
BinaryArithmeticOperator.DIVIDE,
new QueryLiteral<>( 3600, integerType ),
integerType
)
),
integerType,
integerType
),
new CastTarget( integerType )
),
integerType,
integerType
);
}
private Expression getMinutes(
AbstractSqmSelfRenderingFunctionDescriptor floorFunction,
AbstractSqmSelfRenderingFunctionDescriptor castFunction,
BasicType<Integer> integerType,
Expression offsetExpression){
return new SelfRenderingFunctionSqlAstExpression(
"cast",
castFunction,
List.of(
new SelfRenderingFunctionSqlAstExpression(
"floor",
floorFunction,
List.of(
new BinaryArithmeticExpression(
new BinaryArithmeticExpression(
offsetExpression,
BinaryArithmeticOperator.MODULO,
new QueryLiteral<>( 3600, integerType ),
integerType
),
BinaryArithmeticOperator.DIVIDE,
new QueryLiteral<>( 60, integerType ),
integerType
)
),
integerType,
integerType
),
new CastTarget( integerType )
),
integerType,
integerType
);
}
};
}
@Override
public String getArgumentListSignature() {
return "(TEMPORAL datetime as STRING pattern)";
}
}

View File

@ -29,19 +29,12 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEM
*
* @author Christian Beikov
*/
public class SQLServerFormatEmulation extends AbstractSqmSelfRenderingFunctionDescriptor {
public class SQLServerFormatEmulation extends FormatFunction {
private final SQLServerDialect dialect;
public SQLServerFormatEmulation(SQLServerDialect dialect, TypeConfiguration typeConfiguration) {
super(
"format",
CommonFunctionFactory.formatValidator(),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TEMPORAL, STRING )
);
super( "format", typeConfiguration );
this.dialect = dialect;
}
@ -52,7 +45,6 @@ public class SQLServerFormatEmulation extends AbstractSqmSelfRenderingFunctionDe
SqlAstTranslator<?> walker) {
final Expression datetime = (Expression) arguments.get(0);
final boolean isTime = TypeConfiguration.getSqlTemporalType( datetime.getExpressionType() ) == TemporalType.TIME;
final Format format = (Format) arguments.get(1);
sqlAppender.appendSql("format(");
if ( isTime ) {
@ -63,13 +55,8 @@ public class SQLServerFormatEmulation extends AbstractSqmSelfRenderingFunctionDe
else {
datetime.accept( walker );
}
sqlAppender.appendSql(",'");
dialect.appendDatetimeFormat( sqlAppender, format.getFormat() );
sqlAppender.appendSql("')");
}
@Override
public String getArgumentListSignature() {
return "(TEMPORAL datetime as STRING pattern)";
sqlAppender.appendSql(',');
arguments.get( 1 ).accept( walker );
sqlAppender.appendSql(')');
}
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.internal.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
@ -336,6 +337,18 @@ public final class StringHelper {
return result;
}
public static String[] splitFull(String separators, String list) {
final List<String> parts = new ArrayList<>();
int prevIndex = 0;
int index;
while ( ( index = list.indexOf( separators, prevIndex ) ) != -1 ) {
parts.add( list.substring( prevIndex, index ) );
prevIndex = index + separators.length();
}
parts.add( list.substring( prevIndex ) );
return parts.toArray(new String[0]);
}
public static String unqualify(String qualifiedName) {
int loc = qualifiedName.lastIndexOf( '.' );
return ( loc < 0 ) ? qualifiedName : qualifiedName.substring( loc + 1 );

View File

@ -683,6 +683,8 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
if ( timeZoneStorageType != null ) {
switch ( timeZoneStorageType ) {
case COLUMN:
return TimeZoneStorageStrategy.COLUMN;
case NATIVE:
return TimeZoneStorageStrategy.NATIVE;
case NORMALIZE:

View File

@ -103,4 +103,8 @@ public class MultipatternSqmFunctionDescriptor extends AbstractSqmFunctionDescri
public void setArgumentListSignature(String argumentListSignature) {
this.argumentListSignature = argumentListSignature;
}
public SqmFunctionDescriptor getFunction(int argumentCount) {
return functions[argumentCount];
}
}

View File

@ -98,6 +98,7 @@ import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource;
import org.hibernate.metamodel.model.domain.internal.CompositeSqmPathSource;
import org.hibernate.metamodel.model.domain.internal.DiscriminatorSqmPath;
import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource;
import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;
@ -189,6 +190,7 @@ import org.hibernate.query.sqm.tree.expression.SqmDurationUnit;
import org.hibernate.query.sqm.tree.expression.SqmEnumLiteral;
import org.hibernate.query.sqm.tree.expression.SqmEvery;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmExpressionHelper;
import org.hibernate.query.sqm.tree.expression.SqmExtractUnit;
import org.hibernate.query.sqm.tree.expression.SqmFieldLiteral;
import org.hibernate.query.sqm.tree.expression.SqmFormat;
@ -305,6 +307,7 @@ import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression;
import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression;
import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.expression.Star;
import org.hibernate.sql.ast.tree.expression.Summarization;
import org.hibernate.sql.ast.tree.expression.TrimSpecification;
@ -316,6 +319,7 @@ import org.hibernate.sql.ast.tree.from.NamedTableReference;
import org.hibernate.sql.ast.tree.from.PluralTableGroup;
import org.hibernate.sql.ast.tree.from.QueryPartTableGroup;
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
import org.hibernate.sql.ast.tree.from.SyntheticVirtualTableGroup;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
@ -369,10 +373,12 @@ import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.java.BasicJavaType;
import org.hibernate.type.descriptor.java.EnumJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.TemporalJavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.UserVersionType;
import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType;
import org.jboss.logging.Logger;
@ -382,7 +388,6 @@ import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY;
import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT;
import static org.hibernate.query.sqm.TemporalUnit.DAY;
import static org.hibernate.query.sqm.TemporalUnit.EPOCH;
import static org.hibernate.query.sqm.TemporalUnit.NATIVE;
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
import static org.hibernate.query.sqm.UnaryArithmeticOperator.UNARY_MINUS;
import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey;
@ -4687,6 +4692,17 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// We can't determine the type of the expression
return null;
}
if ( nodeType instanceof EmbeddedSqmPathSource<?> ) {
if ( sqmExpression instanceof SqmBinaryArithmetic<?> ) {
final SqmBinaryArithmetic<?> binaryArithmetic = (SqmBinaryArithmetic<?>) sqmExpression;
if ( binaryArithmetic.getLeftHandOperand().getNodeType() == nodeType ) {
return determineValueMapping( binaryArithmetic.getLeftHandOperand(), fromClauseIndex );
}
else if ( binaryArithmetic.getRightHandOperand().getNodeType() == nodeType ) {
return determineValueMapping( binaryArithmetic.getRightHandOperand(), fromClauseIndex );
}
}
}
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
@ -5190,7 +5206,9 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
private Object transformDurationArithmetic(SqmBinaryArithmetic<?> expression) {
BinaryArithmeticOperator operator = expression.getOperator();
final BinaryArithmeticOperator operator = expression.getOperator();
final SqmExpression<?> lhs = SqmExpressionHelper.getActualExpression( expression.getLeftHandOperand() );
final SqmExpression<?> rhs = SqmExpressionHelper.getActualExpression( expression.getRightHandOperand() );
// we have a date or timestamp somewhere to
// the right of us, so we need to restructure
@ -5223,7 +5241,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
Expression timestamp = adjustedTimestamp;
SqmExpressible<?> timestampType = adjustedTimestampType;
adjustedTimestamp = toSqlExpression( expression.getLeftHandOperand().accept( this ) );
adjustedTimestamp = toSqlExpression( lhs.accept( this ) );
JdbcMappingContainer type = adjustedTimestamp.getExpressionType();
if ( type instanceof SqmExpressible) {
adjustedTimestampType = (SqmExpressible<?>) type;
@ -5233,13 +5251,71 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
else {
// else we know it has not been transformed
adjustedTimestampType = expression.getLeftHandOperand().getNodeType();
adjustedTimestampType = lhs.getNodeType();
}
if ( operator == SUBTRACT ) {
negativeAdjustment = !negativeAdjustment;
}
try {
return expression.getRightHandOperand().accept( this );
final Object result = rhs.accept( this );
if ( result instanceof SqlTupleContainer ) {
return result;
}
final Object offset;
if ( lhs != expression.getLeftHandOperand() ) {
offset = ( (SqmPath<?>) expression.getLeftHandOperand() ).get(
AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME
).accept( this );
}
else if ( rhs != expression.getRightHandOperand() ) {
offset = ( (SqmPath<?>) expression.getRightHandOperand() ).get(
AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME
).accept( this );
}
else {
offset = null;
}
if ( offset == null ) {
return result;
}
else {
final EmbeddableValuedModelPart valueMapping = (EmbeddableValuedModelPart) determineValueMapping( expression );
final SqmPath<?> path = SqmExpressionHelper.findPath( expression, expression.getNodeType() );
final FromClauseIndex fromClauseIndex = fromClauseIndexStack.getCurrent();
final TableGroup parentTableGroup = fromClauseIndex.findTableGroup(
path.getLhs().getNavigablePath()
);
final NavigablePath navigablePath = parentTableGroup.getNavigablePath().append(
path.getNavigablePath().getUnaliasedLocalName(),
Long.toString( System.nanoTime() )
);
final TableGroup tableGroup = new SyntheticVirtualTableGroup(
navigablePath,
valueMapping,
parentTableGroup
);
fromClauseIndex.registerTableGroup( navigablePath, tableGroup );
// Register the expressions under the column reference key
final SqlExpressionResolver resolver = getSqlAstCreationState().getSqlExpressionResolver();
final TableReference tableReference = tableGroup.getPrimaryTableReference();
valueMapping.forEachSelectable(
(selectionIndex, selection) -> {
resolver.resolveSqlExpression(
SqlExpressionResolver.createColumnReferenceKey(
tableReference,
selection.getSelectionExpression()
),
processingState -> (Expression) (selectionIndex == 0 ? result : offset)
);
}
);
return new EmbeddableValuedPathInterpretation<>(
new SqlTuple( List.of( (Expression) result, (Expression) offset ), valueMapping ),
navigablePath,
valueMapping,
tableGroup
);
}
}
finally {
if ( operator == SUBTRACT ) {
@ -5260,13 +5336,13 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// x * (d1 - d2) => x * d1 - x * d2
// -x * (d1 + d2) => - x * d1 - x * d2
// -x * (d1 - d2) => - x * d1 + x * d2
Expression duration = toSqlExpression( expression.getLeftHandOperand().accept( this ) );
Expression duration = toSqlExpression( lhs.accept( this ) );
Expression scale = adjustmentScale;
boolean negate = negativeAdjustment;
adjustmentScale = applyScale( duration );
negativeAdjustment = false; //was sucked into the scale
try {
return expression.getRightHandOperand().accept( this );
return rhs.accept( this );
}
finally {
adjustmentScale = scale;
@ -5294,8 +5370,11 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// must apply the scale, and the 'by unit'
// ts1 - ts2
Expression left = cleanly( () -> toSqlExpression( expression.getLeftHandOperand().accept( this ) ) );
Expression right = cleanly( () -> toSqlExpression( expression.getRightHandOperand().accept( this ) ) );
final SqmExpression<?> lhs = SqmExpressionHelper.getActualExpression( expression.getLeftHandOperand() );
final SqmExpression<?> rhs = SqmExpressionHelper.getActualExpression( expression.getRightHandOperand() );
Expression left = getActualExpression( cleanly( () -> toSqlExpression( lhs.accept( this ) ) ) );
Expression right = getActualExpression( cleanly( () -> toSqlExpression( rhs.accept( this ) ) ) );
TypeConfiguration typeConfiguration = getCreationContext().getMappingMetamodel().getTypeConfiguration();
TemporalType leftTimestamp = typeConfiguration.getSqlTemporalType( expression.getLeftHandOperand().getNodeType() );
@ -5303,10 +5382,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// when we're dealing with Dates, we use
// DAY as the smallest unit, otherwise we
// use a platform-specific granularity
// use SECOND granularity with fractions as that is what the DurationJavaType expects
TemporalUnit baseUnit = ( rightTimestamp == TemporalType.TIMESTAMP || leftTimestamp == TemporalType.TIMESTAMP ) ?
NATIVE :
SECOND :
DAY;
if ( adjustedTimestamp != null ) {
@ -5346,6 +5425,16 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
}
private static Expression getActualExpression(Expression expression) {
if ( expression.getExpressionType() instanceof EmbeddableValuedModelPart ) {
final EmbeddableValuedModelPart embeddableValuedModelPart = (EmbeddableValuedModelPart) expression.getExpressionType();
if ( embeddableValuedModelPart.getJavaType() instanceof TemporalJavaType<?> ) {
return ( (SqlTupleContainer) expression ).getSqlTuple().getExpressions().get( 0 );
}
}
return expression;
}
private <J> BasicType<J> basicType(Class<J> javaType) {
return creationContext.getMappingMetamodel().getTypeConfiguration().getBasicTypeForJavaType( javaType );
}

View File

@ -35,10 +35,10 @@ public class EmbeddableValuedPathInterpretation<T> extends AbstractSqmPathInterp
/**
* Static factory
*/
public static <T> EmbeddableValuedPathInterpretation<T> from(
public static <T> Expression from(
SqmEmbeddedValuedSimplePath<T> sqmPath,
SqmToSqlAstConverter converter,
SemanticQueryWalker sqmWalker,
SemanticQueryWalker<?> sqmWalker,
boolean jpaQueryComplianceEnabled) {
TableGroup tableGroup = converter.getFromClauseAccess().findTableGroup( sqmPath.getLhs().getNavigablePath() );

View File

@ -9,20 +9,27 @@ package org.hibernate.query.sqm.tree.expression;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource;
import org.hibernate.query.BindableType;
import org.hibernate.query.hql.spi.SqmCreationState;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.BinaryArithmeticOperator;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.type.descriptor.java.JdbcDateJavaType;
import org.hibernate.type.descriptor.java.JdbcTimeJavaType;
import org.hibernate.type.descriptor.java.JdbcTimestampJavaType;
import org.hibernate.type.descriptor.java.TemporalJavaType;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType;
/**
* @author Steve Ebersole
@ -100,4 +107,63 @@ public class SqmExpressionHelper {
creationState.getCreationContext().getQueryEngine().getCriteriaBuilder()
);
}
public static boolean isCompositeTemporal(SqmExpression<?> expression) {
// When TimeZoneStorageStrategy.COLUMN is used, that implies using a composite user type
return expression instanceof SqmPath<?> && expression.getNodeType() instanceof EmbeddedSqmPathSource<?>
&& expression.getJavaTypeDescriptor() instanceof TemporalJavaType<?>;
}
public static SqmExpression<?> getActualExpression(SqmExpression<?> expression) {
if ( isCompositeTemporal( expression ) ) {
return ( (SqmPath<?>) expression ).get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME );
}
else {
return expression;
}
}
public static SqmExpression<?> getOffsetAdjustedExpression(SqmExpression<?> expression) {
if ( isCompositeTemporal( expression ) ) {
final SqmPath<?> compositePath = (SqmPath<?>) expression;
final SqmPath<Object> instantPath = compositePath.get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME );
final NodeBuilder nodeBuilder = instantPath.nodeBuilder();
return new SqmBinaryArithmetic<>(
BinaryArithmeticOperator.ADD,
instantPath,
new SqmToDuration<>(
compositePath.get( AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME ),
new SqmDurationUnit<>( TemporalUnit.SECOND, nodeBuilder.getIntegerType(), nodeBuilder ),
nodeBuilder.getTypeConfiguration().getBasicTypeForJavaType( Duration.class ),
nodeBuilder
),
instantPath.getNodeType(),
nodeBuilder
);
}
else {
return expression;
}
}
public static SqmPath<?> findPath(SqmExpression<?> expression, SqmExpressible<?> nodeType) {
if ( nodeType != expression.getNodeType() ) {
return null;
}
if ( expression instanceof SqmPath<?> ) {
return (SqmPath<?>) expression;
}
else if ( expression instanceof SqmBinaryArithmetic<?> ) {
final SqmBinaryArithmetic<?> binaryArithmetic = (SqmBinaryArithmetic<?>) expression;
final SqmPath<?> lhs = findPath( binaryArithmetic.getLeftHandOperand(), nodeType );
if ( lhs != null ) {
return lhs;
}
final SqmPath<?> rhs = findPath( binaryArithmetic.getRightHandOperand(), nodeType );
if ( rhs != null ) {
return rhs;
}
}
return null;
}
}

View File

@ -177,7 +177,7 @@ import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND;
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst;
import static org.hibernate.sql.results.graph.DomainResultGraphPrinter.logDomainResultGraph;
@ -3396,6 +3396,10 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
return expression instanceof JdbcParameter || expression instanceof SqmParameterInterpretation;
}
protected final boolean isLiteral(Expression expression) {
return expression instanceof Literal;
}
protected List<SortSpecification> getSortSpecificationsRowNumbering(
SelectClause selectClause,
QueryPart queryPart) {
@ -4432,7 +4436,7 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
public void visitDuration(Duration duration) {
duration.getMagnitude().accept( this );
appendSql(
duration.getUnit().conversionFactor( NANOSECOND, getDialect() )
duration.getUnit().conversionFactor( SECOND, getDialect() )
);
}

View File

@ -0,0 +1,92 @@
/*
* 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.sql.ast.tree.from;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.query.spi.NavigablePath;
/**
* @author Christian Beikov
*/
public class SyntheticVirtualTableGroup extends AbstractTableGroup implements VirtualTableGroup {
private final TableGroup underlyingTableGroup;
private final TableReference syntheticTableReference;
public SyntheticVirtualTableGroup(
NavigablePath navigablePath,
ModelPartContainer modelPart,
TableGroup underlyingTableGroup) {
super(
underlyingTableGroup.canUseInnerJoins(),
navigablePath,
modelPart,
underlyingTableGroup.getSourceAlias(),
null,
null
);
this.underlyingTableGroup = underlyingTableGroup;
this.syntheticTableReference = new NamedTableReference(
navigablePath.getFullPath(),
navigablePath.getLocalName(),
false,
null
);
}
@Override
public ModelPartContainer getExpressionType() {
return getModelPart();
}
@Override
public boolean isFetched() {
return false;
}
@Override
public String getSourceAlias() {
return underlyingTableGroup.getSourceAlias();
}
@Override
public boolean canUseInnerJoins() {
return underlyingTableGroup.canUseInnerJoins();
}
@Override
public void applyAffectedTableNames(Consumer<String> nameCollector) {
}
@Override
public TableReference getPrimaryTableReference() {
return syntheticTableReference;
}
@Override
public List<TableReferenceJoin> getTableReferenceJoins() {
return Collections.emptyList();
}
@Override
public TableReference getTableReferenceInternal(
NavigablePath navigablePath,
String tableExpression,
boolean allowFkOptimization,
boolean resolve) {
final TableReference tableReference = underlyingTableGroup.getPrimaryTableReference()
.getTableReference( navigablePath, tableExpression, allowFkOptimization, resolve );
if ( tableReference != null ) {
return syntheticTableReference;
}
return null;
}
}

View File

@ -7,7 +7,10 @@
package org.hibernate.type.descriptor.java;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.Duration;
import java.util.Locale;
import org.hibernate.dialect.Dialect;
import org.hibernate.internal.util.StringHelper;
@ -37,6 +40,13 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
* Singleton access
*/
public static final DurationJavaType INSTANCE = new DurationJavaType();
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance( Locale.ENGLISH );
private static final ThreadLocal<DecimalFormat> DECIMAL_FORMAT = new ThreadLocal<>() {
@Override
protected DecimalFormat initialValue() {
return new DecimalFormat( "0.000000000", DECIMAL_FORMAT_SYMBOLS );
}
};
public DurationJavaType() {
super( Duration.class, ImmutableMutabilityPlan.instance() );
@ -110,16 +120,11 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
}
if (value instanceof BigDecimal) {
BigDecimal[] secondsAndNanos =
((BigDecimal) value).divideAndRemainder( BigDecimal.ONE );
return Duration.ofSeconds(
secondsAndNanos[0].longValueExact(),
// use intValue() not intValueExact() here, because
// the database will sometimes produce garbage digits
// in a floating point multiplication, and we would
// get an unwanted ArithmeticException
secondsAndNanos[1].intValue()
);
return fromDecimal( value );
}
if (value instanceof Double) {
return fromDecimal( value );
}
if (value instanceof Long) {
@ -133,6 +138,18 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
throw unknownWrap( value.getClass() );
}
private Duration fromDecimal(Object number) {
final String formatted = DECIMAL_FORMAT.get().format( number );
final int dotIndex = formatted.indexOf( '.' );
if (dotIndex == -1) {
return Duration.ofSeconds( Long.parseLong( formatted ) );
}
return Duration.ofSeconds(
Long.parseLong( formatted.substring( 0, dotIndex ) ),
Long.parseLong( formatted.substring( dotIndex + 1 ) )
);
}
@Override
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
if ( jdbcType.getDefaultSqlTypeCode() == SqlTypes.INTERVAL_SECOND ) {

View File

@ -9,6 +9,7 @@ package org.hibernate.type.descriptor.java;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@ -91,6 +92,10 @@ public class InstantJavaType extends AbstractTemporalJavaType<Instant>
return (X) instant;
}
if ( OffsetDateTime.class.isAssignableFrom( type ) ) {
return (X) instant.atOffset( ZoneOffset.UTC );
}
if ( Calendar.class.isAssignableFrom( type ) ) {
return (X) GregorianCalendar.from( instant.atZone( ZoneOffset.UTC ) );
}
@ -144,6 +149,10 @@ public class InstantJavaType extends AbstractTemporalJavaType<Instant>
return (Instant) value;
}
if ( value instanceof OffsetDateTime ) {
return ( (OffsetDateTime) value ).toInstant();
}
if ( value instanceof Timestamp ) {
final Timestamp ts = (Timestamp) value;
/*

View File

@ -0,0 +1,56 @@
/*
* 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.usertype.internal;
import java.io.Serializable;
import org.hibernate.usertype.CompositeUserType;
/**
* @author Christian Beikov
*/
public abstract class AbstractTimeZoneStorageCompositeUserType<T> implements CompositeUserType<T> {
public static final String INSTANT_NAME = "instant";
public static final String ZONE_OFFSET_NAME = "zoneOffset";
@Override
public boolean equals(Object x, Object y) {
return x.equals( y );
}
@Override
public int hashCode(Object x) {
return x.hashCode();
}
@Override
public Object deepCopy(Object value) {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner) {
return cached;
}
@Override
public Object replace(Object detached, Object managed, Object owner) {
return detached;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.usertype.internal;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import org.hibernate.HibernateException;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.spi.ValueAccess;
import org.hibernate.type.SqlTypes;
/**
* @author Christian Beikov
*/
public class OffsetDateTimeCompositeUserType extends AbstractTimeZoneStorageCompositeUserType<OffsetDateTime> {
@Override
public Object getPropertyValue(OffsetDateTime component, int property) throws HibernateException {
switch ( property ) {
case 0:
return component.toInstant();
case 1:
return component.getOffset();
}
return null;
}
@Override
public OffsetDateTime instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
final Instant instant = values.getValue( 0, Instant.class );
final ZoneOffset zoneOffset = values.getValue( 1, ZoneOffset.class );
return instant == null || zoneOffset == null ? null : OffsetDateTime.ofInstant( instant, zoneOffset );
}
@Override
public Class<?> embeddable() {
return OffsetDateTimeEmbeddable.class;
}
@Override
public Class<OffsetDateTime> returnedClass() {
return OffsetDateTime.class;
}
public static class OffsetDateTimeEmbeddable {
private Instant instant;
@JdbcTypeCode( SqlTypes.INTEGER )
private ZoneOffset zoneOffset;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.usertype.internal;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import org.hibernate.HibernateException;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.spi.ValueAccess;
import org.hibernate.type.SqlTypes;
/**
* @author Christian Beikov
*/
public class ZonedDateTimeCompositeUserType extends AbstractTimeZoneStorageCompositeUserType<ZonedDateTime> {
@Override
public Object getPropertyValue(ZonedDateTime component, int property) throws HibernateException {
switch ( property ) {
case 0:
return component.toInstant();
case 1:
return component.getOffset();
}
return null;
}
@Override
public ZonedDateTime instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
final Instant instant = values.getValue( 0, Instant.class );
final ZoneOffset zoneOffset = values.getValue( 1, ZoneOffset.class );
return instant == null || zoneOffset == null ? null : ZonedDateTime.ofInstant( instant, zoneOffset );
}
@Override
public Class<?> embeddable() {
return ZonedDateTimeEmbeddable.class;
}
@Override
public Class<ZonedDateTime> returnedClass() {
return ZonedDateTime.class;
}
public static class ZonedDateTimeEmbeddable {
private Instant instant;
@JdbcTypeCode( SqlTypes.INTEGER )
private ZoneOffset zoneOffset;
}
}