From 12b5e8205db3c6a7b88668c66c39f23de1e2b992 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 80e9297840..4b0bb9a14c 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 0e6c5cab02..259556e542 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 @@ public Void visitInstantiation(HqlParser.InstantiationContext ctx) { } 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 2ef49b95ff..311fdce1d3 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 @@ public QueryTokenStream visitLiteral(HqlParser.LiteralContext ctx) { 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 @@ public QueryTokenStream visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) } } - @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 @@ public QueryTokenStream visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { 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 @@ public QueryTokenStream visitParameterExpression(HqlParser.ParameterExpressionCo 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 @@ public QueryTokenStream visitStandardFunction(HqlParser.StandardFunctionContext return visit(ctx.castFunction()); } - if (ctx.treatedPath() != null) { - return visit(ctx.treatedPath()); - } - if (ctx.extractFunction() != null) { return visit(ctx.extractFunction()); } @@ -1634,7 +1817,7 @@ public QueryTokenStream visitPadSpecification(HqlParser.PadSpecificationContext @Override public QueryTokenStream visitPadCharacter(HqlParser.PadCharacterContext ctx) { - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -1908,7 +2091,7 @@ public QueryTokenStream visitRollup(HqlParser.RollupContext ctx) { @Override public QueryTokenStream visitFormat(HqlParser.FormatContext ctx) { - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -1970,7 +2153,7 @@ public QueryTokenStream visitJpaNonstandardFunctionName(HqlParser.JpaNonstandard return visit(ctx.identifier()); } - return visit(ctx.stringLiteral()); + return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); } @Override @@ -2378,12 +2561,12 @@ public QueryTokenStream visitPath(HqlParser.PathContext ctx) { 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 @@ public QueryTokenStream visitGenericFunction(HqlParser.GenericFunctionContext ct 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 @@ public QueryTokenStream visitTrimSpecification(HqlParser.TrimSpecificationContex @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 @@ public QueryTokenStream visitAnyFunction(HqlParser.AnyFunctionContext ctx) { } @Override - public QueryTokenStream visitTreatedPath(HqlParser.TreatedPathContext ctx) { + public QueryTokenStream visitTreatedNavigablePath(HqlParser.TreatedNavigablePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -2914,24 +3097,88 @@ public QueryTokenStream visitTreatedPath(HqlParser.TreatedPathContext ctx) { 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 @@ public QueryTokenStream visitStringPatternMatching(HqlParser.StringPatternMatchi 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 @@ public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { 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 @@ public QueryTokenStream visitEntityName(HqlParser.EntityNameContext ctx) { @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 2f9dd07363..067a3adbe4 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 @@ * * @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 @@ void pathExpressionSyntaxExample1() { """); } + @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 @@ void fromClauseDowncastingExample1() { assertQuery(""" SELECT b.name, b.ISBN FROM Order o JOIN TREAT(o.product AS Book) b - """); + """); } @Test @@ -308,7 +483,7 @@ void fromClauseDowncastingExample2() { assertQuery(""" SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp WHERE lp.budget > 1000 - """); + """); } /** @@ -323,7 +498,7 @@ void fromClauseDowncastingExample3_SPEC_BUG() { 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 @@ void fromClauseDowncastingExample3fixed() { 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 @@ void fromClauseDowncastingExample4() { SELECT e FROM Employee e WHERE TREAT(e AS Exempt).vacationDays > 10 OR TREAT(e AS Contractor).hours > 100 - """); + """); } @Test @@ -408,7 +583,7 @@ void allExample() { WHERE emp.salary > ALL (SELECT m.salary FROM Manager m WHERE m.department = emp.department) - """); + """); } @Test @@ -420,7 +595,7 @@ void existsSubSelectExample2() { WHERE EXISTS (SELECT spouseEmp FROM Employee spouseEmp WHERE spouseEmp = emp.spouse) - """); + """); } @Test @@ -488,7 +663,7 @@ void updateCaseExample1() { WHEN e.rating = 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -501,7 +676,7 @@ void updateCaseExample2() { WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -541,7 +716,7 @@ void theRest() { SELECT e FROM Employee e WHERE TYPE(e) IN (Exempt, Contractor) - """); + """); } @Test @@ -1509,13 +1684,13 @@ select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc }); } - @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 @@ void castFunctionWithFqdnShouldWork() { 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 40aa7d274b..2ee28f804a 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 @@ where exists ( and iu = u ) and ct.id = :teamId - """, relationshipName, joinAlias, joinAlias)); + """, relationshipName, joinAlias, joinAlias)); } static Stream queriesWithReservedWordsAsIdentifiers() { @@ -933,7 +933,6 @@ static Stream queriesWithReservedWordsAsIdentifiers() { Arguments.of("right", "rt"), // Arguments.of("left", "lt"), // Arguments.of("outer", "ou"), // - Arguments.of("full", "full"), // Arguments.of("inner", "inr")); }