From e0bd821476ef0dd8c672f51701ea026613f77ffc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Dec 2024 16:34:52 +0100 Subject: [PATCH] Polishing. Align HQL parsing with Hibernate to accept generic function names. Also, align literals, add missing expression grammar for ID, NATURALID, VERSION, TYPE, FK and remove parser rules interfering with undesired parsing results. Add missing literals. See #3711 --- .../data/jpa/repository/query/Hql.g4 | 370 +++++++++++++----- .../query/HqlQueryIntrospector.java | 4 +- .../repository/query/HqlQueryRenderer.java | 362 ++++++++++++++--- .../query/HqlQueryRendererTests.java | 213 +++++++++- .../query/HqlQueryTransformerTests.java | 3 +- 5 files changed, 795 insertions(+), 157 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 80e929784..4b0bb9a14 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 @@ -25,7 +25,9 @@ grammar Hql; * management of complex rules in the generated Visitor. Finally, there are labels applied to rule elements (op=('+'|'-') * to simplify the processing. * - * @author Greg Turnquist, Yannick Brandt + * @author Greg Turnquist + * @author Mark Paluch + * @author Yannick Brandt * @since 3.1 */ } @@ -307,9 +309,10 @@ setOperator // Literals // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-literals literal - : NULL + : STRING_LITERAL + | JAVA_STRING_LITERAL + | NULL | booleanLiteral - | stringLiteral | numericLiteral | dateTimeLiteral | binaryLiteral @@ -321,19 +324,16 @@ booleanLiteral | FALSE ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-string-literals -stringLiteral - : STRINGLITERAL - | JAVASTRINGLITERAL - | CHARACTER - ; - // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-numeric-literals numericLiteral - : INTEGER_LITERAL - | FLOAT_LITERAL - | HEXLITERAL - ; + : 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 dateTimeLiteral @@ -399,7 +399,7 @@ dateOrTimeField // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-binary-literals binaryLiteral : BINARY_LITERAL - | '{' HEXLITERAL (',' HEXLITERAL)* '}' + | '{' HEX_LITERAL (',' HEX_LITERAL)* '}' ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-enum-literals @@ -433,19 +433,29 @@ expression ; primaryExpression - : caseList # CaseExpression - | literal # LiteralExpression - | parameter # ParameterExpression - | function # FunctionExpression - | generalPathFragment # GeneralPathExpression + : caseList # CaseExpression + | literal # LiteralExpression + | parameter # ParameterExpression + | entityTypeReference # EntityTypeExpression + | entityIdReference # EntityIdExpression + | entityVersionReference # EntityVersionExpression + | entityNaturalIdReference # EntityNaturalIdExpression + | syntacticDomainPath pathContinuation? # SyntacticPathExpression + | function # FunctionExpression + | generalPathFragment # GeneralPathExpression ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-Datetime-arithmetic -// TBD - -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-path-expressions +/** + * A much more complicated path expression involving operators and functions + * + * A path which needs to be resolved semantically. This recognizes + * any path-like structure. Generally, the path is semantically + * interpreted by the consumer of the parse-tree. However, there + * are certain cases where we can syntactically recognize a navigable + * path; see 'syntacticNavigablePath' rule + */ path - : treatedPath pathContinutation? + : syntacticDomainPath pathContinuation? | generalPathFragment ; @@ -457,14 +467,120 @@ indexedPathAccessFragment : '[' expression ']' ('.' generalPathFragment)? ; +/** + * A simple path expression + * + * - a reference to an identification variable (not case-sensitive), + * - followed by a list of period-separated identifiers (case-sensitive) + */ simplePath : identifier simplePathElement* ; +/** + * An element of a simple path expression: a period, and an identifier (case-sensitive) + */ simplePathElement : '.' identifier ; +/** + * A continuation of a path expression "broken" by an operator or function + */ +pathContinuation + : '.' simplePath + ; + +/** + * The special function 'type()' + */ +entityTypeReference + : TYPE '(' (path | parameter) ')' + ; + +/** + * The special function 'id()' + */ +entityIdReference + : ID '(' path ')' pathContinuation? + ; + +/** + * The special function 'version()' + */ +entityVersionReference + : VERSION '(' path ')' + ; + +/** + * The special function 'naturalid()' + */ +entityNaturalIdReference + : NATURALID '(' path ')' pathContinuation? + ; + +/** + * An operator or function that may occur within a path expression + * + * Rule for cases where we syntactically know that the path is a + * "domain path" because it is one of these special cases: + * + * * TREAT( path ) + * * ELEMENTS( path ) + * * INDICES( path ) + * * VALUE( path ) + * * KEY( path ) + * * path[ selector ] + * * ARRAY_GET( embeddableArrayPath, index ).path + * * COALESCE( array1, array2 )[ selector ].path + */ +syntacticDomainPath + : treatedNavigablePath + | collectionValueNavigablePath + | mapKeyNavigablePath + | simplePath indexedPathAccessFragment + | simplePath slicedPathAccessFragment + | toOneFkReference + | function pathContinuation + | function indexedPathAccessFragment pathContinuation? + | function slicedPathAccessFragment + ; + +/** + * The slice operator to obtain elements between the lower and upper bound. + */ +slicedPathAccessFragment + : '[' expression ':' expression ']' + ; + +/** + * A 'treat()' function that "breaks" a path expression + */ +treatedNavigablePath + : TREAT '(' path AS simplePath ')' pathContinuation? + ; + +/** + * A 'value()' function that "breaks" a path expression + */ +collectionValueNavigablePath + : elementValueQuantifier '(' path ')' pathContinuation? + ; + +/** + * A 'key()' or 'index()' function that "breaks" a path expression + */ +mapKeyNavigablePath + : indexKeyQuantifier '(' path ')' pathContinuation? + ; + +/** + * The special function 'fk()' + */ +toOneFkReference + : FK '(' path ')' + ; + // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-case-expressions caseList : simpleCaseExpression @@ -511,7 +627,7 @@ function */ standardFunction : castFunction - | treatedPath + | treatedNavigablePath | extractFunction | truncFunction | formatFunction @@ -587,7 +703,7 @@ trimSpecification ; trimCharacter - : stringLiteral + : STRING_LITERAL | parameter ; @@ -604,7 +720,7 @@ padSpecification ; padCharacter - : stringLiteral + : STRING_LITERAL ; padLength @@ -756,7 +872,7 @@ rollup * see 'Dialect.appendDatetimeFormat()' */ format - : stringLiteral + : STRING_LITERAL ; /** @@ -785,7 +901,7 @@ jpaNonstandardFunction * The name of a user-defined or native database function, given as a quoted string */ jpaNonstandardFunctionName - : stringLiteral + : STRING_LITERAL | identifier ; @@ -799,7 +915,7 @@ columnFunction * The function name, followed by a parenthesized list of ','-separated expressions */ genericFunction - : genericFunctionName '(' (genericFunctionArguments | ASTERISK)? ')' pathContinutation? + : genericFunctionName '(' (genericFunctionArguments | ASTERISK)? ')' pathContinuation? nthSideClause? nullsClause? withinGroupClause? filterClause? overClause? ; @@ -984,15 +1100,6 @@ frameExclusion | EXCLUDE NO OTHERS ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-treat-type -treatedPath - : TREAT '(' path AS simplePath')' pathContinutation? - ; - -pathContinutation - : '.' simplePath - ; - // Predicates // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-conditional-expressions predicate @@ -1027,6 +1134,16 @@ elementsValuesQuantifier | VALUES ; +elementValueQuantifier + : ELEMENT + | VALUE + ; + +indexKeyQuantifier + : INDEX + | KEY + ; + indicesKeysQuantifier : INDICES | KEYS @@ -1045,7 +1162,7 @@ betweenExpression // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-like-predicate stringPatternMatching - : expression NOT? (LIKE | ILIKE) expression (ESCAPE (stringLiteral|parameter))? + : expression NOT? (LIKE | ILIKE) expression (ESCAPE (STRING_LITERAL | JAVA_STRING_LITERAL |parameter))? ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-elements-indices @@ -1097,9 +1214,12 @@ parameterOrNumberLiteral | numericLiteral ; +/** + * An identification variable (an entity alias) + */ variable : AS identifier - | reservedWord + | nakedIdentifier ; parameter @@ -1111,16 +1231,9 @@ entityName : identifier ('.' identifier)* ; -identifier - : reservedWord - ; - -functionName - : reservedWord ('.' reservedWord)* - ; - -reservedWord - : IDENTIFICATION_VARIABLE +nakedIdentifier + : IDENTIFIER + | QUOTED_IDENTIFIER | f=(ALL | AND | ANY @@ -1133,8 +1246,10 @@ reservedWord | BY | CASE | CAST - | CEILING | COLLATE + | COLUMN + | CONFLICT + | CONSTRAINT | CONTAINS | COUNT | CROSS @@ -1153,6 +1268,7 @@ reservedWord | DEPTH | DESC | DISTINCT + | DO | ELEMENT | ELEMENTS | ELSE @@ -1166,18 +1282,15 @@ reservedWord | EXCEPT | EXCLUDE | EXISTS - | EXP | EXTRACT - | FALSE | FETCH | FILTER | FIRST - | FLOOR + | FK | FOLLOWING | FOR | FORMAT | FROM - | FULL | FUNCTION | GROUP | GROUPS @@ -1187,10 +1300,9 @@ reservedWord | IGNORE | ILIKE | IN - | INCLUDES | INDEX + | INCLUDES | INDICES - | INNER | INSERT | INSTANT | INTERSECT @@ -1199,15 +1311,14 @@ reservedWord | IS | JOIN | KEY + | KEYS | LAST | LATERAL | LEADING - | LEFT | LIKE | LIMIT | LIST | LISTAGG - | LN | LOCAL | LOCAL_DATE | LOCAL_DATETIME @@ -1231,6 +1342,7 @@ reservedWord | NEXT | NO | NOT + | NOTHING | NULLS | OBJECT | OF @@ -1241,7 +1353,6 @@ reservedWord | OR | ORDER | OTHERS - | OUTER | OVER | OVERFLOW | OVERLAY @@ -1250,7 +1361,6 @@ reservedWord | PERCENT | PLACING | POSITION - | POWER | PRECEDING | QUARTER | RANGE @@ -1267,7 +1377,6 @@ reservedWord | SOME | SUBSTRING | SUM - | TRUE | THEN | TIES | TIME @@ -1295,7 +1404,17 @@ reservedWord | WITH | WITHIN | WITHOUT - | YEAR) + | YEAR + | ZONED) + ; + +identifier + : nakedIdentifier + | FULL + | INNER + | LEFT + | OUTER + | RIGHT ; /* @@ -1334,14 +1453,20 @@ fragment X: 'x' | 'X'; fragment Y: 'y' | 'Y'; fragment Z: 'z' | 'Z'; +ASTERISK : '*'; + // The following are reserved identifiers: +ID : I D; +VERSION : V E R S I O N; +VERSIONED : V E R S I O N E D; +NATURALID : N A T U R A L I D; +FK : F K; ALL : A L L; AND : A N D; ANY : A N Y; AS : A S; ASC : A S C; -ASTERISK : '*'; AVG : A V G; BETWEEN : B E T W E E N; BOTH : B O T H; @@ -1349,7 +1474,6 @@ BREADTH : B R E A D T H; BY : B Y; CASE : C A S E; CAST : C A S T; -CEILING : C E I L I N G; COLLATE : C O L L A T E; COLUMN : C O L U M N; CONFLICT : C O N F L I C T; @@ -1386,14 +1510,10 @@ EVERY : E V E R Y; EXCEPT : E X C E P T; EXCLUDE : E X C L U D E; EXISTS : E X I S T S; -EXP : E X P; EXTRACT : E X T R A C T; -FALSE : F A L S E; FETCH : F E T C H; FILTER : F I L T E R; FIRST : F I R S T; -FK : F K; -FLOOR : F L O O R; FOLLOWING : F O L L O W I N G; FOR : F O R; FORMAT : F O R M A T; @@ -1404,7 +1524,6 @@ GROUP : G R O U P; GROUPS : G R O U P S; HAVING : H A V I N G; HOUR : H O U R; -ID : I D; IGNORE : I G N O R E; ILIKE : I L I K E; IN : I N; @@ -1429,7 +1548,6 @@ LIKE : L I K E; LIMIT : L I M I T; LIST : L I S T; LISTAGG : L I S T A G G; -LN : L N; LOCAL : L O C A L; LOCAL_DATE : L O C A L '_' D A T E ; LOCAL_DATETIME : L O C A L '_' D A T E T I M E; @@ -1448,13 +1566,11 @@ MININDEX : M I N I N D E X; MINUTE : M I N U T E; MONTH : M O N T H; NANOSECOND : N A N O S E C O N D; -NATURALID : N A T U R A L I D; NEW : N E W; NEXT : N E X T; NO : N O; NOT : N O T; NOTHING : N O T H I N G; -NULL : N U L L; NULLS : N U L L S; OBJECT : O B J E C T; OF : O F; @@ -1474,7 +1590,6 @@ PARTITION : P A R T I T I O N; PERCENT : P E R C E N T; PLACING : P L A C I N G; POSITION : P O S I T I O N; -POWER : P O W E R; PRECEDING : P R E C E D I N G; QUARTER : Q U A R T E R; RANGE : R A N G E; @@ -1501,7 +1616,6 @@ TO : T O; TRAILING : T R A I L I N G; TREAT : T R E A T; TRIM : T R I M; -TRUE : T R U E; TRUNC : T R U N C; TRUNCATE : T R U N C A T E; TYPE : T Y P E; @@ -1511,8 +1625,6 @@ UPDATE : U P D A T E; USING : U S I N G; VALUE : V A L U E; VALUES : V A L U E S; -VERSION : V E R S I O N; -VERSIONED : V E R S I O N E D; WEEK : W E E K; WHEN : W H E N; WHERE : W H E R E; @@ -1520,20 +1632,102 @@ WITH : W I T H; WITHIN : W I T H I N; WITHOUT : W I T H O U T; YEAR : Y E A R; +ZONED : Z O N E D; + +NULL : N U L L; +TRUE : T R U E; +FALSE : F A L S E; + +fragment +INTEGER_NUMBER + : DIGIT+ + ; + +fragment +FLOATING_POINT_NUMBER + : DIGIT+ '.' DIGIT* EXPONENT? + | '.' DIGIT+ EXPONENT? + | DIGIT+ EXPONENT + | DIGIT+ + ; + +fragment +EXPONENT : [eE] [+-]? DIGIT+; -fragment INTEGER_NUMBER : ('0' .. '9')+ ; -fragment FLOAT_NUMBER : INTEGER_NUMBER+ '.'? INTEGER_NUMBER* (E [+-]? INTEGER_NUMBER)? ; fragment HEX_DIGIT : [0-9a-fA-F]; +fragment SINGLE_QUOTE : '\''; +fragment DOUBLE_QUOTE : '"'; + +STRING_LITERAL : SINGLE_QUOTE ( SINGLE_QUOTE SINGLE_QUOTE | ~('\'') )* SINGLE_QUOTE; + +JAVA_STRING_LITERAL + : DOUBLE_QUOTE ( ESCAPE_SEQUENCE | ~('"') )* DOUBLE_QUOTE + | [jJ] SINGLE_QUOTE ( ESCAPE_SEQUENCE | ~('\'') )* SINGLE_QUOTE + | [jJ] DOUBLE_QUOTE ( ESCAPE_SEQUENCE | ~('\'') )* DOUBLE_QUOTE + ; + +INTEGER_LITERAL : INTEGER_NUMBER ('_' INTEGER_NUMBER)*; + +LONG_LITERAL : INTEGER_NUMBER ('_' INTEGER_NUMBER)* LONG_SUFFIX; + +FLOAT_LITERAL : FLOATING_POINT_NUMBER FLOAT_SUFFIX; + +DOUBLE_LITERAL : FLOATING_POINT_NUMBER DOUBLE_SUFFIX?; + +BIG_INTEGER_LITERAL : INTEGER_NUMBER BIG_INTEGER_SUFFIX; + +BIG_DECIMAL_LITERAL : FLOATING_POINT_NUMBER BIG_DECIMAL_SUFFIX; + +HEX_LITERAL : '0' [xX] HEX_DIGIT+ LONG_SUFFIX?; -CHARACTER : '\'' (~ ('\'' | '\\' )) '\'' ; -STRINGLITERAL : '\'' ('\'' '\'' | ~('\''))* '\'' ; -JAVASTRINGLITERAL : '"' ( ('\\' [btnfr"']) | ~('"'))* '"'; -INTEGER_LITERAL : INTEGER_NUMBER (L | B I)? ; -FLOAT_LITERAL : FLOAT_NUMBER (D | F | B D)?; -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' | '$' | '_')* ; +fragment +LETTER : [a-zA-Z\u0080-\ufffe_$]; + +fragment +DIGIT : [0-9]; + +fragment +LONG_SUFFIX : [lL]; + +fragment +FLOAT_SUFFIX : [fF]; + +fragment +DOUBLE_SUFFIX : [dD]; + +fragment +BIG_DECIMAL_SUFFIX : [bB] [dD]; + +fragment +BIG_INTEGER_SUFFIX : [bB] [iI]; + +// Identifiers +IDENTIFIER + : LETTER (LETTER | DIGIT)* + ; + +fragment +BACKTICK : '`'; + +fragment BACKSLASH : '\\'; + +fragment +UNICODE_ESCAPE + : 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT + ; + +fragment +ESCAPE_SEQUENCE + : BACKSLASH [btnfr"'] + | BACKSLASH UNICODE_ESCAPE + | BACKSLASH BACKSLASH + ; + +QUOTED_IDENTIFIER + : BACKTICK ( ESCAPE_SEQUENCE | '\\' BACKTICK | ~([`]) )* BACKTICK + ; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java index 0e6c5cab0..259556e54 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.ArrayList; import java.util.Collections; @@ -84,7 +84,7 @@ class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIn } private static String capturePrimaryAlias(VariableContext ctx) { - return ((ctx).reservedWord() != null ? ctx.reservedWord() : ctx.identifier().reservedWord()).getText(); + return ((ctx).nakedIdentifier() != null ? ctx.nakedIdentifier() : ctx.identifier()).getText(); } private static List captureSelectItems(List selections, 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 2ef49b95f..311fdce1d 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 @@ -1032,8 +1032,10 @@ class HqlQueryRenderer extends HqlBaseVisitor { return QueryRendererBuilder.from(QueryTokens.expression(ctx.NULL())); } else if (ctx.booleanLiteral() != null) { return visit(ctx.booleanLiteral()); - } else if (ctx.stringLiteral() != null) { - return visit(ctx.stringLiteral()); + } else if (ctx.JAVA_STRING_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.JAVA_STRING_LITERAL())); + } else if (ctx.STRING_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRING_LITERAL())); } else if (ctx.numericLiteral() != null) { return visit(ctx.numericLiteral()); } else if (ctx.dateTimeLiteral() != null) { @@ -1057,27 +1059,23 @@ class HqlQueryRenderer extends HqlBaseVisitor { } } - @Override - public QueryTokenStream visitStringLiteral(HqlParser.StringLiteralContext ctx) { - - if (ctx.STRINGLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRINGLITERAL())); - } else if (ctx.CHARACTER() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); - } else { - return QueryTokenStream.empty(); - } - } - @Override public QueryTokenStream visitNumericLiteral(HqlParser.NumericLiteralContext ctx) { if (ctx.INTEGER_LITERAL() != null) { return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + } else if (ctx.LONG_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.LONG_LITERAL())); + } else if (ctx.BIG_INTEGER_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.BIG_INTEGER_LITERAL())); } else if (ctx.FLOAT_LITERAL() != null) { return QueryRendererBuilder.from(QueryTokens.token(ctx.FLOAT_LITERAL())); - } else if (ctx.HEXLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.HEXLITERAL())); + } else if (ctx.DOUBLE_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.DOUBLE_LITERAL())); + } else if (ctx.BIG_DECIMAL_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.BIG_DECIMAL_LITERAL())); + } else if (ctx.HEX_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.HEX_LITERAL())); } else { return QueryTokenStream.empty(); } @@ -1238,11 +1236,11 @@ class HqlQueryRenderer extends HqlBaseVisitor { if (ctx.BINARY_LITERAL() != null) { builder.append(QueryTokens.expression(ctx.BINARY_LITERAL())); - } else if (ctx.HEXLITERAL() != null) { + } else if (ctx.HEX_LITERAL() != null) { builder.append(TOKEN_OPEN_BRACE); - builder.append(QueryTokenStream.concat(ctx.HEXLITERAL(), it -> { + builder.append(QueryTokenStream.concat(ctx.HEX_LITERAL(), it -> { return QueryRendererBuilder.from(QueryTokens.token(it)); }, TOKEN_COMMA)); @@ -1425,6 +1423,195 @@ class HqlQueryRenderer extends HqlBaseVisitor { return visit(ctx.parameter()); } + @Override + public QueryTokenStream visitEntityTypeExpression(HqlParser.EntityTypeExpressionContext ctx) { + return visit(ctx.entityTypeReference()); + } + + @Override + public QueryTokenStream visitEntityIdExpression(HqlParser.EntityIdExpressionContext ctx) { + return visit(ctx.entityIdReference()); + } + + @Override + public QueryTokenStream visitEntityVersionExpression(HqlParser.EntityVersionExpressionContext ctx) { + return visit(ctx.entityVersionReference()); + } + + @Override + public QueryTokenStream visitEntityNaturalIdExpression(HqlParser.EntityNaturalIdExpressionContext ctx) { + return visit(ctx.entityNaturalIdReference()); + } + + @Override + public QueryTokenStream visitSyntacticPathExpression(HqlParser.SyntacticPathExpressionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendInline(visit(ctx.syntacticDomainPath())); + + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); + } + + return builder; + } + + @Override + public QueryTokenStream visitPathContinuation(HqlParser.PathContinuationContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(TOKEN_DOT); + builder.append(visit(ctx.simplePath())); + + return builder; + } + + @Override + public QueryTokenStream visitEntityTypeReference(HqlParser.EntityTypeReferenceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.TYPE())); + builder.append(TOKEN_OPEN_PAREN); + + if (ctx.path() != null) { + builder.appendInline(visit(ctx.path())); + } + + if (ctx.parameter() != null) { + builder.appendInline(visit(ctx.parameter())); + } + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.ID())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); + } + + return builder; + } + + @Override + public QueryTokenStream visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.VERSION())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferenceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.NATURALID())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); + } + + return builder; + } + + @Override + public QueryTokenStream visitSyntacticDomainPath(HqlParser.SyntacticDomainPathContext ctx) { + + if (ctx.treatedNavigablePath() != null) { + return visit(ctx.treatedNavigablePath()); + } + + if (ctx.collectionValueNavigablePath() != null) { + return visit(ctx.collectionValueNavigablePath()); + } + + if (ctx.mapKeyNavigablePath() != null) { + return visit(ctx.mapKeyNavigablePath()); + } + + if (ctx.toOneFkReference() != null) { + return visit(ctx.toOneFkReference()); + } + + if (ctx.function() != null) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.function())); + + if (ctx.indexedPathAccessFragment() != null) { + builder.append(visit(ctx.indexedPathAccessFragment())); + } + + if (ctx.slicedPathAccessFragment() != null) { + builder.append(visit(ctx.slicedPathAccessFragment())); + } + + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); + } + + return builder; + } + + if (ctx.indexedPathAccessFragment() != null) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.simplePath())); + builder.append(visit(ctx.indexedPathAccessFragment())); + + return builder; + } + + if (ctx.slicedPathAccessFragment() != null) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.simplePath())); + builder.append(visit(ctx.slicedPathAccessFragment())); + + return builder; + } + + return QueryRenderer.empty(); + } + + @Override + public QueryTokenStream visitSlicedPathAccessFragment(HqlParser.SlicedPathAccessFragmentContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(TOKEN_OPEN_SQUARE_BRACKET); + builder.appendInline(visit(ctx.expression(0))); + builder.append(TOKEN_COLON); + builder.appendInline(visit(ctx.expression(1))); + builder.append(TOKEN_CLOSE_SQUARE_BRACKET); + + return builder; + } + @Override public QueryTokenStream visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { return visit(ctx.function()); @@ -1479,10 +1666,6 @@ class HqlQueryRenderer extends HqlBaseVisitor { return visit(ctx.castFunction()); } - if (ctx.treatedPath() != null) { - return visit(ctx.treatedPath()); - } - if (ctx.extractFunction() != null) { return visit(ctx.extractFunction()); } @@ -1634,7 +1817,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryTokenStream visitPadCharacter(HqlParser.PadCharacterContext ctx) { - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -1908,7 +2091,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryTokenStream visitFormat(HqlParser.FormatContext ctx) { - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -1970,7 +2153,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { return visit(ctx.identifier()); } - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -2378,12 +2561,12 @@ class HqlQueryRenderer extends HqlBaseVisitor { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.treatedPath() != null) { + if (ctx.syntacticDomainPath() != null) { - builder.append(visit(ctx.treatedPath())); + builder.append(visit(ctx.syntacticDomainPath())); - if (ctx.pathContinutation() != null) { - builder.append(visit(ctx.pathContinutation())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } } else if (ctx.generalPathFragment() != null) { builder.append(visit(ctx.generalPathFragment())); @@ -2549,8 +2732,8 @@ class HqlQueryRenderer extends HqlBaseVisitor { nested.append(TOKEN_CLOSE_PAREN); builder.append(nested); - if (ctx.pathContinutation() != null) { - builder.append(visit(ctx.pathContinutation())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } if (ctx.nthSideClause() != null) { @@ -2821,8 +3004,8 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryTokenStream visitTrimCharacter(HqlParser.TrimCharacterContext ctx) { - if (ctx.stringLiteral() != null) { - return visit(ctx.stringLiteral()); + if (ctx.STRING_LITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } return visit(ctx.parameter()); @@ -2899,7 +3082,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { } @Override - public QueryTokenStream visitTreatedPath(HqlParser.TreatedPathContext ctx) { + public QueryTokenStream visitTreatedNavigablePath(HqlParser.TreatedNavigablePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -2914,24 +3097,88 @@ class HqlQueryRenderer extends HqlBaseVisitor { builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); - if (ctx.pathContinutation() != null) { - builder.append(visit(ctx.pathContinutation())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } return builder; } @Override - public QueryTokenStream visitPathContinutation(HqlParser.PathContinutationContext ctx) { + public QueryTokenStream visitCollectionValueNavigablePath(HqlParser.CollectionValueNavigablePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_DOT); - builder.append(visit(ctx.simplePath())); + builder.append(visit(ctx.elementValueQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); + } return builder; } + @Override + public QueryTokenStream visitMapKeyNavigablePath(HqlParser.MapKeyNavigablePathContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.indexKeyQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); + } + + return builder; + } + + @Override + public QueryTokenStream visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.FK())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitElementValueQuantifier(HqlParser.ElementValueQuantifierContext ctx) { + + if (ctx.ELEMENT() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.ELEMENT())); + } + + if (ctx.VALUE() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.VALUE())); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitIndexKeyQuantifier(HqlParser.IndexKeyQuantifierContext ctx) { + + if (ctx.INDEX() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.INDEX())); + } + + if (ctx.KEY() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.KEY())); + } + + return QueryTokenStream.empty(); + } + @Override public QueryTokenStream visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext ctx) { @@ -3176,8 +3423,10 @@ class HqlQueryRenderer extends HqlBaseVisitor { builder.append(QueryTokens.expression(ctx.ESCAPE())); - if (ctx.stringLiteral() != null) { - builder.appendExpression(visit(ctx.stringLiteral())); + if (ctx.STRING_LITERAL() != null) { + builder.append(QueryTokens.expression(ctx.STRING_LITERAL())); + } else if (ctx.JAVA_STRING_LITERAL() != null) { + builder.append(QueryTokens.expression(ctx.JAVA_STRING_LITERAL())); } else if (ctx.parameter() != null) { builder.appendExpression(visit(ctx.parameter())); } @@ -3335,8 +3584,8 @@ class HqlQueryRenderer extends HqlBaseVisitor { builder.append(QueryTokens.expression(ctx.AS())); builder.append(visit(ctx.identifier())); - } else if (ctx.reservedWord() != null) { - builder.append(visit(ctx.reservedWord())); + } else if (ctx.nakedIdentifier() != null) { + builder.append(visit(ctx.nakedIdentifier())); } return builder; @@ -3371,20 +3620,33 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryTokenStream visitIdentifier(HqlParser.IdentifierContext ctx) { - if (ctx.reservedWord() != null) { - return visit(ctx.reservedWord()); - } else { - return QueryTokenStream.empty(); + if (ctx.nakedIdentifier() != null) { + return visit(ctx.nakedIdentifier()); + } else if (ctx.FULL() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.FULL())); + } else if (ctx.LEFT() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.LEFT())); + } else if (ctx.INNER() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.INNER())); + } else if (ctx.OUTER() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.OUTER())); + } else if (ctx.RIGHT() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.RIGHT())); } + + return QueryTokenStream.empty(); } @Override - public QueryTokenStream visitReservedWord(HqlParser.ReservedWordContext ctx) { + public QueryTokenStream visitNakedIdentifier(HqlParser.NakedIdentifierContext ctx) { - if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + if (ctx.IDENTIFIER() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.IDENTIFIER())); + } else if (ctx.QUOTED_IDENTIFIER() != null) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.QUOTED_IDENTIFIER())); } else { return QueryRendererBuilder.from(QueryTokens.token(ctx.f)); } } + } 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 2f9dd0736..067a3adbe 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 @@ -37,7 +37,8 @@ import org.junit.jupiter.params.provider.ValueSource; * * @author Greg Turnquist * @author Christoph Strobl - * @author Mark Paluch, Yannick Brandt + * @author Mark Paluch + * @author Yannick Brandt * @since 3.1 */ class HqlQueryRendererTests { @@ -179,6 +180,180 @@ class HqlQueryRendererTests { """); } + @Test // GH-3711 + void entityTypeReference() { + + assertQuery(""" + SELECT TYPE(e) + FROM Employee e + """); + + assertQuery(""" + SELECT TYPE(?0) + FROM Employee e + """); + } + + @Test // GH-3711 + void entityIdReference() { + + assertQuery(""" + SELECT ID(e) + FROM Employee e + """); + + assertQuery(""" + SELECT ID(e).foo + FROM Employee e + """); + } + + @Test // GH-3711 + void entityNaturalIdReference() { + + assertQuery(""" + SELECT NATURALID(e) + FROM Employee e + """); + + assertQuery(""" + SELECT NATURALID(e).foo + FROM Employee e + """); + } + + @Test // GH-3711 + void entityVersionReference() { + + assertQuery(""" + SELECT VERSION(e) + FROM Employee e + """); + } + + @Test // GH-3711 + void treatedNavigablePath() { + + assertQuery(""" + SELECT TREAT(e as Integer).foo + FROM Employee e + """); + } + + @Test // GH-3711 + void collectionValueNavigablePath() { + + assertQuery(""" + SELECT ELEMENT(e) + FROM Employee e + """); + + assertQuery(""" + SELECT ELEMENT(e).foo + FROM Employee e + """); + + assertQuery(""" + SELECT VALUE(e) + FROM Employee e + """); + + assertQuery(""" + SELECT VALUE(e).foo + FROM Employee e + """); + } + + @Test // GH-3711 + void mapKeyNavigablePath() { + + assertQuery(""" + SELECT KEY(e) + FROM Employee e + """); + + assertQuery(""" + SELECT KEY(e).foo + FROM Employee e + """); + + assertQuery(""" + SELECT INDEX(e) + FROM Employee e + """); + } + + @Test // GH-3711 + void toOneFkReference() { + + assertQuery(""" + SELECT FK(e) + FROM Employee e + """); + + assertQuery(""" + SELECT FK(e.foo) + FROM Employee e + """); + } + + @Test // GH-3711 + void indexedPathAccessFragment() { + + assertQuery(""" + SELECT e.names[0] + FROM Employee e + """); + + assertQuery(""" + SELECT e.payments[1].id + FROM Employee e + """); + + assertQuery(""" + SELECT some_function()[0] + FROM Employee e + """); + + assertQuery(""" + SELECT some_function()[1].id + FROM Employee e + """); + } + + @Test // GH-3711 + void slicedPathAccessFragment() { + + assertQuery(""" + SELECT e.names[0:1] + FROM Employee e + """); + + assertQuery(""" + SELECT e.payments[1:2].id + FROM Employee e + """); + + assertQuery(""" + SELECT some_function()[0:1] + FROM Employee e + """); + + assertQuery(""" + SELECT some_function()[1:2].id + FROM Employee e + """); + } + + @Test // GH-3711 + void functionPathContinuation() { + + assertQuery(""" + SELECT some_function().foo + FROM Employee e + """); + } + @Test void joinsExample1() { @@ -299,7 +474,7 @@ class HqlQueryRendererTests { assertQuery(""" SELECT b.name, b.ISBN FROM Order o JOIN TREAT(o.product AS Book) b - """); + """); } @Test @@ -308,7 +483,7 @@ class HqlQueryRendererTests { assertQuery(""" SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp WHERE lp.budget > 1000 - """); + """); } /** @@ -323,7 +498,7 @@ class HqlQueryRendererTests { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" - """); + """); } @Test @@ -334,7 +509,7 @@ class HqlQueryRendererTests { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE 'cost overrun' - """); + """); } @Test @@ -344,7 +519,7 @@ class HqlQueryRendererTests { SELECT e FROM Employee e WHERE TREAT(e AS Exempt).vacationDays > 10 OR TREAT(e AS Contractor).hours > 100 - """); + """); } @Test @@ -408,7 +583,7 @@ class HqlQueryRendererTests { WHERE emp.salary > ALL (SELECT m.salary FROM Manager m WHERE m.department = emp.department) - """); + """); } @Test @@ -420,7 +595,7 @@ class HqlQueryRendererTests { WHERE EXISTS (SELECT spouseEmp FROM Employee spouseEmp WHERE spouseEmp = emp.spouse) - """); + """); } @Test @@ -488,7 +663,7 @@ class HqlQueryRendererTests { WHEN e.rating = 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -501,7 +676,7 @@ class HqlQueryRendererTests { WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -541,7 +716,7 @@ class HqlQueryRendererTests { SELECT e FROM Employee e WHERE TYPE(e) IN (Exempt, Contractor) - """); + """); } @Test @@ -1509,13 +1684,13 @@ class HqlQueryRendererTests { }); } - @Test + @Test // GH-3711 void ceilingFunctionShouldWork() { assertQuery("select ceiling(1.5) from Element a"); } - @Test - void lnFunctionSouldWork() { + @Test // GH-3711 + void lnFunctionShouldWork() { assertQuery("select ln(7.5) from Element a"); } @@ -1568,10 +1743,18 @@ class HqlQueryRendererTests { void durationLiteralsShouldWork(String dtField) { assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE (ce.endDate - ce.startDate) > 5 %s".formatted(dtField)); - assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE ce.text LIKE :text GROUP BY year(cd.date) HAVING (ce.endDate - ce.startDate) > 5 %s".formatted(dtField)); + assertQuery( + "SELECT ce.id FROM CalendarEvent ce WHERE ce.text LIKE :text GROUP BY year(cd.date) HAVING (ce.endDate - ce.startDate) > 5 %s" + .formatted(dtField)); assertQuery("SELECT ce.id as id, cd.startDate + 5 %s AS summedDate FROM CalendarEvent ce".formatted(dtField)); } + @ParameterizedTest // GH-3711 + @ValueSource(strings = { "1", "1_000", "1L", "1_000L", "1bi", "1.1f", "2.2d", "2.2bd" }) + void numberLiteralsShouldWork(String literal) { + assertQuery(String.format("SELECT %s FROM User u where u.id = %s", literal, literal)); + } + @Test // GH-3025 void binaryLiteralsShouldWork() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 40aa7d274..2ee28f804 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -924,7 +924,7 @@ class HqlQueryTransformerTests { and iu = u ) and ct.id = :teamId - """, relationshipName, joinAlias, joinAlias)); + """, relationshipName, joinAlias, joinAlias)); } static Stream queriesWithReservedWordsAsIdentifiers() { @@ -933,7 +933,6 @@ class HqlQueryTransformerTests { Arguments.of("right", "rt"), // Arguments.of("left", "lt"), // Arguments.of("outer", "ou"), // - Arguments.of("full", "full"), // Arguments.of("inner", "inr")); }