From 572efd28a9a7b6e9acdd250708ad8802ee38ec09 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Wed, 14 Jun 2023 14:20:57 -0500 Subject: [PATCH] Handle dateTimeField property with HQL. Hibernate supports varisous duration literals. They also support a couple formats for binary literals. See #3025 --- .../data/jpa/repository/query/Hql.g4 | 29 ++++++- .../repository/query/HqlQueryRenderer.java | 76 +++++++++++++++++++ .../query/JpaQueryParsingToken.java | 1 + .../query/HqlQueryRendererTests.java | 13 ++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 index 679c6293b..e5ad3cfe8 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 @@ -299,6 +299,7 @@ literal | stringLiteral | numericLiteral | dateTimeLiteral + | binaryLiteral ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-boolean-literals @@ -337,10 +338,24 @@ dateTimeLiteral ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-duration-literals -// TBD +datetimeField + : YEAR + | MONTH + | DAY + | WEEK + | QUARTER + | HOUR + | MINUTE + | SECOND + | NANOSECOND + | EPOCH + ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-binary-literals -// TBD +binaryLiteral + : BINARY_LITERAL + | '{' HEXLITERAL (',' HEXLITERAL)* '}' + ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-enum-literals // TBD @@ -362,6 +377,8 @@ expression | primaryExpression # PlainPrimaryExpression | op=('+' | '-') numericLiteral # SignedNumericLiteral | op=('+' | '-') expression # SignedExpression + | expression datetimeField # ToDurationExpression + | expression BY datetimeField # FromDurationExpression | expression op=('*' | '/') expression # MultiplicationExpression | expression op=('+' | '-') expression # AdditionExpression | expression '||' expression # HqlConcatenationExpression @@ -607,7 +624,6 @@ inList ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-exists-predicate -// TBD existsExpression : EXISTS (ELEMENTS | INDICES) '(' simplePath ')' | EXISTS expression @@ -1058,13 +1074,18 @@ YEAR : Y E A R; fragment INTEGER_NUMBER : ('0' .. '9')+ ; fragment FLOAT_NUMBER : INTEGER_NUMBER+ '.'? INTEGER_NUMBER* (E [+-]? INTEGER_NUMBER)? ; +fragment HEX_DIGIT : [0-9a-fA-F]; + CHARACTER : '\'' (~ ('\'' | '\\' )) '\'' ; STRINGLITERAL : '\'' ('\'' '\'' | ~('\'' | '\\'))* '\'' ; JAVASTRINGLITERAL : '"' ( ('\\' [btnfr"']) | ~('"'))* '"'; INTEGER_LITERAL : INTEGER_NUMBER (L | B I)? ; FLOAT_LITERAL : FLOAT_NUMBER (D | F | B D)?; -HEXLITERAL : '0' X ('0' .. '9' | A | B | C | D | E)+ ; +HEXLITERAL : '0' X HEX_DIGIT+ ; +BINARY_LITERAL : [xX] '\'' HEX_DIGIT+ '\'' + | [xX] '"' HEX_DIGIT+ '"' + ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 3ad3d4e24..0affea51a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -1047,6 +1047,8 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return visit(ctx.numericLiteral()); } else if (ctx.dateTimeLiteral() != null) { return visit(ctx.dateTimeLiteral()); + } else if (ctx.binaryLiteral() != null) { + return visit(ctx.binaryLiteral()); } else { return List.of(); } @@ -1135,6 +1137,56 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return tokens; } + @Override + public List visitDatetimeField(HqlParser.DatetimeFieldContext ctx) { + + if (ctx.YEAR() != null) { + return List.of(new JpaQueryParsingToken(ctx.YEAR())); + } else if (ctx.MONTH() != null) { + return List.of(new JpaQueryParsingToken(ctx.MONTH())); + } else if (ctx.DAY() != null) { + return List.of(new JpaQueryParsingToken(ctx.DAY())); + } else if (ctx.WEEK() != null) { + return List.of(new JpaQueryParsingToken(ctx.WEEK())); + } else if (ctx.QUARTER() != null) { + return List.of(new JpaQueryParsingToken(ctx.QUARTER())); + } else if (ctx.HOUR() != null) { + return List.of(new JpaQueryParsingToken(ctx.HOUR())); + } else if (ctx.MINUTE() != null) { + return List.of(new JpaQueryParsingToken(ctx.MINUTE())); + } else if (ctx.SECOND() != null) { + return List.of(new JpaQueryParsingToken(ctx.SECOND())); + } else if (ctx.NANOSECOND() != null) { + return List.of(new JpaQueryParsingToken(ctx.NANOSECOND())); + } else if (ctx.EPOCH() != null) { + return List.of(new JpaQueryParsingToken(ctx.EPOCH())); + } else { + return List.of(); + } + } + + @Override + public List visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.BINARY_LITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.BINARY_LITERAL())); + } else if (ctx.HEXLITERAL() != null) { + + tokens.add(TOKEN_OPEN_BRACE); + ctx.HEXLITERAL().forEach(terminalNode -> { + tokens.add(new JpaQueryParsingToken(terminalNode)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + tokens.add(TOKEN_CLOSE_BRACE); + } + + return tokens; + } + @Override public List visitPlainPrimaryExpression(HqlParser.PlainPrimaryExpressionContext ctx) { return visit(ctx.primaryExpression()); @@ -1177,6 +1229,7 @@ class HqlQueryRenderer extends HqlBaseVisitor> { tokens.add(TOKEN_OPEN_PAREN); tokens.addAll(visit(ctx.expression())); + NOSPACE(tokens); tokens.add(TOKEN_CLOSE_PAREN); return tokens; @@ -1242,6 +1295,29 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return tokens; } + @Override + public List visitToDurationExpression(HqlParser.ToDurationExpressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.expression())); + tokens.addAll(visit(ctx.datetimeField())); + + return tokens; + } + + @Override + public List visitFromDurationExpression(HqlParser.FromDurationExpressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.expression())); + tokens.add(new JpaQueryParsingToken(ctx.BY())); + tokens.addAll(visit(ctx.datetimeField())); + + return tokens; + } + @Override public List visitCaseExpression(HqlParser.CaseExpressionContext ctx) { return visit(ctx.caseList()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java index 59c409e9e..00717de3d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java @@ -48,6 +48,7 @@ class JpaQueryParsingToken { public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET = new JpaQueryParsingToken("]"); public static final JpaQueryParsingToken TOKEN_COLON = new JpaQueryParsingToken(":", false); public static final JpaQueryParsingToken TOKEN_QUESTION_MARK = new JpaQueryParsingToken("?", false); + public static final JpaQueryParsingToken TOKEN_OPEN_BRACE = new JpaQueryParsingToken("{", false); public static final JpaQueryParsingToken TOKEN_CLOSE_BRACE = new JpaQueryParsingToken("}"); public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET_BRACE = new JpaQueryParsingToken("]}"); public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN_BRACE = new JpaQueryParsingToken(")}"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index a1e62a7e7..f37e341ad 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1526,4 +1526,17 @@ class HqlQueryRendererTests { void castFunctionWithFqdnShouldWork() { assertQuery("SELECT o FROM Order o WHERE CAST(:userId AS java.util.UUID) IS NULL OR o.user.id = :userId"); } + + @Test // GH-3025 + void durationLiteralsShouldWork() { + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE (ce.endDate - ce.startDate) > 5 MINUTE"); + } + + @Test // GH-3025 + void binaryLiteralsShouldWork() { + + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE ce.value = {0xDE, 0xAD, 0xBE, 0xEF}"); + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE ce.value = X'DEADBEEF'"); + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE ce.value = x'deadbeef'"); + } }