Add HQL support for JDBC literals.

Closes #3739
This commit is contained in:
Mark Paluch
2025-01-09 12:37:06 +01:00
parent dcf4991f39
commit 90f7ed99fd
4 changed files with 586 additions and 56 deletions

View File

@@ -314,8 +314,10 @@ literal
| NULL
| booleanLiteral
| numericLiteral
| dateTimeLiteral
| binaryLiteral
| temporalLiteral
| arrayLiteral
| generalizedLiteral
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-boolean-literals
@@ -326,30 +328,157 @@ booleanLiteral
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-numeric-literals
numericLiteral
: INTEGER_LITERAL
| LONG_LITERAL
| BIG_INTEGER_LITERAL
| FLOAT_LITERAL
| DOUBLE_LITERAL
| BIG_DECIMAL_LITERAL
| HEX_LITERAL
;
: INTEGER_LITERAL
| LONG_LITERAL
| BIG_INTEGER_LITERAL
| FLOAT_LITERAL
| DOUBLE_LITERAL
| BIG_DECIMAL_LITERAL
| HEX_LITERAL
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-datetime-literals
/**
* A literal datetime, in braces, or with the 'datetime' keyword
*/
dateTimeLiteral
: LOCAL_DATE
| LOCAL_TIME
| LOCAL_DATETIME
| CURRENT_DATE
| CURRENT_TIME
| CURRENT_TIMESTAMP
| OFFSET_DATETIME
| (LOCAL | CURRENT) DATE
| (LOCAL | CURRENT) TIME
| (LOCAL | CURRENT | OFFSET) DATETIME
| INSTANT
: localDateTimeLiteral
| zonedDateTimeLiteral
| offsetDateTimeLiteral
;
localDateTimeLiteral
: '(' localDateTime ')'
| LOCAL? DATETIME localDateTime
;
zonedDateTimeLiteral
: '(' zonedDateTime ')'
| ZONED? DATETIME zonedDateTime
;
offsetDateTimeLiteral
: '(' offsetDateTime ')'
| OFFSET? DATETIME offsetDateTimeWithMinutes
;
/**
* A literal date, in braces, or with the 'date' keyword
*/
dateLiteral
: '(' date ')'
| LOCAL? DATE date
;
/**
* A literal time, in braces, or with the 'time' keyword
*/
timeLiteral
: '(' time ')'
| LOCAL? TIME time
;
/**
* A literal datetime
*/
dateTime
: localDateTime
| zonedDateTime
| offsetDateTime
;
localDateTime
: date time
;
zonedDateTime
: date time zoneId
;
offsetDateTime
: date time offset
;
offsetDateTimeWithMinutes
: date time offsetWithMinutes
;
/**
* A JDBC-style timestamp escape, as required by JPQL
*/
jdbcTimestampLiteral
: TIMESTAMP_ESCAPE_START (dateTime | genericTemporalLiteralText) '}'
;
/**
* A JDBC-style date escape, as required by JPQL
*/
jdbcDateLiteral
: DATE_ESCAPE_START (date | genericTemporalLiteralText) '}'
;
/**
* A JDBC-style time escape, as required by JPQL
*/
jdbcTimeLiteral
: TIME_ESCAPE_START (time | genericTemporalLiteralText) '}'
;
genericTemporalLiteralText
: STRING_LITERAL
;
/**
* A generic format for specifying literal values of arbitary types
*/
arrayLiteral
: '[' (expression (',' expression)*)? ']'
;
/**
* A generic format for specifying literal values of arbitary types
*/
generalizedLiteral
: '(' generalizedLiteralType ':' generalizedLiteralText ')'
;
generalizedLiteralType : STRING_LITERAL;
generalizedLiteralText : STRING_LITERAL;
/**
* A literal date
*/
date
: year '-' month '-' day
;
/**
* A literal time
*/
time
: hour ':' minute (':' second)?
;
/**
* A literal offset
*/
offset
: (PLUS | MINUS) hour (':' minute)?
;
offsetWithMinutes
: (PLUS | MINUS) hour ':' minute
;
year: INTEGER_LITERAL;
month: INTEGER_LITERAL;
day: INTEGER_LITERAL;
hour: INTEGER_LITERAL;
minute: INTEGER_LITERAL;
second: INTEGER_LITERAL | DOUBLE_LITERAL;
zoneId
: IDENTIFIER ('/' IDENTIFIER)?
| STRING_LITERAL;
/**
* A field that may be extracted from a date, time, or datetime
*/
@@ -402,8 +531,17 @@ binaryLiteral
| '{' HEX_LITERAL (',' HEX_LITERAL)* '}'
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-enum-literals
// TBD
/**
* A literal date, time, or datetime, in HQL syntax, or as a JDBC-style "escape" syntax
*/
temporalLiteral
: dateTimeLiteral
| dateLiteral
| timeLiteral
| jdbcTimestampLiteral
| jdbcDateLiteral
| jdbcTimeLiteral
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-java-constants
// TBD
@@ -1685,6 +1823,16 @@ BINARY_LITERAL : [xX] '\'' HEX_DIGIT+ '\''
| [xX] '"' HEX_DIGIT+ '"'
;
// ESCAPE start tokens
TIMESTAMP_ESCAPE_START : '{ts';
DATE_ESCAPE_START : '{d';
TIME_ESCAPE_START : '{t';
PLUS : '+';
MINUS : '-';
fragment
LETTER : [a-zA-Z\u0080-\ufffe_$];

View File

@@ -1038,8 +1038,12 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRING_LITERAL()));
} else if (ctx.numericLiteral() != null) {
return visit(ctx.numericLiteral());
} else if (ctx.dateTimeLiteral() != null) {
return visit(ctx.dateTimeLiteral());
} else if (ctx.temporalLiteral() != null) {
return visit(ctx.temporalLiteral());
} else if (ctx.arrayLiteral() != null) {
return visit(ctx.arrayLiteral());
} else if (ctx.generalizedLiteral() != null) {
return visit(ctx.generalizedLiteral());
} else if (ctx.binaryLiteral() != null) {
return visit(ctx.binaryLiteral());
} else {
@@ -1084,48 +1088,365 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
@Override
public QueryTokenStream visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) {
if (ctx.localDateTimeLiteral() != null) {
return visit(ctx.localDateTimeLiteral());
}
if (ctx.offsetDateTimeLiteral() != null) {
return visit(ctx.offsetDateTimeLiteral());
}
if (ctx.zonedDateTimeLiteral() != null) {
return visit(ctx.zonedDateTimeLiteral());
}
return QueryTokenStream.empty();
}
@Override
public QueryTokenStream visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.LOCAL_DATE() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL_DATE()));
} else if (ctx.LOCAL_TIME() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL_TIME()));
} else if (ctx.LOCAL_DATETIME() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL_DATETIME()));
} else if (ctx.CURRENT_DATE() != null) {
builder.append(QueryTokens.expression(ctx.CURRENT_DATE()));
} else if (ctx.CURRENT_TIME() != null) {
builder.append(QueryTokens.expression(ctx.CURRENT_TIME()));
} else if (ctx.CURRENT_TIMESTAMP() != null) {
builder.append(QueryTokens.expression(ctx.CURRENT_TIMESTAMP()));
} else if (ctx.OFFSET_DATETIME() != null) {
builder.append(QueryTokens.expression(ctx.OFFSET_DATETIME()));
} else {
if (ctx.DATETIME() != null) {
if (ctx.LOCAL() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL()));
} else if (ctx.CURRENT() != null) {
builder.append(QueryTokens.expression(ctx.CURRENT()));
} else if (ctx.OFFSET() != null) {
builder.append(QueryTokens.expression(ctx.OFFSET()));
}
if (ctx.DATE() != null) {
builder.append(QueryTokens.expression(ctx.DATE()));
} else if (ctx.TIME() != null) {
builder.append(QueryTokens.expression(ctx.TIME()));
} else if (ctx.DATETIME() != null) {
builder.append(QueryTokens.expression(ctx.DATETIME()));
}
if (ctx.INSTANT() != null) {
builder.append(QueryTokens.expression(ctx.INSTANT()));
}
builder.append(QueryTokens.expression(ctx.DATETIME()));
builder.append(visit(ctx.localDateTime()));
} else {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.localDateTime()));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
}
@Override
public QueryTokenStream visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.DATETIME() != null) {
if (ctx.ZONED() != null) {
builder.append(QueryTokens.expression(ctx.ZONED()));
}
builder.append(QueryTokens.expression(ctx.DATETIME()));
builder.append(visit(ctx.zonedDateTime()));
} else {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.zonedDateTime()));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
}
@Override
public QueryTokenStream visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.DATETIME() != null) {
if (ctx.OFFSET() != null) {
builder.append(QueryTokens.expression(ctx.OFFSET()));
}
builder.append(QueryTokens.expression(ctx.DATETIME()));
builder.append(visit(ctx.offsetDateTimeWithMinutes()));
} else {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.offsetDateTime()));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
}
@Override
public QueryTokenStream visitDateLiteral(HqlParser.DateLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.DATE() != null) {
if (ctx.LOCAL() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL()));
}
builder.append(QueryTokens.expression(ctx.DATE()));
builder.append(visit(ctx.date()));
} else {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.date()));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
}
@Override
public QueryTokenStream visitTimeLiteral(HqlParser.TimeLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.TIME() != null) {
if (ctx.LOCAL() != null) {
builder.append(QueryTokens.expression(ctx.LOCAL()));
}
builder.append(QueryTokens.expression(ctx.TIME()));
builder.append(visit(ctx.time()));
} else {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.time()));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
}
@Override
public QueryTokenStream visitDateTime(HqlParser.DateTimeContext ctx) {
if (ctx.localDateTime() != null) {
return visit(ctx.localDateTime());
}
if (ctx.offsetDateTime() != null) {
return visit(ctx.offsetDateTime());
}
if (ctx.zonedDateTime() != null) {
return visit(ctx.zonedDateTime());
}
return QueryTokenStream.empty();
}
@Override
public QueryTokenStream visitLocalDateTime(HqlParser.LocalDateTimeContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.date()));
builder.appendExpression(visit(ctx.time()));
return builder;
}
@Override
public QueryTokenStream visitZonedDateTime(HqlParser.ZonedDateTimeContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.date()));
builder.appendExpression(visit(ctx.time()));
builder.appendExpression(visit(ctx.zoneId()));
return builder;
}
@Override
public QueryTokenStream visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.date()));
builder.appendInline(visit(ctx.time()));
builder.appendInline(visit(ctx.offset()));
return builder;
}
@Override
public QueryTokenStream visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.date()));
builder.appendInline(visit(ctx.time()));
builder.appendInline(visit(ctx.offsetWithMinutes()));
return builder;
}
@Override
public QueryTokenStream visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(TOKEN_OPEN_BRACE);
builder.append(QueryTokens.token("ts"));
builder.append(visit(ctx.dateTime() != null ? ctx.dateTime() : ctx.genericTemporalLiteralText()));
builder.append(QueryTokens.TOKEN_CLOSE_BRACE);
return builder;
}
@Override
public QueryTokenStream visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(TOKEN_OPEN_BRACE);
builder.append(QueryTokens.token("d"));
builder.append(visit(ctx.date() != null ? ctx.date() : ctx.genericTemporalLiteralText()));
builder.append(QueryTokens.TOKEN_CLOSE_BRACE);
return builder;
}
@Override
public QueryTokenStream visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(TOKEN_OPEN_BRACE);
builder.append(QueryTokens.token("t"));
builder.append(visit(ctx.time() != null ? ctx.time() : ctx.genericTemporalLiteralText()));
builder.append(QueryTokens.TOKEN_CLOSE_BRACE);
return builder;
}
@Override
public QueryTokenStream visitGenericTemporalLiteralText(HqlParser.GenericTemporalLiteralTextContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL()));
}
@Override
public QueryTokenStream visitArrayLiteral(HqlParser.ArrayLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(TOKEN_OPEN_SQUARE_BRACKET);
builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA));
builder.append(TOKEN_CLOSE_SQUARE_BRACKET);
return builder;
}
@Override
public QueryTokenStream visitGeneralizedLiteral(HqlParser.GeneralizedLiteralContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.generalizedLiteralType()));
builder.append(TOKEN_COLON);
builder.append(visit(ctx.generalizedLiteralText()));
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
@Override
public QueryTokenStream visitGeneralizedLiteralType(HqlParser.GeneralizedLiteralTypeContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL()));
}
@Override
public QueryTokenStream visitGeneralizedLiteralText(HqlParser.GeneralizedLiteralTextContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL()));
}
@Override
public QueryTokenStream visitDate(HqlParser.DateContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(visit(ctx.year()));
builder.append(TOKEN_DASH);
builder.append(visit(ctx.month()));
builder.append(TOKEN_DASH);
builder.append(visit(ctx.day()));
return builder;
}
@Override
public QueryTokenStream visitTime(HqlParser.TimeContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(visit(ctx.hour()));
builder.append(TOKEN_COLON);
builder.append(visit(ctx.minute()));
if (ctx.second() != null) {
builder.append(TOKEN_COLON);
builder.append(visit(ctx.second()));
}
return builder;
}
@Override
public QueryTokenStream visitOffset(HqlParser.OffsetContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.MINUS() != null) {
builder.append(QueryTokens.token(ctx.MINUS()));
} else if (ctx.PLUS() != null) {
builder.append(QueryTokens.token(ctx.PLUS()));
}
builder.append(visit(ctx.hour()));
if (ctx.minute() != null) {
builder.append(TOKEN_COLON);
builder.append(visit(ctx.minute()));
}
return builder;
}
@Override
public QueryTokenStream visitOffsetWithMinutes(HqlParser.OffsetWithMinutesContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.MINUS() != null) {
builder.append(QueryTokens.token(ctx.MINUS()));
} else if (ctx.PLUS() != null) {
builder.append(QueryTokens.token(ctx.PLUS()));
}
builder.append(visit(ctx.hour()));
builder.append(TOKEN_COLON);
builder.append(visit(ctx.minute()));
return builder;
}
@Override
public QueryTokenStream visitYear(HqlParser.YearContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitMonth(HqlParser.MonthContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitDay(HqlParser.DayContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitHour(HqlParser.HourContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitMinute(HqlParser.MinuteContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitSecond(HqlParser.SecondContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL()));
}
@Override
public QueryTokenStream visitZoneId(HqlParser.ZoneIdContext ctx) {
return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL()));
}
@Override
public QueryTokenStream visitDatetimeField(HqlParser.DatetimeFieldContext ctx) {
@@ -1250,6 +1571,36 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
return builder;
}
@Override
public QueryTokenStream visitTemporalLiteral(HqlParser.TemporalLiteralContext ctx) {
if (ctx.dateTimeLiteral() != null) {
return visit(ctx.dateTimeLiteral());
}
if (ctx.dateLiteral() != null) {
return visit(ctx.dateLiteral());
}
if (ctx.timeLiteral() != null) {
return visit(ctx.timeLiteral());
}
if (ctx.jdbcTimestampLiteral() != null) {
return visit(ctx.jdbcTimestampLiteral());
}
if (ctx.jdbcDateLiteral() != null) {
return visit(ctx.jdbcDateLiteral());
}
if (ctx.jdbcTimeLiteral() != null) {
return visit(ctx.jdbcTimeLiteral());
}
return QueryTokenStream.empty();
}
@Override
public QueryTokenStream visitPlainPrimaryExpression(HqlParser.PlainPrimaryExpressionContext ctx) {
return visit(ctx.primaryExpression());

View File

@@ -47,6 +47,7 @@ class QueryTokens {
static final QueryToken TOKEN_OPEN_SQUARE_BRACKET = token("[");
static final QueryToken TOKEN_CLOSE_SQUARE_BRACKET = token("]");
static final QueryToken TOKEN_COLON = token(":");
static final QueryToken TOKEN_DASH = token("-");
static final QueryToken TOKEN_QUESTION_MARK = token("?");
static final QueryToken TOKEN_OPEN_BRACE = token("{");
static final QueryToken TOKEN_CLOSE_BRACE = token("}");

View File

@@ -1749,6 +1749,36 @@ class HqlQueryRendererTests {
assertQuery("SELECT ce.id as id, cd.startDate + 5 %s AS summedDate FROM CalendarEvent ce".formatted(dtField));
}
@Test // GH-3739
void dateTimeLiterals() {
assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}");
assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00'}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'something weird'}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts2012-01-03 09:00:00+1}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts2012-01-03 09:00:00-1}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts2012-01-03 09:00:00+1:00}");
assertQuery("SELECT e FROM Employee e WHERE e.version = {ts2012-01-03 09:00:00-1:00}");
assertQuery("SELECT e FROM Employee e WHERE e.version = OFFSET DATETIME 2012-01-03 09:00:00+1:01");
assertQuery("SELECT e FROM Employee e WHERE e.version = OFFSET DATETIME 2012-01-03 09:00:00-1:01");
}
@Test
void literals() {
assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'");
assertQuery("SELECT e FROM Employee e WHERE e.names = [e.firstName, e.lastName]");
assertQuery("SELECT e FROM Employee e WHERE e.id = 1234");
assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L");
assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F");
assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D");
assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE");
assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE");
assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager");
}
@ParameterizedTest // GH-3711
@ValueSource(strings = { "1", "1_000", "1L", "1_000L", "1bi", "1.1f", "2.2d", "2.2bd" })
void numberLiteralsShouldWork(String literal) {